Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 32 additions & 1 deletion content/docs/ai/connect-mcp.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,36 @@ OAuth requires TLS — plain-HTTP deployments (except `localhost`) fall back to
run insecurely.
</Callout>

## 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:
Expand Down Expand Up @@ -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` |
Expand Down
29 changes: 29 additions & 0 deletions content/docs/permissions/authorization.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 |