MCP server - Spacefast Docs
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 |
{
"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 |
{ "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 |
{
"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 |
{ "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.
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:
{ "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.
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.
{ "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.
SPACEFAST_MCP_WORKSPACE_ROOT, or the--workspace-rootflag onsf mcp.- The first
file:root your MCP client advertises. - 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.