mirror of
https://github.com/ModernRelay/omnigraph.git
synced 2026-06-18 02:24:27 +02:00
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:
parent
c8e91c11f0
commit
c06343362a
11 changed files with 349 additions and 13 deletions
|
|
@ -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* |
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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
89
docs/releases/v0.8.0.md
Normal 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.
|
||||
|
|
@ -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
154
docs/user/operations/mcp.md
Normal 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.
|
||||
|
|
@ -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`
|
||||
|
|
|
|||
|
|
@ -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()>, … }`
|
||||
|
|
|
|||
|
|
@ -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 |
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue