> ## Documentation Index
> Fetch the complete documentation index at: https://docs.stage.systems/llms.txt
> Use this file to discover all available pages before exploring further.

# Stage MCP

> The remote MCP server on the shared API: how it authenticates, the tool catalog, and how a client site offers its own tools to agents.

One remote [MCP](https://modelcontextprotocol.io) server, `POST https://api.stage.systems/mcp`, in
the shared API. Every org connects the same URL; the tools a connection sees are generated per
request from the caller's org, role, scopes and the org's enabled tools. Content edits go through
the same id + path patch and CAS model as `/admin`. Runbook and architecture: `docs/mcp.md` in the
platform repo.

## The one rule

A connection can only ever touch the site(s) it is bound to. A credential names them (an API key's
row, or the sites picked at OAuth consent); tools take no free `org_id`; every tool runs the same
`canActOnOrg` + `canDo` gates as the REST routes; the role is the ceiling and scopes only narrow it.

A connection approved for several sites ("All sites" at consent) gives every tool a required
`site` argument (name, slug or id). The role can differ per site, so a scope approved because
one site allows it is still refused on a site whose role does not. `stage_whoami` lists the
sites. A site's own (federated) tools are offered on single-site connections only.

## Authentication

| Credential | For | How |
| - | - | - |
| OAuth 2.1 | claude.ai, Claude Desktop, Cowork, Claude Code, Cursor, ChatGPT | The API is its own authorization server: RFC 9728/8414 discovery, Client ID Metadata Documents (DCR as fallback), PKCE S256, a consent page with the Stage sign-in + site picker + scope ticks, 1 h access tokens, 30 d rotating refresh tokens |
| API key `stg_live_…` | Claude Code, scripts, CI | Minted in Settings → Apps & API (admins); `Authorization: Bearer …` |
| Session JWT | local rigs | A Supabase session token works as the bearer; multi-org users must use a key |

Unauthenticated requests get a real `401` with `WWW-Authenticate: Bearer
resource_metadata="https://api.stage.systems/.well-known/oauth-protected-resource/mcp" scope="…"`.

Scopes: `content:read content:write publish media:read media:write analytics:read campaigns:read
campaigns:write campaigns:publish inbox:read booking site:tools` (+ `fleet`, staff). A tool is
offered when the credential has the scope **and** the role holds the capability **and**, for
flagged tools, the org has the flag.

## Tools

| Tool | Scope |
| - | - |
| `stage_whoami` | — |
| `stage_pages_list`, `stage_content_get {kind, slug, format}`, `stage_content_search {query}`, `stage_schema_get` | content:read |
| `stage_content_patch {kind, slug, patches:[{id, path, value}]}`, `stage_content_add`, `stage_content_remove`, `stage_page_create`, `stage_article_create`, `stage_content_delete {confirm}` | content:write |
| `stage_publish` | publish |
| `stage_media_search`, `stage_media_upload {source_url}` (image or video by public URL), `stage_upload_ticket {kind, filename}` (a local file: one-off PUT url + curl line), `stage_media_status {id}` (video progress → `mux:<id>` url) | media:read / media:write |
| `stage_analytics_overview {range}`, `stage_analytics_sections {path}`, `stage_funnels`, `stage_experiments`, `stage_signups` | analytics:read |
| `stage_inbox_list`, `stage_inbox_person` | inbox:read |
| `stage_social_accounts`, `stage_campaigns_list`, `stage_campaign_get/metrics/funnel/replies` | campaigns:read + `social_tool` |
| `stage_campaign_create/update/request_approval/short_link/delete` | campaigns:write + `social_tool` |
| `stage_campaign_approve` | campaigns:publish |
| `stage_booking_overview`, `stage_bookings_list` | booking |
| `stage_fleet_overview {filter}`, `stage_fleet_org {org}` — the studio's command center (the fleet org only) | fleet |
| `stage_request {note}` | every role |
| `site_<name>` | site:tools (see below) |

`stage_content_get` with `format: "outline"` returns each component's stable id, type and its
text fields as `{path, value}` pairs (`props.title`, `props.items.2.label`) — exactly what
`stage_content_patch` addresses. Every write is sanitised server-side, saved with version CAS +
three-way merge, stamped `updated_by_name = "<email> via MCP"`, and logged to the activity feed.

## What the site must serve

Two optional routes, both in the template:

* **`GET /stage/schema.json`** (`src/pages/stage/schema.json.ts`) — the site's `ADMIN_SCHEMA` as
  JSON. The API reads it (cached 5 min) so `stage_schema_get` and `stage_content_add` know the
  arrays, item fields and page palettes. Without it, adds still work by copying a sibling item.
  Health check: `platform.schema-route`.
* **Your own tools** — see below. Health check: `platform.site-tools`.

## Give the site its own tools

A client repo can offer agents tools only it knows how to run — sync the shop's products, rebuild a
feed, run a report. Declare them in `src/stage/mcp-tools.ts`:

```ts theme={null}
import type { FederatedToolDef, FederatedPrincipal } from "@sp-stage/sdk";

export const SITE_TOOLS = [
  {
    name: "sync_products",                       // listed to agents as site_sync_products
    title: "Sync products",
    description: "Pull the shop's products into the site. Returns how many changed.",
    input_schema: { type: "object", properties: { dry_run: { type: "boolean" } } },
    annotations: { readOnlyHint: false, destructiveHint: false },
    async run(args, principal: FederatedPrincipal) { /* … */ return { changed: 12 }; },
  },
] satisfies (FederatedToolDef & { run: Function })[];
```

The template's `src/pages/stage/mcp-tools.json.ts` serves the manifest and
`src/pages/stage/mcp-tools/[name].ts` runs a tool. The Stage API:

1. reads the manifest through its DNS-pinned transport (cached 5 min), validates each tool and
   lists it as `site_<name>` to credentials that carry `site:tools` and an editor+ role;
2. validates the arguments against the tool's JSON Schema before anything leaves Stage;
3. forwards the call `POST /stage/mcp-tools/<name>` with
   `X-Stage-Signature: t=<unix>,v1=<hex HMAC-SHA256(secret, "<t>.<body>")>` and a body of
   `{ arguments, principal: { email, role, org_id, via } }`.

The route verifies with `verifyStageSignature(secret, header, rawBody)` from the SDK (WebCrypto,
five-minute skew, constant-time compare) and returns `{ content }` or `{ error }`. The site decides
what a given role may run; it never sees a Stage token.

Switching a site on is a studio action: `scripts/mcp-client-tools.ts <orgId> --on` sets
`settings.mcp_client_tools`, mints the per-org secret, and prints it once for the site's
`STAGE_MCP_TOOLS_SECRET` env.

## Testing

The platform repo has six suites under `apps/api/test/mcp-*.mjs` (isolation, writes, OAuth as a
client, the consent page in real Chrome, campaigns, site tools) and an **eval set**
(`mcp-eval.mjs`): fifteen realistic tasks run through headless Claude Code against a seeded
throwaway org, each with a hard check on what must — or must not — have changed, reporting tool
calls, turns, time and cost per task.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.