docs(mcp): document the MCP surface, authoring controls, and skill (v0.8.0)

Document the per-graph MCP surface (POST /graphs/{id}/mcp, shipped in the
preceding commits and landing under v0.8.0) and the `.gq` authoring controls
that shape stored-query tools.

- New docs/user/operations/mcp.md: the client-facing guide — transport, tool
  catalog (built-ins + stored queries), projection modes, structured output,
  authorization (call-authoritative + list-relaxation), Host/Origin policy, the
  protocol-version contract.
- docs/user/operations/server.md: the /mcp endpoint + an "MCP surface" section;
  docs/user/index.md: a "Connect an MCP agent" pointer.
- docs/user/queries/index.md: an Annotations section — query @description /
  @instruction / @mcp(expose, tool_name) and per-parameter @description.
- AGENTS.md: topic-table row + MCP note on the HTTP-server capability row.
- docs/dev/testing.md: the omnigraph-mcp crate + server tests/mcp.rs.
- docs/dev/rfc-005 §D5: retire the "cluster = everything exposed" bridge —
  cluster mode honors source `@mcp(expose: …)`; presentation vs authorization
  split made explicit.
- skills/omnigraph: server-policy.md MCP section; stored-queries.md corrected
  (per-query controls now ship via @mcp, not "planned"); SKILL.md MCP triggers,
  Deep Dives row, version → 0.8.0.
- docs/releases/v0.8.0.md: the MCP surface + authoring-controls release notes.

Crate version manifests are deliberately NOT bumped — that is the v0.8.0
release-cut step; this lands on the feature branch.
This commit is contained in:
Ragnor Comerford 2026-06-17 16:04:29 +02:00
parent c8e91c11f0
commit c06343362a
No known key found for this signature in database
11 changed files with 349 additions and 13 deletions

View file

@ -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 <dir | s3://…>`, 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 <dir | s3://…>`, 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* |

View file

@ -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)

View file

@ -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.

89
docs/releases/v0.8.0.md Normal file
View file

@ -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: <bool>)`.** 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://<host>/graphs/<id>/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.

View file

@ -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) |

154
docs/user/operations/mcp.md Normal file
View file

@ -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.

View file

@ -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: <mutate>; 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`

View file

@ -3,8 +3,8 @@
## Query declarations
```
query <name>($p1: T1, $p2: T2?, …)
@description("…") @instruction("…") {
query <name>(@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: "<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: <bool>` — 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: <literal | $param | now()>, … }`

View file

@ -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 |

View file

@ -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:

View file

@ -15,7 +15,7 @@ graphs:
queries: queries/ # discover every `query <name>` 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