Connect an external MCP client
SupaCloud exposes a developer-facing MCP gateway at POST /api/mcp. It is a
standard Model Context Protocol Streamable-HTTP
endpoint (stateless JSON-RPC 2.0), so any conformant MCP client — a local IDE, a
coding agent, or a hand-rolled curl — can list and call a curated set of
SupaCloud tools. Auth is a scoped scmt_ management token, not an OAuth flow:
SupaCloud is an OIDC relying party, not an Authorization Server, so the gateway
never serves /token or /.well-known. Follow these steps to wire a client to it.
Prerequisites
Section titled “Prerequisites”- The instance must run the enterprise edition. The gateway is gated by the
mcp_gateway_externalfeature (fail-closed ininterfaces/http/external_mcp.rs); on a Community instance every request to/api/mcpreturns403. - You need permission to mint a management token for the target Workspace (the token must be workspace-scoped — a global/bootstrap token is rejected).
-
Mint a scoped
scmt_token.The gateway reuses the existing Management-API token (
scmt_…, hash-only), extended with two MCP scopes:mcp:read— the read tools (list/get/observe), includingrun.list/run.get/run.events.mcp:ops— impliesmcp:readand adds the ops + script-run tools (workflow.trigger,script.run, the wholeschedule.*group, issue/PR/ repo-sync ops). Use this when your local dev should actually run workflows and scripts, not just watch.
Create the token via the Management API. The response carries the plaintext bearer once — it is never re-readable:
Terminal window curl -sS -X POST \"$SUPACLOUD_URL/api/management/v1/workspaces/$WORKSPACE_SLUG/api-tokens" \-H "Authorization: Bearer $BOOTSTRAP_MGMT_TOKEN" \-H "Content-Type: application/json" \-d '{ "label": "local-ide-mcp", "scopes": ["mcp:ops"] }'The returned
tokenfield (startingscmt_) is your MCP bearer. Store it in$SC_MCP_TOKEN. -
Point your MCP client at the gateway.
The transport is Streamable HTTP, stateless — a single endpoint, no
Mcp-Session-Id, every request self-contained from the bearer. Most clients take an HTTP MCP server URL plus a staticAuthorizationheader. For example, a local IDE / agent MCP config:{"mcpServers": {"supacloud": {"url": "https://your-instance.example.com/api/mcp","headers": {"Authorization": "Bearer scmt_your_token_here"}}}}The gateway advertises protocol version
2025-06-18and atoolscapability in itsinitializeresult. -
List the tools your token can reach.
tools/listreturns only the tools on the external surface for your scope. A read token sees the read tier; an ops token additionally sees ops + script-run:Terminal window curl -sS -X POST "$SUPACLOUD_URL/api/mcp" \-H "Authorization: Bearer $SC_MCP_TOKEN" \-H "Content-Type: application/json" \-d '{ "jsonrpc": "2.0", "id": 1, "method": "tools/list" }'{"jsonrpc": "2.0","id": 1,"result": {"tools": [{ "name": "run.list", "description": "…", "inputSchema": { … }, "outputSchema": { … } }]}} -
Call a tool.
tools/calltakes anameand an optionalargumentsobject. A read example (lists recent runs in the workspace):Terminal window curl -sS -X POST "$SUPACLOUD_URL/api/mcp" \-H "Authorization: Bearer $SC_MCP_TOKEN" \-H "Content-Type: application/json" \-d '{"jsonrpc": "2.0","id": 2,"method": "tools/call","params": { "name": "run.list", "arguments": {} }}'A successful call returns a result whose
structuredContentis the typed tool output (thecontenttext block is the same value as a JSON string):{"jsonrpc": "2.0","id": 2,"result": {"content": [ { "type": "text", "text": "{\"runs\":[…]}" } ],"structuredContent": { "runs": [ … ] },"isError": false}}An ops example (requires an
mcp:opstoken) triggers a workflow:Terminal window curl -sS -X POST "$SUPACLOUD_URL/api/mcp" \-H "Authorization: Bearer $SC_MCP_TOKEN" \-H "Content-Type: application/json" \-d '{"jsonrpc": "2.0","id": 3,"method": "tools/call","params": { "name": "workflow.trigger", "arguments": { "workflow_id": "…" } }}'
What your token can reach
Section titled “What your token can reach”The surface is a positive filter (services/mcp/external.rs::external_tool_allowed)
applied to both tools/list and tools/call, on top of the inherited tier
allowlist:
| Scope | Reachable tools |
|---|---|
mcp:read |
Read tier — run.list, run.get, run.events, workflow.list/get, project.list, issue.list/get, conversation reads, app-build/migration reads, memory.list_closeouts |
mcp:ops (adds) |
Ops + ScriptRun — workflow.trigger, script.run, the whole schedule.* group (create/update/pause/resume), issue/PR/repo-sync ops |
A few read-tier tools are deliberately excluded because they need an agent
identity an M2M token doesn’t have: memory.create (profile-gated write) and
permission.list. On the ops surface, permission.grant / permission.revoke
are excluded for the same reason.
What it does NOT do (by design)
Section titled “What it does NOT do (by design)”- The Deploy tier is never reachable.
app.deploy/app.migration.applyare excluded by the surface filter and the external allowlist set, and they would require the HMAC confirmation secret an external caller never holds. Deploy stays inside the agent runner. - No token pass-through. Every tool call runs through SupaCloud’s internal
services::*— which act as their own OAuth client against GitHub/Linear/etc. Yourscmt_token never leaves SupaCloud. - It is not an OAuth provider. There is no
/tokenendpoint, no JWKS, no audience model. Full OAuth 2.1 (DCR/PRM/PKCE) is a deferred Authelia work package, fronting the gateway — not built into SupaCloud.
Rate limits
Section titled “Rate limits”The external identity is the token row, so limits key on the token, not a user:
- A per-identity in-process burst limit stops one token from flooding the gateway.
- A per-identity hourly ops cap — the external analog of the per-profile cap —
counts the token’s own ops audit rows in a rolling hour. Default 30, tunable
via
AGENT_MCP_EXTERNAL_OPS_HOURLY_LIMIT. Over the cap, an ops call returns aForbiddentool error. - An ops call also reserves a slot against the shared workspace daily ops cap.
So a single developer’s token cannot drain the workspace ops budget on its own.
Troubleshooting
Section titled “Troubleshooting”401 Unauthorized— missing or emptyAuthorization: Bearerheader, or a token that does not resolve.403 Forbiddenat the transport — one of: the instance is not enterprise edition (mcp_gateway_externaldisabled); the token is global/bootstrap, not workspace-scoped; or a presentOriginheader does not match the control-plane host (the DNS-rebind guard — an absent Origin, as a server-to-server client sends, is allowed).- JSON-RPC error
-32601“tool not available on the external surface” — the tool is not on your scope’s surface. A read token calling an ops tool, or any caller hitting an excluded tool (deploy,memory.create,permission.*), lands here. Mint anmcp:opstoken if you need the ops tier. - JSON-RPC error
-32700“parse error” — the request body was not a JSON object. "isError": truein atools/callresult — the call ran but the tool failed (HTTP is still200). Server-class detail (SQL/vault/internal) is redacted to a generic message; the raw error is logged server-side.- Ops call rejected as
Forbidden— you have hit the hourly ops cap (default 30) or the workspace daily ops cap.
See also
Section titled “See also”server/src/interfaces/http/external_mcp.rs— thePOST /api/mcphandlerserver/src/services/mcp/external.rs— theexternal_tool_allowedsurface filter- ADR 0054 — the secure-external-MCP-exposure design