Skip to content
Select themeSelect language

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.

  • The instance must run the enterprise edition. The gateway is gated by the mcp_gateway_external feature (fail-closed in interfaces/http/external_mcp.rs); on a Community instance every request to /api/mcp returns 403.
  • You need permission to mint a management token for the target Workspace (the token must be workspace-scoped — a global/bootstrap token is rejected).
  1. 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), including run.list / run.get / run.events.
    • mcp:ops — implies mcp:read and adds the ops + script-run tools (workflow.trigger, script.run, the whole schedule.* 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 token field (starting scmt_) is your MCP bearer. Store it in $SC_MCP_TOKEN.

  2. 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 static Authorization header. 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-18 and a tools capability in its initialize result.

  3. List the tools your token can reach.

    tools/list returns 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": { } }
    ]
    }
    }
  4. Call a tool.

    tools/call takes a name and an optional arguments object. 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 structuredContent is the typed tool output (the content text 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:ops token) 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": "…" } }
    }'

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.

  • The Deploy tier is never reachable. app.deploy / app.migration.apply are 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. Your scmt_ token never leaves SupaCloud.
  • It is not an OAuth provider. There is no /token endpoint, 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.

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 a Forbidden tool 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.

  • 401 Unauthorized — missing or empty Authorization: Bearer header, or a token that does not resolve.
  • 403 Forbidden at the transport — one of: the instance is not enterprise edition (mcp_gateway_external disabled); the token is global/bootstrap, not workspace-scoped; or a present Origin header 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 an mcp:ops token if you need the ops tier.
  • JSON-RPC error -32700 “parse error” — the request body was not a JSON object.
  • "isError": true in a tools/call result — the call ran but the tool failed (HTTP is still 200). 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.