# MCP server

Connect to the Spacefast MCP server over hosted HTTP or local stdio, and use its five tools, execute sandbox, and approval model.

After this page you can connect any MCP client to Spacefast, read every tool’s input schema, write `execute` programs against the whole Spacefast API, and know exactly when a call will stop and ask you for approval.

## Two runtimes, one tool set

The same server ships in two runtimes. Both register the same five tools. The difference is the filesystem.

|  | Hosted | Local |
| --- | --- | --- |
| Transport | Streamable HTTP | stdio |
| Endpoint or command | `https://mcp.spacefast.com` | `sf mcp`, or `npx -y spacefast mcp` |
| Filesystem | None. `publish` requires inline `files` | Reads your workspace. `publish` takes a `path` or publishes the current workspace |
| Auth | OAuth, per client | Your CLI login, or `SPACEFAST_TOKEN` |
| Where it runs | Spacefast | Your machine |

The hosted endpoint also answers on `/mcp` and `/mcp/`. Only the `mcp.` hostname serves MCP; the path `/mcp` on the API host does not.

There is a third lane, `sf mcp proxy`, which speaks stdio to your editor and HTTPS to the hosted endpoint using your CLI login.

## Authentication

### Hosted

The hosted server is an OAuth 2.0 protected resource. It is never anonymous. A request with no token gets a `401` and a `WWW-Authenticate` challenge that points at the resource metadata document, and your client walks the rest from there. Bearer and DPoP are both accepted; a DPoP-shaped request gets a DPoP challenge back.

By default a client asks for these scopes.

| Scope | What it grants |
| --- | --- |
| `teams:read` | Read the teams the account belongs to |
| `teams:create` | Create teams |
| `spaces:read` | Read spaces, versions, and build status |
| `spaces:write` | Create and update spaces and their settings |
| `spaces:rename` | Rename space slugs and managed addresses |
| `publish:write` | Publish new immutable versions |
| `domains:read` | Read domain inventory and DNS diagnostics |
| `domains:write` | Attach, verify, and mutate domains and DNS records |
| `offline_access` | Refresh access without another sign-in |

Each tool declares the scopes it needs, and clients can read them from `tools/list` before calling.

| Tool | Required scopes |
| --- | --- |
| `publish` | `publish:write` |
| `operation_status` | `spaces:read` |
| `show_space` | The scopes required by the selected view |
| `execute`, `resume_execution` | The called API operation checks the credential’s scopes |

Your client never receives a Spacefast API token. Each authenticated request mints an internal delegation token that lives five minutes.

Hosted requests are rate limited to 600 per minute per credential and 300 per minute per IP.

### Local

The local server takes its credential from the process, never from client config. `sf mcp` resolves your CLI login and hands it to the in-process server. Standalone runs read `SPACEFAST_TOKEN`.

| Variable | Effect |
| --- | --- |
| `SPACEFAST_TOKEN` | Bearer token for API calls |
| `SPACEFAST_API_URL` | API base URL |
| `SPACEFAST_MCP_WORKSPACE_ROOT` | Pins the one directory `publish` may read from |

## The tools

| Tool | What it does | Key inputs | Returns |
| --- | --- | --- | --- |
| `publish` | Uploads files and creates an immutable version | `path` or `files`, `spaceId`, `teamId`, `access` | Publish receipt and status |
| `operation_status` | Checks a publish that is still running | `operationId` | Status, progress, diagnostics, `done` |
| `show_space` | Opens a read-only Space view | `request.view`, `request.input` | The selected view and its data |
| `execute` | Runs a JavaScript program against the API catalog | `code` | Program result or a pause for approval |
| `resume_execution` | Continues a paused program | `executionId` and the requested decision fields | The continued program’s result |

Every result is capped at 24,000 characters across the whole structured value.

### publish

`publish` is synchronous by default. It waits for the version to finalize and returns the receipt.

| Field | Type | What it does |
| --- | --- | --- |
| `path` | string | Local runtime only. One local directory, file, or archive. Omit `path` and `files` to publish the current workspace |
| `files` | array of 1 to 1000 | Inline files. Required on hosted. Each is `{ path, content, encoding }` with `encoding` one of `utf8` or `base64`, default `utf8` |
| `spaceId` | string starting `spc_` | Update this Space. Omit to create one, or on local to use the Space linked to this workspace |
| `teamId` | string | Destination team id or slug for a new Space. Send one after a `team_required` error |
| `access` | `private`, `team`, or `public` | Initial root access for a Space this publish creates. Invalid with `spaceId` |
| `createNew` | boolean, default `false` | Local runtime only. Forces a separate Space. Invalid with `spaceId` |
| `slug` | string, 2 to 55 | Slug for a new Space |
| `title` | string, 1 to 255 | Title for a new Space |
| `description` | string, up to 2000 | Changelog for this version. It does not change the Space description |
| `requestId` | string, 16 to 200 | Idempotency handle. Reuse the same value only when retrying a call that timed out |

```json
{
  "path": "dist",
  "description": "Fix the pricing table"
}
```

The result carries `status` of `publishing` or `published`, `liveUrl`, `immutableUrl`, and a `receipt` with the Space, the version, and any diagnostics. When `status` is `publishing`, `result.job.poll.operationId` is the handle for `operation_status`.

### operation_status

| Field | Type | What it does |
| --- | --- | --- |
| `operationId` | string, required | Copy `publish.result.job.poll.operationId` |

```json
{ "operationId": "opr_7f2b8c1d4e5a6b7c" }
```

Poll about every two seconds. Stop when `result.done` is true. Report success only when `result.status` is `succeeded`.

### show_space

Choose a view, then provide the input that view requires. Detailed views require a claimed Space.

| View | What it shows |
| --- | --- |
| `deployments` | Deployment history and build status |
| `domains` | Domains, DNS records, and TLS status |
| `analytics` | Traffic over the selected window |
| `source_files` | Repository files and folders |
| `source_changes` | Pending source workspace changes |
| `source_history` | Repository commits |
| `source_comparison` | Differences between source commits |
| `deployment_comparison` | Differences between published versions |
| `build_logs` | Logs from one build |

```json
{
  "request": {
    "view": "deployments",
    "input": { "space": "docs.example.com" }
  }
}
```

### execute

| Field | Type | What it does |
| --- | --- | --- |
| `code` | string, required | One complete JavaScript program |

### resume_execution

The input depends on where the approval decision is made.

| Field | Type | What it does |
| --- | --- | --- |
| `executionId` | string, required | The id from the paused `execute` result |

```json
{ "executionId": "exe_9c3f1b7a20d4" }
```

## The execute sandbox

`execute` runs your program in QuickJS inside a worker. The whole Spacefast OpenAPI catalog is mounted as a lazy `tools` proxy.

| Limit | Value |
| --- | --- |
| Runtime | 30 seconds |
| Memory | 64 MiB |
| Stack | 1 MiB |
| Network | No `fetch`. Only `tools.*` |

### Find the Space, then publish to it

`execute` is not the publish path. Use it to resolve which Space you want, then call `publish` with the id.

```javascript
const page = await tools.search({ query: "list spaces", limit: 10 });
const match = page.items.find((item) => item.path.endsWith(".spaces.listSpaces"));
if (!match) return { error: "No matching list-spaces operation." };

const shape = await tools.describe.tool({ path: match.path });
if (shape.error) return { error: shape.error };

const result = await tools[match.path]({});
if (!result.ok) return { error: result.error };

const spaces = result.data?.data ?? result.data;
return spaces.map((space) => ({ id: space.id, slug: space.slug }));
```

Then, with the id in hand:

```json
{ "spaceId": "spc_4a91c0de2f3b", "path": "dist" }
```

### Add a domain, then watch it

One program creates the domain and binds it to the Space. Use `show_space` with the `domains` view to inspect DNS and TLS afterward.

```javascript
const found = await tools.search({ query: "create domain for space", limit: 10 });
const create = found.items.find((item) => item.path.endsWith(".domains.createDomain"));
if (!create) return { error: "No matching create-domain operation." };

const shape = await tools.describe.tool({ path: create.path });
if (shape.error) return { error: shape.error };

const added = await tools[create.path]({
  body: { name: "docs.example.com", teamId: "tm_2b7d40a9" },
});
if (!added.ok) return { error: added.error };
const domain = added.data?.data ?? added.data;

const page = await tools.search({ query: "assign domain to space", limit: 10 });
const bind = page.items.find((item) => item.path.endsWith(".domains.updateDomain"));
if (!bind) return { error: "No matching update-domain operation." };

const bound = await tools[bind.path]({
  path: { domainId: domain.id },
  body: { spaceId: "spc_4a91c0de2f3b", primary: true },
});
return bound.ok ? { domainId: domain.id, name: domain.name } : { error: bound.error };
```

Then watch the DNS and TLS state with a read tool until it settles.

```json
{ "space": "spc_4a91c0de2f3b" }
```

## Approvals

A write inside an `execute` program can pause before its HTTP request goes out.

The program pauses

`execute` returns `status: "user_approval_required"` with an `executionId`, an `approvalUrl`, and a `resumePrompt`.

You review it in the browser

The `approvalUrl` opens the approvals page in the dashboard.

The agent resumes

The agent calls `resume_execution` with the same `executionId`. That call blocks until your decision lands, then the saved program continues from where it stopped.

## Local file access

Only the local runtime reads files, and it reads them from exactly one directory.

The workspace root is resolved in this order.

1. `SPACEFAST_MCP_WORKSPACE_ROOT`, or the `--workspace-root` flag on `sf mcp`.
2. The first `file:` root your MCP client advertises.
3. The directory the server process started in.

Setting the environment variable turns off root discovery. The variable wins and your client’s advertised root is ignored.

Some files never upload, at any depth. `.env`, `.git`, `node_modules`, `.ssh`, and other sensitive files are disallowed.
