diff --git a/AGENTS.md b/AGENTS.md
index 378de88..e231d45 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -95,6 +95,7 @@ Full diagram and concurrency model: [docs/dev/architecture.md](docs/dev/architec
| Cluster operator guide (deploy/manage clusters, approvals, recovery, serving) | [docs/user/clusters/index.md](docs/user/clusters/index.md) |
| Cedar policy actions, scopes, CLI | [docs/user/operations/policy.md](docs/user/operations/policy.md) |
| HTTP server endpoints, auth, error model, body limits | [docs/user/operations/server.md](docs/user/operations/server.md) |
+| MCP surface — tools/resources for MCP agents (`POST /graphs/{id}/mcp`) | [docs/user/operations/mcp.md](docs/user/operations/mcp.md) |
| CLI quick-start | [docs/user/cli/index.md](docs/user/cli/index.md) |
| CLI command surface and config schema (`~/.omnigraph/config.yaml`) | [docs/user/cli/reference.md](docs/user/cli/reference.md) |
| Audit / actor tracking | [docs/user/operations/audit.md](docs/user/operations/audit.md) |
@@ -265,7 +266,7 @@ omnigraph policy explain --cluster ./company-brain --graph knowledge --actor act
| Three-way row-level merge | — | `OrderedTableCursor` + `StagedTableWriter`, structured `MergeConflictKind` |
| Change feeds | — | `diff_between` / `diff_commits` with manifest fast path + ID streaming |
| Cedar policy | — | Per-graph actions plus server-scoped actions (see [docs/user/operations/policy.md](docs/user/operations/policy.md) for the current list), branch / target_branch / protected scopes, validate/test/explain CLI. **Engine-wide enforcement** (MR-722): every `_as` writer (`apply_schema_as`, `mutate_as`, `load_as` — the deprecated `ingest_as` shims route through it — `branch_create_as` / `branch_create_from_as`, `branch_delete_as`, `branch_merge_as`) calls `Omnigraph::enforce(action, scope, actor)` — HTTP, CLI, embedded SDK all hit the same gate. |
-| HTTP server | — | Axum, OpenAPI via utoipa, bearer auth (SHA-256, AWS Secrets Manager option), `authorize_request` at the HTTP boundary (resolves bearer→actor, applies admission control), NDJSON streaming export, **cluster-only boot (RFC-011): always `--cluster
`, serving N graphs (N ≥ 1) under multi-graph routes + read-only `GET /graphs` enumeration + per-graph + server-level Cedar policies. Add/remove graphs via `cluster apply` and restart.** |
+| HTTP server | — | Axum, OpenAPI via utoipa, bearer auth (SHA-256, AWS Secrets Manager option), `authorize_request` at the HTTP boundary (resolves bearer→actor, applies admission control), NDJSON streaming export, **cluster-only boot (RFC-011): always `--cluster `, serving N graphs (N ≥ 1) under multi-graph routes + read-only `GET /graphs` enumeration + per-graph + server-level Cedar policies. Add/remove graphs via `cluster apply` and restart.** Each served graph also exposes a per-graph **MCP surface** (`POST /graphs/{id}/mcp`, RFC-003): stateless Streamable-HTTP transport projecting built-ins + stored queries as tools and schema/branches as resources, same Cedar gate, `tools/list` a relaxation of the per-call gate. |
| CLI with config | — | two-surface config (team `cluster.yaml` dir + per-operator `~/.omnigraph/config.yaml`), scope addressing (`--store`/`--server`/`--cluster`/`--profile`/defaults, RFC-011), aliases, multi-format output (json/jsonl/csv/kv/table) |
| Audit / actor tracking | — | `_as` write APIs + actor map in commit graph |
| Local S3 testing | — | run RustFS/MinIO + the `AWS_*` env; see [docs/user/deployment.md](docs/user/deployment.md) → *Testing against S3 locally* |
diff --git a/docs/dev/rfc-005-server-cluster-boot.md b/docs/dev/rfc-005-server-cluster-boot.md
index 85df875..d4fddfc 100644
--- a/docs/dev/rfc-005-server-cluster-boot.md
+++ b/docs/dev/rfc-005-server-cluster-boot.md
@@ -88,9 +88,24 @@ Boot is fail-fast, matching the server's existing stance (bad policy YAML refuse
| stored query fails type-check against the live schema | boot error (existing `validate_and_attach` behavior) |
| state lock held | **not** an error — boot takes no lock; it reads a point-in-time snapshot of an immutable-once-written state file (the CAS discipline means a concurrent apply produces a *new* file atomically; the server reads whichever was current at open) |
-### D5. The `mcp.expose` bridge in cluster mode
+### D5. MCP presentation (`@mcp(expose, tool_name)`) in cluster mode
-The cluster query registry has no `expose` flag by design (axiom 14: exposure is a policy decision — Phase 6). Until Phase 6 ships, cluster-mode servers list **all** stored queries in `GET /queries`. This is the documented bridge: *cluster mode = everything exposed; omnigraph.yaml mode = `mcp.expose` honored as today*. Its named sunset is Phase 6's policy-filtered catalog (Compatibility Stance #9). Invocation remains gated by the existing coarse `invoke_query` Cedar action in both modes.
+**Superseded (v0.8.0).** The old "bridge" — cluster mode force-lists every stored
+query because the cluster registry had no `expose` flag — is gone. Per-query MCP
+presentation is now carried in the `.gq` **source** via the `@mcp(expose: …,
+tool_name: …)` annotation (re-parsed at boot from the content-addressed query
+blob), so cluster mode honors it the same as any other deployment; the boot path
+no longer hardcodes `expose: true`. Default with no `@mcp`: exposed, tool name =
+query name.
+
+Crucially this splits two axes the original bridge conflated: **`expose` is
+presentation** (does the query appear on the agent tool surface — `tools/list` /
+`GET /queries`), carried in source; **authorization** (who may invoke a query)
+stays the coarse `invoke_query` Cedar action, with a per-query refinement the
+durable future direction. An `expose: false` query is still HTTP/service-callable
+by name for any caller holding `invoke_query`. See
+[../user/queries/index.md](../user/queries/index.md#annotations) and
+[../user/operations/mcp.md](../user/operations/mcp.md).
### D6. Migration path (exit criterion 7)
diff --git a/docs/dev/testing.md b/docs/dev/testing.md
index 8d6a305..8cb683e 100644
--- a/docs/dev/testing.md
+++ b/docs/dev/testing.md
@@ -9,7 +9,8 @@ This file is the always-on map of the test surface. **Consult it before every ta
| `omnigraph` (engine) | `crates/omnigraph/tests/` | Integration tests (28 files), fixture-driven, share `tests/helpers/mod.rs` |
| `omnigraph-cli` | `crates/omnigraph-cli/tests/` | Per-area suites (post-modularization): `cli_cluster.rs` (cluster command surface + operator-actor cascade), `cli_cluster_e2e.rs` (spawned-binary lifecycle compositions — lost-state re-import recovery, out-of-band drift, graph-root destruction, multi-graph mixed-disposition convergence), `cli_data.rs` (load/read/change/branch/commit/export/snapshot/policy/embed/maintenance + operator format cascade), `cli_schema_config.rs` (init/config, schema plan/apply), `cli_queries.rs`, `parity_matrix.rs` (RFC-009 Phase 1: the embedded-vs-remote referee — every forked verb run against both arms with matched Cedar policy and the same actor, scrubbed-JSON + exit-code equality; divergences are pinned in its `KNOWN_DIVERGENCES` ledger, never silently repaired), `system_local.rs` (full-cycle cluster lifecycle with a spawned `--cluster` server, applied-policy enforcement over HTTP, keyed-credential auth, operator aliases), `system_remote.rs`; share `tests/support/mod.rs` (hermetic `OMNIGRAPH_HOME` by default) |
| `omnigraph-cluster` | mostly in-source `#[cfg(test)] mod tests`; `tests/failpoints.rs` (feature-gated); `tests/s3_cluster.rs` (bucket-gated full lifecycle on object storage) | Cluster config parser, local JSON state diff, state CAS/lock handling/recovery, read-only validate/plan/status plus explicit refresh/import graph observations, config-only apply (content-addressed payload publish, disposition gating, composite-digest convergence, idempotent re-apply), catalog payload verification (status read-only, refresh drift + self-heal), failpoint crash-mid-apply / CAS-race coverage, Stage 4A graph creation (create executor, recovery sidecars + sweep rows, create crash windows), Stage 4B schema apply (migration previews in plan, schema executor, schema-apply sweep classification, schema crash windows), Stage 4C gated deletes (digest-bound approvals, delete executor + tombstones, delete sweep rows, delete crash windows), and 5A policy binding metadata (applies_to in the applied revision, binding-change diffing + convergence, pre-5A backfill), and the 5B serving-snapshot read API (converged read, refusal rows) |
-| `omnigraph-server` | `crates/omnigraph-server/tests/` | Per-area suites (post-modularization): `auth_policy.rs`, `data_routes.rs`, `schema_routes.rs`, `stored_queries.rs`, `multi_graph.rs` (cluster-mode boot — converged serving, policy binding wiring, boot refusals — + the concurrent branch-ops matrix), `boot_settings.rs` (mode inference, PolicySource), `s3.rs` (bucket-gated: single-graph serving + config-free `--cluster s3://` boot), `openapi.rs` (OpenAPI drift / regeneration); share `tests/support/mod.rs` |
+| `omnigraph-server` | `crates/omnigraph-server/tests/` | Per-area suites (post-modularization): `auth_policy.rs`, `data_routes.rs`, `schema_routes.rs`, `stored_queries.rs`, `mcp.rs` (the `/graphs/{id}/mcp` surface — protocol conformance, Cedar-filtered listing + the argument-scoped relaxation, stored-tool projection modes + `expose` reachability, structured output), `multi_graph.rs` (cluster-mode boot — converged serving, policy binding wiring, boot refusals — + the concurrent branch-ops matrix), `boot_settings.rs` (mode inference, PolicySource), `s3.rs` (bucket-gated: single-graph serving + config-free `--cluster s3://` boot), `openapi.rs` (OpenAPI drift / regeneration); share `tests/support/mod.rs` |
+| `omnigraph-mcp` | `crates/omnigraph-mcp/tests/standalone.rs` | Crate-level MCP transport conformance with a trivial backend (no engine dep): `initialize`/`tools/list`, `405` for `GET`/`DELETE`, fail-closed Origin, the full loopback `Host` set (incl. `::1`), and the `MCP-Protocol-Version` contract (non-init → 400, init exempt). Also the **rmcp surface guard** — first smoke check on an rmcp bump. |
| `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/releases/v0.8.0.md b/docs/releases/v0.8.0.md
new file mode 100644
index 0000000..283ba08
--- /dev/null
+++ b/docs/releases/v0.8.0.md
@@ -0,0 +1,89 @@
+# Omnigraph v0.8.0
+
+v0.8.0 makes every served graph an **MCP (Model Context Protocol) server**. An
+MCP-capable agent — Claude Code/Desktop, Cursor, the OpenAI Responses `mcp` tool,
+and others — can connect to a graph and operate it directly: run reads and
+mutations, load data, manage branches, browse commits, read the schema, and
+invoke the graph's curated stored queries. The surface adds no new capability and
+no new business logic; every tool delegates to the same engine/handler path the
+REST routes use and is gated by the same Cedar policy.
+
+## Highlights
+
+### MCP surface (`POST /graphs/{id}/mcp`)
+
+- **One MCP endpoint per served graph**, mounted automatically by the cluster
+ server — no separate flag. It is a stateless Streamable-HTTP transport: a
+ single `application/json` JSON-RPC response per call, no SSE, no session id.
+- **Built-in tools** cover the operational surface: `graph_query`,
+ `graph_mutate`, `graph_load`, `graph_snapshot`, `schema_get`, `branch_list`,
+ `branch_create` / `branch_delete` / `branch_merge`, `commit_list` /
+ `commit_get`, `schema_apply` (disabled with a `409` under cluster-backed
+ serving — evolve via `cluster apply` and restart), and a `graph_health`
+ liveness probe.
+- **Stored queries as tools.** A graph's stored-query registry is projected as
+ tools, in one of two modes chosen automatically from the exposed-query count:
+ `per_query` (each exposed query is its own typed tool) below a threshold, or a
+ `stored_query_list` + `stored_query_run` discovery/execute pair at or above it,
+ so a client's tool count stays bounded.
+- **Resources.** The graph schema (`omnigraph://schema`) and branch list
+ (`omnigraph://branches`) are exposed as MCP resources.
+- **Structured output.** Tool results carry `structuredContent` (the same typed
+ result envelopes as the REST routes) plus a text mirror.
+
+### Authorization parity with REST
+
+- Every tool and resource resolves the actor from the bearer token and passes the
+ same Cedar gate as the equivalent REST route; the call-time gate is
+ authoritative.
+- **`tools/list` is a relaxation of the per-call gate**: a tool the actor could
+ invoke on *some* branch is listed, so listing never hides a tool you can call,
+ while an actor with no grant for an action still does not see its tools. Under
+ the common "protect `main`, write feature branches" policy, `graph_mutate` is
+ listed for an actor who can write unprotected branches.
+- Stored queries sit behind the coarse `invoke_query` gate (a stored mutation is
+ additionally `change`-gated); for a caller without `invoke_query`, a stored
+ tool masks as an unknown tool so the catalog can't be probed. An
+ `expose: false` query is unreachable on the MCP surface entirely (not listed,
+ not runnable by name) while remaining HTTP/service callable.
+
+### Authoring stored queries as MCP tools
+
+`.gq` gains the controls to shape how a stored query appears as an MCP tool, all
+carried in the query source:
+
+- **`@instruction("…")` reaches agents.** The query's `@instruction` annotation
+ is folded into the MCP tool description (after `@description`), so the
+ how/when-to-use guidance shows up in `tools/list` — previously it surfaced only
+ in the REST catalog.
+- **Per-parameter docs.** A leading `@description("…")` on a parameter
+ (`@description("the user's slug") $slug: String`) is surfaced into the
+ parameter's JSON-Schema `description` in both the MCP tool input schema and the
+ `GET /queries` catalog.
+- **`@mcp(tool_name: "…", expose: )`.** A dedicated MCP-presentation
+ annotation: `tool_name` overrides the tool id (unique-checked at boot, can't
+ shadow a built-in); `expose: false` hides the query from the agent tool surface
+ (`tools/list` / `stored_query_list` / `stored_query_run`) while keeping it
+ HTTP/service-callable by name. `expose` is presentation only — Cedar
+ `invoke_query` remains the authority for who may call a query.
+
+### Transport hardening
+
+- **Fail-closed Host / Origin posture**, derived from the bind address at
+ startup. A loopback bind accepts the full loopback `Host` set
+ (`127.0.0.1`, `::1`, `localhost`) regardless of which IP stack it bound; a
+ non-loopback bind rejects an unexpected browser `Origin` and restricts `Host`
+ to the configured public hosts.
+- The `MCP-Protocol-Version` header is validated on follow-up requests (an
+ unsupported version is a `400`); `initialize` negotiates the version in its
+ body and is exempt by design.
+
+## Upgrade notes
+
+- **No breaking changes.** The REST surface, CLI, cluster config, and on-disk
+ format are unchanged. The MCP endpoint is additive.
+- **Pointing an agent at a graph:** configure your MCP client with the URL
+ `https:///graphs//mcp` and the same bearer token you use for REST.
+ See [docs/user/operations/mcp.md](../user/operations/mcp.md) for the connect
+ recipe, the tool catalog, projection modes, and the Host/Origin and
+ protocol-version contracts. Design and rationale: RFC-003.
diff --git a/docs/user/index.md b/docs/user/index.md
index cabd98a..263a94d 100644
--- a/docs/user/index.md
+++ b/docs/user/index.md
@@ -45,6 +45,7 @@ start with install, then follow the section that matches your task.
|---|---|
| Deploy the binary or container | [deployment.md](deployment.md) |
| Use HTTP endpoints | [operations/server.md](operations/server.md) |
+| Connect an MCP agent (Claude, Cursor, …) | [operations/mcp.md](operations/mcp.md) |
| Compact, repair, and clean old versions | [operations/maintenance.md](operations/maintenance.md) |
| Configure Cedar authorization | [operations/policy.md](operations/policy.md) |
| Track actors and audit behavior | [operations/audit.md](operations/audit.md) |
diff --git a/docs/user/operations/mcp.md b/docs/user/operations/mcp.md
new file mode 100644
index 0000000..be02b86
--- /dev/null
+++ b/docs/user/operations/mcp.md
@@ -0,0 +1,154 @@
+# MCP Surface (`POST /graphs/{id}/mcp`)
+
+Every graph a cluster server serves is also exposed as a [Model Context
+Protocol](https://modelcontextprotocol.io) server, so an MCP-capable agent
+(Claude Code/Desktop, Cursor, the OpenAI Responses `mcp` tool, …) can operate the
+graph directly — no bespoke client, no `.gq` source on the wire. The MCP surface
+adds **no new capability or business logic**: every tool delegates to the same
+engine/handler path the REST routes use and is gated by the same Cedar policy.
+
+Available since **v0.8.0**. It is served automatically by `omnigraph-server
+--cluster …` — there is no separate flag to enable it.
+
+## Transport
+
+One endpoint per served graph:
+
+```
+POST /graphs/{id}/mcp
+```
+
+It is a **stateless Streamable-HTTP** MCP transport: each call returns a single
+`application/json` JSON-RPC response — no SSE, no session id. `GET`/`DELETE`
+return `405` (`Allow: POST`).
+
+```bash
+curl -sS https://graph.example.com/graphs/sales/mcp \
+ -H "Authorization: Bearer $TOKEN" \
+ -H "Content-Type: application/json" \
+ -H "Accept: application/json, text/event-stream" \
+ -d '{"jsonrpc":"2.0","id":1,"method":"initialize",
+ "params":{"protocolVersion":"2025-11-25","capabilities":{},
+ "clientInfo":{"name":"my-agent","version":"0"}}}'
+```
+
+A typical MCP client is configured with just the URL and the bearer token; it
+then drives `initialize` → `tools/list` / `resources/list` → `tools/call` /
+`resources/read` itself.
+
+Every tool operates on the single graph in the URL path, so the graph id never
+appears in tool arguments or output.
+
+## Tools
+
+### Built-in tools
+
+| Tool | Action | Notes |
+|---|---|---|
+| `graph_health` | — | liveness/identity probe; always visible |
+| `graph_query` | `read` | run an ad-hoc read-only GQ query (mutations rejected) |
+| `graph_snapshot` | `read` | manifest version + per-table metadata of a branch |
+| `schema_get` | `read` | the graph's `.pg` source |
+| `branch_list` | `read` | branch names |
+| `commit_list` / `commit_get` | `read` | commit history |
+| `graph_mutate` | `change` | ad-hoc GQ insert/update/delete against a branch |
+| `graph_load` | `change` (+ `branch_create` with `from`) | bulk NDJSON load |
+| `branch_create` / `branch_delete` / `branch_merge` | `branch_create` / `branch_delete` / `branch_merge` | branch ops |
+| `schema_apply` | `schema_apply` | **disabled (409) under cluster-backed serving** — evolve via `cluster apply` + restart |
+
+Tool names are domain-qualified `snake_case`. Read tools are annotated
+`readOnly`; writers are annotated `destructive` so clients can prompt for
+confirmation (annotations are advisory hints — Cedar is the enforcement
+boundary).
+
+### Stored-query tools
+
+The graph's stored-query registry (declared in `cluster.yaml`, see
+[stored queries](server.md#stored-query-catalog-get-queries)) is projected as
+tools too, in one of two modes chosen automatically from the count of exposed
+queries:
+
+- **`per_query`** (fewer than 24 exposed queries) — each exposed query is its own
+ tool, named by its `@mcp(tool_name: …)` (default: the query name), with a typed
+ input schema. Query parameters are nested under a `params` object; `branch`
+ (and, for reads, `snapshot`) sit alongside it.
+- **`meta`** (24 or more) — the per-query tools collapse to a discovery + execute
+ pair, `stored_query_list` (filter/inspect the catalog) and
+ `stored_query_run(name, params, branch?, snapshot?)`, so a client's tool count
+ stays bounded.
+
+A stored-query tool's metadata comes from the `.gq` source (see
+[query annotations](../queries/index.md#annotations)):
+
+- **description** = `@description`, with `@instruction` folded in after a blank
+ line (so the agent sees both in `tools/list`).
+- **tool name** = `@mcp(tool_name: …)`, else the query name.
+- **parameter docs** = each parameter's `@description`, surfaced into the input
+ schema's per-property `description`.
+
+Only **exposed** queries are reachable on the MCP surface in either mode. Set
+`@mcp(expose: false)` to hide a query from `tools/list`, from
+`stored_query_list`, and from `stored_query_run` (by name). This is presentation
+only — the query stays HTTP/service-callable via `POST /queries/{name}` for any
+caller with the `invoke_query` grant.
+
+## Resources
+
+| URI | Gate | Contents |
+|---|---|---|
+| `omnigraph://schema` | `read` | the graph's `.pg` schema source |
+| `omnigraph://branches` | `read` | branch names as JSON |
+
+## Structured output
+
+Tool results carry **structured output**: `structuredContent` (the typed result
+DTO — the same shape as the REST `ReadOutput` / `ChangeOutput` envelopes) plus a
+text mirror for clients that don't parse it.
+
+## Authorization
+
+Authorization is identical to the REST routes — the bearer token resolves to a
+server-side actor, and every tool/resource hits the same Cedar gate:
+
+- **Calls are authoritative.** A built-in tool runs only if the actor's Cedar
+ grant permits the action on the *actual* branch argument; a denial comes back
+ as a tool error (`isError`), not a silent success.
+- **Listing is a relaxation.** `tools/list` shows a tool if the actor could
+ invoke it on *some* branch, so a tool you can call is never hidden. Under the
+ canonical "protect `main`, write feature branches" policy, `graph_mutate` is
+ listed for an actor who can change unprotected branches even though a write to
+ `main` would be denied. An actor with no write grant at all does not see the
+ write tools.
+- **Stored queries** sit behind one coarse `invoke_query` gate (a stored
+ *mutation* is additionally `change`-gated). For a caller lacking `invoke_query`,
+ a stored tool masks as an **unknown tool**, so the catalog can't be probed.
+
+## Host & Origin policy
+
+The transport derives a fail-closed DNS-rebinding / browser posture from the bind
+address at startup:
+
+- **Loopback bind** (`127.0.0.1` or `::1`) — the `Host` allow-list is the full
+ loopback set `127.0.0.1`, `::1`, `localhost` (so either IP stack works
+ regardless of which one the server bound), and `Origin` is unchecked (local-dev
+ convenience).
+- **Non-loopback bind** — the `Host` allow-list is the configured public host(s)
+ (or unrestricted if none are configured — rely on the bearer token there), and
+ any *present* browser `Origin` is rejected (`403`) unless it is in the
+ configured browser-origins list. A non-browser MCP client sends no `Origin` and
+ passes.
+
+A disallowed `Host` is `403`.
+
+## Protocol version
+
+The `MCP-Protocol-Version` header is validated on follow-up requests (an
+unsupported version → `400`). The `initialize` request is exempt by design — it
+negotiates the version in its JSON-RPC body (`protocolVersion`), so the header is
+not checked there.
+
+## Not supported
+
+MCP prompts, elicitation, sampling, tasks, and `tools/list_changed` /
+`resources/list_changed` subscriptions are not implemented — the surface is
+`initialize` + `tools` + `resources` over the stateless POST transport.
diff --git a/docs/user/operations/server.md b/docs/user/operations/server.md
index bd14e1e..1430c7c 100644
--- a/docs/user/operations/server.md
+++ b/docs/user/operations/server.md
@@ -46,6 +46,7 @@ graph id from the cluster's applied revision:
| POST | `/graphs/{id}/change` | bearer + `change` | **deprecated** alias of `/mutate` (carries `Deprecation: true` + `Link: ; rel="successor-version"`) |
| GET | `/graphs/{id}/queries` | bearer + `read` | list the `mcp.expose` stored queries as a typed tool catalog |
| POST | `/graphs/{id}/queries/{name}` | bearer + `invoke_query` (+ `change` for a stored mutation) | invoke a named query from the `queries:` registry; deny == 404 |
+| POST | `/graphs/{id}/mcp` | bearer + same per-tool Cedar gate | MCP (Model Context Protocol) surface — built-ins + stored queries as tools, schema/branches as resources (see [mcp.md](mcp.md)) |
| GET | `/graphs/{id}/schema` | bearer + `read` | get current `.pg` source |
| POST | `/graphs/{id}/schema/apply` | bearer + `schema_apply` (target=`main`) | disabled for cluster-backed serving; returns 409 and points operators at `omnigraph cluster apply` + restart |
| POST | `/graphs/{id}/load` | bearer + `branch_create` (only when `from` is set and the branch is created) + `change` | bulk load (canonical); branch creation is opt-in via `from` — without it a missing `branch` is a 404, never an implicit fork (32 MB body limit) |
@@ -65,7 +66,7 @@ Server-level management endpoints:
### Stored-query catalog (`GET /queries`)
-List the graph's **`mcp.expose`** stored queries as a typed tool catalog — enough for a client (e.g. an MCP server) to register each as a tool without fetching `.gq` source. Each entry: `{ name, tool_name, description, instruction, mutation, params }`, where each param is `{ name, kind, item_kind?, vector_dim?, nullable }`. `kind` is one of `string | bool | int | bigint | float | date | datetime | blob | vector | list` (decomposed so a consumer maps it with a closed `switch`, never re-parsing GQ type spelling). `bigint` (I64/U64), `date`, `datetime`, and `blob` are carried as JSON **strings** — a 64-bit integer loses precision as a JSON number, dates are ISO strings, and a blob is a URI string.
+List the graph's **`mcp.expose`** stored queries as a typed tool catalog — enough for a client to register each as a tool without fetching `.gq` source. (The server also projects these queries as live MCP tools at `POST /graphs/{id}/mcp` — see [mcp.md](mcp.md); this catalog endpoint is the REST view of the same registry.) Each entry: `{ name, tool_name, description, instruction, mutation, params }`, where each param is `{ name, kind, item_kind?, vector_dim?, nullable }`. `kind` is one of `string | bool | int | bigint | float | date | datetime | blob | vector | list` (decomposed so a consumer maps it with a closed `switch`, never re-parsing GQ type spelling). `bigint` (I64/U64), `date`, `datetime`, and `blob` are carried as JSON **strings** — a 64-bit integer loses precision as a JSON number, dates are ISO strings, and a blob is a URI string.
- **Read-gated** (works in default-deny mode). The catalog is **graph-wide** (branch-independent; `read` is authorized against `main`).
- **`mcp.expose` defaults to `true`** — declaring a query in `queries:` lists it; set `mcp: { expose: false }` to keep it HTTP/service-callable but hidden from the catalog.
@@ -80,6 +81,22 @@ 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 surface (`POST /graphs/{id}/mcp`)
+
+Each served graph is also an MCP (Model Context Protocol) server at
+`POST /graphs/{id}/mcp` — a stateless Streamable-HTTP transport that projects the
+built-in operations and the graph's stored-query registry as MCP **tools**, and
+the schema / branch list as MCP **resources**. It adds no new capability: every
+tool delegates to the same engine/handler path the REST routes use and is gated
+by the same Cedar policy (resolved from the same bearer token). `tools/list` is a
+*relaxation* of the per-call gate — a tool callable on some branch is never
+hidden, while the per-call gate stays authoritative. Served automatically by the
+cluster server; no separate flag.
+
+Full client guide — connecting, the tool catalog, projection modes, structured
+output, Host/Origin policy, and the protocol-version contract — is in
+[mcp.md](mcp.md).
+
## Adding and removing graphs
Runtime add/remove via API is **not** exposed — neither `POST /graphs`
diff --git a/docs/user/queries/index.md b/docs/user/queries/index.md
index c8a70c5..2a6d65e 100644
--- a/docs/user/queries/index.md
+++ b/docs/user/queries/index.md
@@ -3,8 +3,8 @@
## Query declarations
```
-query ($p1: T1, $p2: T2?, …)
- @description("…") @instruction("…") {
+query (@description("…") $p1: T1, $p2: T2?, …)
+ @description("…") @instruction("…") @mcp(tool_name: "…", expose: true) {
…
}
```
@@ -19,6 +19,31 @@ Multi-modal search functions (`nearest`, `bm25`, `rrf`, …) used inside `match`
Param types reuse all schema scalars; trailing `?` makes a param optional. The compiler reserves `$__nanograph_now` for `now()`.
+### Annotations
+
+Annotations after the param list are optional and order-independent:
+
+- `@description("…")` — human-readable summary of the query (shown in the
+ stored-query catalog and as the MCP tool description).
+- `@instruction("…")` — agent-facing *how/when to use* guidance. It is folded
+ into the [MCP](../operations/mcp.md) tool description (appended after
+ `@description`), so an agent reading `tools/list` sees it.
+- `@mcp(...)` — **MCP-presentation** controls for when the query is served as an
+ agent tool (see [mcp.md](../operations/mcp.md)). Both keys are optional:
+ - `tool_name: ""` — the tool id to expose the query under (default: the
+ query name). Must be unique across exposed queries and must not shadow a
+ built-in tool, or the server refuses to boot.
+ - `expose: ` — whether the query appears on the agent tool surface
+ (default `true`). `expose: false` keeps the query HTTP/service-callable by
+ name but hides it from `tools/list` and the catalog. This is **presentation
+ only** — not an authorization control (Cedar `invoke_query` governs who may
+ call it).
+
+A **per-parameter** `@description("…")` (written before the variable, e.g.
+`@description("the user's slug") $slug: String`) documents that argument; it is
+surfaced into the parameter's JSON-Schema `description` in the catalog and the
+MCP tool input schema.
+
## MATCH clauses
- **Binding**: `$x: NodeType { prop: , … }`
diff --git a/skills/omnigraph/SKILL.md b/skills/omnigraph/SKILL.md
index 7bf044a..be87369 100644
--- a/skills/omnigraph/SKILL.md
+++ b/skills/omnigraph/SKILL.md
@@ -1,11 +1,11 @@
---
name: omnigraph
-description: Store, retrieve, and query knowledge, memory, and relationships in an Omnigraph graph, and operate a local or remote Omnigraph deployment. Use when the user wants to capture or recall facts, notes, or entities, build or query a knowledge graph or agent memory, or run Omnigraph — and whenever you see Omnigraph CLI commands (omnigraph init/query/mutate/load/schema/lint/embed/branch/commit/login/profile/cluster), .pg schema or .gq query files, s3:// graph URIs, bearer-authed graph endpoints, 504 errors, or a cluster.yaml / omnigraph.yaml / ~/.omnigraph/config.yaml. Covers cluster-mode deployments (cluster.yaml plan/apply, omnigraph-server --cluster), the two config surfaces (cluster.yaml + ~/.omnigraph/config.yaml), schema evolution, query linting, data writes (mutate; load needs --mode/--from), branches, embeddings, Cedar policy, and remote ops. Especially important before schema apply (plan first), any load (--mode required), any .gq/.pg edit (lint after), or any remote write (verify via commit list).
+description: Store, retrieve, and query knowledge, memory, and relationships in an Omnigraph graph, and operate a local or remote Omnigraph deployment. Use when the user wants to capture or recall facts, notes, or entities, build or query a knowledge graph or agent memory, or run Omnigraph — and whenever you see Omnigraph CLI commands (omnigraph init/query/mutate/load/schema/lint/embed/branch/commit/login/profile/cluster), .pg schema or .gq query files, s3:// graph URIs, bearer-authed graph endpoints, an MCP endpoint or MCP client config for a graph (POST /graphs/{id}/mcp), 504 errors, or a cluster.yaml / omnigraph.yaml / ~/.omnigraph/config.yaml. Covers cluster-mode deployments (cluster.yaml plan/apply, omnigraph-server --cluster), the MCP surface (graphs as MCP servers), the two config surfaces (cluster.yaml + ~/.omnigraph/config.yaml), schema evolution, query linting, data writes (mutate; load needs --mode/--from), branches, embeddings, Cedar policy, and remote ops. Especially important before schema apply (plan first), any load (--mode required), any .gq/.pg edit (lint after), or any remote write (verify via commit list).
license: MIT (see LICENSE at repo root)
-compatibility: Requires omnigraph CLI >= 0.7.0 — the unified `load`, the two config surfaces (cluster.yaml + ~/.omnigraph/config.yaml), and cluster apply/serve all require 0.7.0.
+compatibility: Requires omnigraph CLI >= 0.7.0 — the unified `load`, the two config surfaces (cluster.yaml + ~/.omnigraph/config.yaml), and cluster apply/serve all require 0.7.0. The MCP surface (POST /graphs/{id}/mcp) requires omnigraph-server >= 0.8.0.
metadata:
author: ModernRelay
- version: "0.7.0"
+ version: "0.8.0"
repository: https://github.com/ModernRelay/omnigraph
---
@@ -409,6 +409,6 @@ For anything beyond the basics, load the relevant reference file. Each is self-c
| [`references/search.md`](references/search.md) | Embeddings, `@embed`, vector/text ranking, scope-then-rank pattern |
| [`references/aliases.md`](references/aliases.md) | Defining aliases for agents, structured output, JSON args |
| [`references/stored-queries.md`](references/stored-queries.md) | Server-side stored-query registry: declared in `cluster.yaml`, `omnigraph queries validate/list`, `GET /graphs/{id}/queries` + `POST /graphs/{id}/queries/{name}`, `invoke_query` Cedar gating |
-| [`references/server-policy.md`](references/server-policy.md) | Starting the HTTP server, routes, bearer auth, Cedar policy gating, multi-graph mode |
+| [`references/server-policy.md`](references/server-policy.md) | Starting the HTTP server, routes, bearer auth, Cedar policy gating, multi-graph mode, the MCP surface (`POST /graphs/{id}/mcp` — connecting agents, tool catalog, list-vs-call gating) |
| [`references/commands.md`](references/commands.md) | `snapshot`, `export`, `commit list/show`, addressing & resolution |
| [`references/migrations.md`](references/migrations.md) | Migrating a pre-0.7.0 setup, or you hit an old config/command/flag/route/error and need its current form |
diff --git a/skills/omnigraph/references/server-policy.md b/skills/omnigraph/references/server-policy.md
index 225c708..1ed9776 100644
--- a/skills/omnigraph/references/server-policy.md
+++ b/skills/omnigraph/references/server-policy.md
@@ -7,6 +7,7 @@
- Setup operations bypass the server
- Cedar policy
- Multi-graph mode
+- MCP surface
- Server + policy together
- Cluster-booted servers
@@ -37,6 +38,7 @@ All per-graph routes are nested under `/graphs/{id}/...` (`{id}` = a graph id fr
| `POST /graphs/{id}/load` | bulk JSONL load, 32 MB; branch creation opt-in via `from` (`/ingest` = deprecated alias) |
| `POST /graphs/{id}/export` | NDJSON stream of a branch |
| `GET /graphs/{id}/queries` · `POST /graphs/{id}/queries/{name}` | stored-query catalog (`read`) + invocation (`invoke_query`, +`change` for a stored mutation; deny == 404) |
+| `POST /graphs/{id}/mcp` | MCP surface — built-ins + stored queries as tools, schema/branches as resources (same per-tool Cedar gate; see *MCP surface* below) |
| `GET /graphs/{id}/schema` · `POST /graphs/{id}/schema/apply` | read `.pg` · migrate (`schema_apply`) |
| `GET/POST /graphs/{id}/branches` · `DELETE …/branches/{b}` · `POST …/branches/merge` | branch ops |
| `GET /graphs/{id}/commits?branch=` · `…/commits/{commit_id}` | history |
@@ -210,6 +212,21 @@ Policy attaches at two levels via `cluster.yaml` `applies_to`:
There is no runtime add/remove of graphs — edit `cluster.yaml`, `cluster apply`, restart.
+## MCP surface
+
+Since **v0.8.0**, every served graph is also an MCP (Model Context Protocol) server at `POST /graphs/{id}/mcp` — mounted automatically by the `--cluster` server, no extra flag. An MCP agent (Claude, Cursor, OpenAI Responses `mcp` tool) connects with just the URL and the graph's bearer token, and operates the graph through tools:
+
+- **Built-in tools** — `graph_query`, `graph_mutate`, `graph_load`, `graph_snapshot`, `schema_get`, `branch_list`, `branch_create`/`delete`/`merge`, `commit_list`/`get`, `schema_apply` (returns 409 under cluster serving — evolve via `cluster apply`), and `graph_health`.
+- **Stored-query tools** — the graph's registry, projected per-query below a threshold (default 24 exposed) or as a `stored_query_list` + `stored_query_run` pair above it. Honors `expose`/`tool_name` (see [`stored-queries.md`](stored-queries.md#mcp-exposure)).
+- **Resources** — `omnigraph://schema` and `omnigraph://branches`.
+
+It adds **no new capability**: every tool delegates to the same engine/handler path as the REST routes and passes the **same Cedar gate** (resolved from the same bearer token). Two MCP-specific behaviors to know:
+
+- **`tools/list` is a relaxation of the per-call gate.** A tool is listed if the actor could invoke it on *some* branch, so a callable tool is never hidden — under "protect `main`, write unprotected branches", `graph_mutate` is listed for a branch-writer even though writing `main` is denied. The per-call gate stays authoritative (a denied call returns a tool error). An actor with no write grant at all sees no write tools.
+- **Stored-query denials mask as unknown tools.** Behind the coarse `invoke_query` gate (a stored mutation is additionally `change`-gated), so the catalog can't be probed by a caller lacking the grant.
+
+Host/Origin posture is fail-closed and derived from the bind: a loopback bind accepts the full loopback `Host` set (`127.0.0.1`/`::1`/`localhost`); a remote bind rejects unexpected browser `Origin`s. Full client guide: `docs/user/operations/mcp.md`.
+
## Server + Policy Together
When the server is running with a policy file:
diff --git a/skills/omnigraph/references/stored-queries.md b/skills/omnigraph/references/stored-queries.md
index 02aaf75..b03ff05 100644
--- a/skills/omnigraph/references/stored-queries.md
+++ b/skills/omnigraph/references/stored-queries.md
@@ -15,7 +15,7 @@ graphs:
queries: queries/ # discover every `query ` in queries/*.gq
```
-`queries` also accepts an explicit file list (`[a.gq, b.gq]`) or a fine-grained `name: { file: … }` map; an unparseable `.gq` or a duplicate query name across files fails `cluster validate`. `cluster apply` publishes them to the content-addressed catalog, and the `--cluster` server type-checks and serves every applied query. Every applied query is listed (per-query `mcp:`/expose flags are a planned phase).
+`queries` also accepts an explicit file list (`[a.gq, b.gq]`) or a fine-grained `name: { file: … }` map; an unparseable `.gq` or a duplicate query name across files fails `cluster validate`. `cluster apply` publishes them to the content-addressed catalog, and the `--cluster` server type-checks and serves every applied query. Per-query MCP presentation (`expose`, `tool_name`) is set in the `.gq` source via the `@mcp(...)` annotation (see *MCP exposure* below), so it travels content-addressed with the query — no `cluster.yaml` per-query block needed.
## CLI
@@ -46,7 +46,23 @@ omnigraph queries list # print the addressed graph's registry: query nam
## MCP exposure
-Every applied query is listed in `GET /graphs/{id}/queries` as a typed MCP tool. Per-query exposure controls (`mcp.expose`, `tool_name`) are a planned phase — there is no per-query `mcp:` flag in cluster mode today.
+Stored queries are surfaced two ways: the REST catalog `GET /graphs/{id}/queries` (typed entries for a client to register), and — since v0.8.0 — **live MCP tools** at `POST /graphs/{id}/mcp` (per-query tools below an exposed-count threshold, or a `stored_query_list` + `stored_query_run` pair above it). See [`server-policy.md`](server-policy.md#mcp-surface) and `docs/user/operations/mcp.md`.
+
+Per-query MCP presentation is set in the `.gq` source via `@mcp(...)`:
+
+```gq
+query find_user(@description("the user's slug") $slug: String)
+ @description("Look up a user by slug.")
+ @instruction("Use for exact slugs; for fuzzy names use search_users.")
+ @mcp(tool_name: "lookup_user", expose: true)
+{ match { $u: User { slug: $slug } } return { $u.name } }
+```
+
+- `@mcp(tool_name: "…")` — the MCP tool id (default: the query name). Must be unique across exposed queries and must not shadow a built-in, or the server refuses to boot.
+- `@mcp(expose: false)` — hide from `tools/list`, `stored_query_list`, and `stored_query_run` (by name). Presentation only: the query stays HTTP/service-callable via `POST /queries/{name}` for any caller with `invoke_query` (which is the *authorization* gate — Cedar, not `expose`).
+- The MCP tool description folds `@instruction` after `@description`; per-param `@description` documents each argument in the tool input schema.
+
+Defaults (no `@mcp`): exposed, tool name = query name. There is no `cluster.yaml` per-query block — the source annotation is the single home.
## Note on per-query authorization