diff --git a/content/docs/ai/connect-mcp.mdx b/content/docs/ai/connect-mcp.mdx index f3f41db772..3f74dc6399 100644 --- a/content/docs/ai/connect-mcp.mdx +++ b/content/docs/ai/connect-mcp.mdx @@ -86,6 +86,36 @@ OAuth requires TLS — plain-HTTP deployments (except `localhost`) fall back to run insecurely. +## Local stdio transport (opt-in) + +Alongside the HTTP endpoint, the runtime can also speak MCP over a long-lived +**stdio** transport — a local pipe for a client on the same host rather than a +network surface. It is **off by default** and deliberately stricter than HTTP; +turn it on at boot with two environment variables: + +```bash +OS_MCP_STDIO_ENABLED=true OS_MCP_STDIO_API_KEY=osk_... os start +``` + +The HTTP surface authenticates **each caller** (OAuth as a person, or their own +API key). A stdio pipe has no per-request identity to resolve, so it runs as a +**single principal you pin up front** — the identity of `OS_MCP_STDIO_API_KEY`. +That key is resolved through the **same** verify + authorization chain as an +HTTP/REST request and **re-checked on every call**, so RBAC, row-level +security, field-level security, and tenant isolation apply to the stdio session +exactly as they would to that identity in the Console — and revoking the key +stops the session on its next call. + +The transport is **fail-closed**: a missing, unknown, revoked, expired, or +owner-less key makes stdio **refuse to start** rather than fall back to an +unscoped or `system` session — there is deliberately no bypass. For a +full-authority local agent, mint the key on a platform-admin or dedicated +**service** identity; for a scoped one, mint it on a user with exactly the +access it should have. See +[environment variables](/docs/deployment/environment-variables#mcp-server) for +the switches and [ADR-0101](/adr/0101-mcp-stdio-principal-admission) for the +decision. + ## What the agent gets Eleven tools, generated from your metadata: @@ -159,8 +189,9 @@ skill and a guided `/objectstack:connect` command. | Symptom | Cause → fix | |:---|:---| -| `404` on `/api/v1/mcp` | The surface is disabled — unset `OS_MCP_SERVER_ENABLED` (default is on) | +| `404` on `/api/v1/mcp` | The HTTP surface is disabled — unset `OS_MCP_SERVER_ENABLED` (default is on) | | `501 Not Implemented` | The MCP plugin isn't part of this build — check your stack's plugins | +| stdio won't start / boot fails closed | `OS_MCP_STDIO_ENABLED=true` but `OS_MCP_STDIO_API_KEY` is missing, unknown, revoked, or expired — fail-closed by design (ADR-0101). Set a valid `osk_` key; there is no unscoped or `system` fallback | | `401` on every call | Anonymous or invalid credentials. Interactive clients: complete the browser login (the `WWW-Authenticate` header advertises the OAuth metadata). Headless: check the `osk_` key and header spelling | | `403 insufficient_scope` | The OAuth token lacks the scope for that tool family (e.g. writes without `data:write`) — reconnect and grant the scope | | An action is missing from `list_actions` | `ai.exposed` is not `true`, `ai.description` is shorter than 40 characters, the type isn't headless-callable here (only `script` with a body/handler and `flow` with an automation service are — `url`, `modal`, `form`, and `api` never appear), it targets a `sys_*` object, or the caller fails its `requiredPermissions` | diff --git a/content/docs/permissions/authorization.mdx b/content/docs/permissions/authorization.mdx index 061e9d6996..8ca869adfa 100644 --- a/content/docs/permissions/authorization.mdx +++ b/content/docs/permissions/authorization.mdx @@ -66,6 +66,33 @@ strict containment for scope grants), and everyone else is denied. The `everyone`/`guest` audience-anchor bindings reject high-privilege sets at the data layer for every caller. +### The MCP execution surface (ADR-0096 / ADR-0101) + +The serve-side MCP server (`@objectstack/mcp`) is a first-class **execution +surface**, not a side door around the chain above. Whatever an agent does over +MCP reaches object data only through the **same** gates 3–6 as a REST request: +a per-request, **principal-bound bridge** runs every tool through `callData` +with the caller's `ExecutionContext`, so object CRUD, OWD/sharing, RLS, and FLS +apply identically. Identity is admitted **fail-closed at each transport +boundary**, and both transports hold an `enforced` row in the conformance +matrix (`mcp-http-identity`, `mcp-stdio-authority`), so a fail-open regression +breaks CI like any other surface. + +- **HTTP** (`/api/v1/mcp`, default-on — ADR-0096). The dispatcher resolves the + same `ExecutionContext` a REST call would; **no `userId` and not `isSystem` ⇒ + 401** (advertising RFC 9728 OAuth metadata in `WWW-Authenticate` when the + OAuth track is live, else a plain 401). An OAuth token additionally narrows + the exposed tool families to its granted scopes (`403` on none). Enforcement: + `packages/runtime/src/http-dispatcher.ts` `handleMcp` + + `buildMcpBridge(context)`. +- **stdio** (opt-in — ADR-0101). The long-lived local transport has no ambient + identity: it mints its principal solely from `OS_MCP_STDIO_API_KEY`, resolved + through the **same** `resolveAuthzContext` chain and re-resolved per call + (revocation takes effect live). It is **fail-closed** — a missing / unknown / + revoked / expired / owner-less key refuses to start the transport — with **no + `system` bypass** (full authority means minting an admin or service key). + Enforcement: `packages/mcp/src/plugin.ts` `start()`. + ## Combination semantics (the fixed order) From ADR-0066 *Precedence / combination semantics* — the contract, not an @@ -393,3 +420,5 @@ The complete, prioritized gap map lives in issue **#2561** (the production | [0086](/adr/0086-authz-metadata-config-boundary-and-cross-package-composition) | Metadata↔config boundary, package provenance, cross-package composition | | 0090 | Permission Model v2: position rename + vocabulary freeze, profile removal, fail-closed OWD default + external dial, audience anchors, principal taxonomy, publish linter, delegated administration, explain engine + access matrix | | 0091 | Grant lifecycle: validity windows + resolution-time filtering (L1, landed), delegation, break-glass, recertification substrate | +| [0096](/adr/0096-execution-surface-identity-admission) | Execution-surface identity admission — no data-engine call without an explicit principal (the MCP HTTP surface admits identity here) | +| [0101](/adr/0101-mcp-stdio-principal-admission) | MCP stdio principal admission — env-supplied API-key identity, fail-closed, no `system` bypass |