diff --git a/docs/dev/rfc-003-mcp-server-surface.md b/docs/dev/rfc-003-mcp-server-surface.md index 32fbce5..d26459d 100644 --- a/docs/dev/rfc-003-mcp-server-surface.md +++ b/docs/dev/rfc-003-mcp-server-surface.md @@ -1,6 +1,6 @@ # RFC: MCP Server Surface for `omnigraph-server` — Full Tool Parity, Stored Queries, Modular Auth -**Status:** Proposed +**Status:** Partially implemented — the server-side surface (Streamable-HTTP transport, built-in tools, stored-query tools, resources, Cedar-filtered list/call) shipped in [omnigraph#157](https://github.com/ModernRelay/omnigraph/pull/157). Deferred: per-query `invoke_query` scope (PR 0b), the OAuth/RFC-9728 layer (MR-956), and the stdio→proxy collapse. **Date:** 2026-06-01 **Tickets:** MR-969 (stored queries + MCP exposure — the surface this completes), MR-956 (federated auth / WorkOS OAuth — the auth substrate this consumes), MR-971 (per-server credential resolver), MR-974 (agent setup surface — the installer that wires this), MR-668 (multi-graph server — shipped, the routing this builds on) **Builds on:** [omnigraph#128](https://github.com/ModernRelay/omnigraph/pull/128) (`ragnorc/stored-queries-mcp`) — the shipped stored-query registry, `GET /queries`, `POST /queries/{name}`, and the coarse `invoke_query` gate. @@ -222,9 +222,9 @@ With ad-hoc `query`/`mutate`/`schema_apply` present as tools, the **only** thing ## Relationship to the `@modernrelay/omnigraph-mcp` stdio package -Verified surface of the package (`omnigraph-ts`, pkg version `0.3.0`, `@modelcontextprotocol/sdk@^1.29.0`, **stdio only**): **13 tools** (`health`, `snapshot`, `read`, `schema_get`, `branches_list`, `commits_list`, `commits_get`, `change`, `ingest`, `branches_create`, `branches_delete`, `branches_merge`, `schema_apply`) and **2 resources** (`omnigraph://schema`, `omnigraph://branches`). It is a thin client over the SDK → HTTP routes and **forwards the caller's bearer verbatim** (no inspection). +Surface of the package (`omnigraph-ts`, `@modelcontextprotocol/sdk@^1.29.0`, **stdio only**). *Figures refreshed 2026-06: the package re-synced to the engine in [omnigraph-ts#11](https://github.com/ModernRelay/omnigraph-ts/pull/11) and is now at `0.6.1` — not the `0.3.0` this RFC was first drafted against.* It exposes **16 tools** (`health`, `snapshot`, `query`, `read`, `schema_get`, `branches_list`, `graphs_list`, `commits_list`, `commits_get`, `mutate`, `change`, `ingest`, `branches_create`, `branches_delete`, `branches_merge`, `schema_apply` — note it already canonicalized `query`/`mutate` with `read`/`change` as deprecated aliases, and added `graphs_list`) and **~9 resources** (`omnigraph://schema`, `omnigraph://branches`, `omnigraph://graphs`, plus a vendored `omnigraph://best-practices/*` cookbook). It is a thin client over the SDK → HTTP routes and **forwards the caller's bearer verbatim** (no inspection). -Once parity lands, **collapse to one implementation**: the in-server MCP is canonical (Cedar-gated, remote-capable, the path that becomes a Claude-web connector via MR-956). The stdio package degrades to a **thin stdio↔HTTP proxy** forwarding JSON-RPC (and the incoming `Authorization`) to `/mcp` — staying the local on-ramp for Claude Code/Desktop while sharing one tool set, one Cedar gate. Transition: keep the current independent stdio package on its `0.3.x`/`0.6.x` line; ship proxy mode in a later TS minor once the server endpoint is GA. (Note: the package is currently several minors behind the server — its vendored `spec/openapi.json` predates the stored-query routes — so it needs the standard re-sync regardless of MCP work.) +Once parity lands, **collapse to one implementation**: the in-server MCP is canonical (Cedar-gated, remote-capable, the path that becomes a Claude-web connector via MR-956). The stdio package degrades to a **thin stdio↔HTTP proxy** forwarding JSON-RPC (and the incoming `Authorization`) to `/mcp` — staying the local on-ramp for Claude Code/Desktop while sharing one tool set, one Cedar gate. Transition: keep the current independent stdio package on its `0.6.x` line; ship proxy mode in a later TS minor once the server endpoint is GA. (The package already re-synced to `0.6.1` in [omnigraph-ts#11](https://github.com/ModernRelay/omnigraph-ts/pull/11); its client-side stored-query-tools attempt, [omnigraph-ts#7](https://github.com/ModernRelay/omnigraph-ts/pull/7), was **closed** in favor of this server-side surface.) ## Testing diff --git a/docs/dev/testing.md b/docs/dev/testing.md index 1ec7038..e7fdde0 100644 --- a/docs/dev/testing.md +++ b/docs/dev/testing.md @@ -8,7 +8,7 @@ This file is the always-on map of the test surface. **Consult it before every ta |---|---|---| | `omnigraph` (engine) | `crates/omnigraph/tests/` | Integration tests (21 files), fixture-driven, share `tests/helpers/mod.rs` | | `omnigraph-cli` | `crates/omnigraph-cli/tests/` | `cli.rs` (unit-ish), `system_local.rs`, `system_remote.rs`, share `tests/support/mod.rs` | -| `omnigraph-server` | `crates/omnigraph-server/tests/` | `server.rs` (HTTP-level), `openapi.rs` (OpenAPI drift / regeneration) | +| `omnigraph-server` | `crates/omnigraph-server/tests/` | `server.rs` (HTTP-level, incl. the `mcp_*` MCP-surface tests that reuse its `app_*`/`json_response` helpers), `openapi.rs` (OpenAPI drift / regeneration) | | `omnigraph-compiler` | mostly in-source `#[cfg(test)] mod tests` | Parser, type-checker, IR lowering, lint | The engine's `tests/` is the principal coverage surface; most graph-shaped behavior is exercised there. diff --git a/docs/user/server.md b/docs/user/server.md index 67b5afe..e33f972 100644 --- a/docs/user/server.md +++ b/docs/user/server.md @@ -51,6 +51,7 @@ Per-graph endpoints — same body shape across modes; URLs differ: | POST | `/branches/merge` | `/graphs/{id}/branches/merge` | bearer + `branch_merge` | merge `source → target` | `server_branch_merge` | | GET | `/commits?branch=` | `/graphs/{id}/commits?branch=` | bearer + `read` | list | `server_commit_list` | | GET | `/commits/{commit_id}` | `/graphs/{id}/commits/{commit_id}` | bearer + `read` | show | `server_commit_show` | +| POST | `/mcp` | `/graphs/{id}/mcp` | bearer + per-tool Cedar | MCP Streamable HTTP (JSON-RPC: `initialize`/`tools/*`/`resources/*`); `GET` → 405; not in `openapi.json` | `mcp::mcp_router` | Server-level management endpoints (v0.6.0+): @@ -75,6 +76,40 @@ Invoke a curated, server-side stored query by **name** — the source comes from - **Requires an explicit policy grant when auth is on.** In default-deny mode (bearer tokens but no `policy.file`), only `read` is permitted, so *every* `/queries/{name}` call returns `404` until an `invoke_query` rule is configured. - A stored mutation cannot target a `snapshot` (`400`); a parameter type error is a structured `400` naming the parameter. +## MCP server surface (`POST /mcp`) + +`POST /mcp` exposes the server as a **Model Context Protocol** endpoint over [Streamable HTTP](https://modelcontextprotocol.io/specification/2025-11-25/basic/transports) (built on `rmcp`). It projects the server's operations as MCP **tools** and **resources** for LLM clients (Claude Code, Cursor, VS Code, the Claude Messages API connector), Cedar-gated through the **same `authorize` path the REST routes use** — there is no separate MCP authorization. Single-graph mode serves it at `/mcp`; multi-graph mode nests it at `/graphs/{id}/mcp` (per-graph isolation). + +**Transport.** Stateless, POST-only, `application/json` responses (no SSE, no `Mcp-Session-Id`); each request is authenticated independently by the same bearer middleware as the REST routes. `GET`/`DELETE /mcp` → `405`; a disallowed `Host`/`Origin` → `403` (DNS-rebinding guard, loopback by default); an unsupported `MCP-Protocol-Version` → `400`. JSON-RPC methods: `initialize`, `notifications/initialized`, `ping`, `tools/list`, `tools/call`, `resources/list`, `resources/read`. Being JSON-RPC rather than REST, it is **not** described in `openapi.json`. + +**Tools.** 14 built-ins, each reusing the exact Cedar action and engine path of its REST route, plus one tool per **`mcp.expose`** stored query (named by its `tool_name`, parameters projected to JSON Schema): + +| Tool(s) | Cedar action | +|---|---| +| `health` | none | +| `snapshot`, `schema_get`, `branches_list`, `commits_list`, `commits_get`, `query` | `read` | +| `mutate`, `ingest` | `change` (+ `branch_create` for a forking ingest) | +| `branches_create` / `branches_delete` / `branches_merge` | `branch_create` / `branch_delete` / `branch_merge` | +| `schema_apply` | `schema_apply` | +| `graphs_list` | `graph_list` (server-scoped; multi-graph only) | +| *stored queries* | `invoke_query` (coarse), then inner `read`/`change` | + +Tools carry `readOnlyHint`/`destructiveHint` annotations from their read/mutate nature; the engine is a closed world, so `openWorldHint` is always `false`. + +**Resources.** `omnigraph://schema` (`read`), `omnigraph://branches` (`read`), and `omnigraph://graphs` (`graph_list`, multi-graph only). + +**Authorization (the gate).** `tools/list` and `resources/list` return **only** what the actor's policy permits — an actor with no grants sees an empty catalog (default-deny). `tools/call` enforces the same gate and **masks a denied or unknown tool identically** (`unknown tool: `), so the catalog can't be probed without the grant. Stored-query tools are gated by the **coarse** `invoke_query` action (all exposed queries on the graph, or none — per-query scope is a future refinement) and then double-gated by the inner `read`/`change`. A bad/absent bearer is a transport-level `401` before any tool runs; an engine or validation error inside a tool is a `CallToolResult` with `isError: true` (so the model can self-correct), distinct from a JSON-RPC protocol error (unknown method/tool). + +> **`tools/list` is best-effort for branch-scoped policies.** Visibility is evaluated against the default branch, so a branch-scoped `change` policy may list `mutate` yet deny a call on a protected branch. `tools/call` is always the authoritative gate. + +**Connecting.** With a static bearer: + +``` +claude mcp add --transport http omnigraph https://og.example/mcp --header "Authorization: Bearer $OG_TOKEN" +``` + +This works with Claude Code, Cursor, VS Code, and the Claude Messages API MCP connector. **claude.ai web and ChatGPT consumer connectors require OAuth** (RFC 9728 + an authorization server), a planned additive layer: the handler already consumes a resolved actor and is agnostic to how the token was verified, so OAuth slots in without changing the tool surface. + ## Adding and removing graphs (multi mode) Runtime add/remove via API is **not** exposed in v0.6.0 — neither @@ -215,3 +250,7 @@ See [deployment.md](deployment.md) for token-source operational details. add `tower_http::limit` if a graph-wide cap is needed. - Pagination — none (commits/branches return everything; export streams). - Runtime graph add/remove — edit `omnigraph.yaml` and restart. +- MCP OAuth — `/mcp` accepts a static bearer only today; OAuth (RFC 9728 + protected-resource metadata + an authorization server) for claude.ai web / + ChatGPT connectors is a planned additive layer (the handler is already + auth-method-agnostic).