omnigraph/docs/releases/v0.8.0.md
Ragnor Comerford 4d4c2164de
Merge branch 'main' into ragnorc/omnigraph-mcp-crate
Folds in v0.7.2 (release #301) + RFC-013 Phase 7 (graph lineage in __manifest,
internal schema v3→v4 migration #299; WriteTxn #298; recovery convergence #296)
under the MCP branch.

Conflict resolutions (2 files):
- crates/omnigraph-server/Cargo.toml: take main's 0.7.2 path-dep constraints;
  keep our omnigraph-mcp dep (bumped to 0.7.2).
- docs/releases/v0.8.0.md (add/add): both branches drafted v0.8.0 notes for the
  same next minor — combined them. v0.8.0 now documents BOTH the MCP surface
  (ours) and main's __manifest lineage fold + the breaking internal-schema-v4
  upgrade-order requirement (kept prominent under Upgrade notes). Corrected our
  'no breaking changes / on-disk format unchanged' line, which the v4 migration
  makes false.

Coherence: omnigraph-mcp [package] + Cargo.lock bumped 0.7.1→0.7.2; openapi.json
auto-merged to info.version 0.7.2 (no API-surface drift from the incoming
engine-internal commits). Verification deferred to CI (no local rebuild).
2026-06-25 15:53:53 +02:00

152 lines
8.7 KiB
Markdown

# Omnigraph v0.8.0
v0.8.0 has two headline changes:
1. **Every served graph becomes 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. 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. It is **additive**.
2. **Graph commit lineage moves into `__manifest`** (RFC-013 Phase 7), folded
into the publish CAS, via a one-time on-disk migration (internal schema
**v3 → v4**). This is the first internal-schema change since v0.4.0 and carries
an **upgrade-order requirement** — read the upgrade notes before rolling it out.
## MCP surface (`POST /graphs/{id}/mcp`)
An MCP-capable agent can connect to a graph and run reads and mutations, load
data, manage branches, browse commits, read the schema, and invoke the graph's
curated stored queries.
- **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.
## Graph lineage now lives in `__manifest` (internal schema v4)
The graph commit DAG (commits, parents, merge parents, per-branch heads, and the
authoring actor) is now stored in `__manifest` as `graph_commit` / `graph_head`
rows, written in the **same commit (CAS)** as the table-version rows of a graph
publish. Previously the lineage lived in a separate `_graph_commits.lance`
dataset written after the manifest commit, leaving a narrow window where a crash
could land a manifest version with no matching lineage row. Folding the lineage
into the publish closes that gap by construction: a graph commit and its lineage
now land atomically at one manifest version. The in-memory commit graph is a
projection of those manifest rows; `_graph_commits.lance` is retained only as a
carrier for Lance branch refs and no longer receives commit rows.
This bumps the `__manifest` internal schema stamp from **v3 to v4**.
### Existing graphs migrate seamlessly on first write
A graph created by an earlier binary (internal schema v3) keeps its lineage in
`_graph_commits.lance` with none in `__manifest`. On the **first read-write
open**, Omnigraph backfills that lineage into `__manifest` (the `migrate_v3_to_v4`
internal-schema step) and bumps the stamp to v4. The migration:
- is **per-branch** — each branch backfills on its first write;
- is **idempotent and crash-safe** — the stamp bump is the last step, and the
backfill is keyed on the commit id, so a crash mid-migration re-runs harmlessly
on the next open;
- **preserves all data** — every commit, parent, merge parent, actor, and head is
carried over; commit ids are stable, so existing references still resolve.
No data is lost and no operator action is required beyond upgrading the binary.
Before its first write migrates the graph, a **read-only** open of a v3 graph
(e.g. `omnigraph commit list`, NDJSON export) still reads correct history via a
transitional fallback that sources the commit DAG from `_graph_commits.lance`
read-only opens never write, so they never migrate, but they never show an empty
history either.
## Upgrade notes
- **Breaking: internal schema v4 — upgrade writer (and reader) binaries first.**
Internal schema v4 is a hard version gate. Once a graph has been opened for
write by a v0.8.0 binary, its `__manifest` is stamped v4, and an **older binary
will refuse to open it** — read-write *and* read-only — with an
`upgrade omnigraph before opening this graph` error rather than silently
misreading the new lineage. This is the standard forward-version protection
(same shape as the v1→v2 / v2→v3 steps), now enforced on the read-only path
too. Upgrade every writer (and reader) binary that touches a graph to v0.8.0
before, or together with, the first write under the new version. A mixed fleet
where an old binary still writes the same graph is unsupported, as with any
internal-schema bump.
- **`GET /graphs/{id}/queries` is now `invoke_query`-gated (was `read`).** The
stored-query catalog uses the same authority as invocation and the MCP
`tools/list` surface, so discovery and invocation agree ("see the menu iff you
can order from it"). A caller with only `read` (and no `invoke_query`) now gets
`403` instead of a listing; in default-deny mode the endpoint returns `403`
until an `invoke_query` rule is configured. This is the one observable REST
behavior change in this release.
- **The MCP endpoint is additive.** Apart from the `GET /queries` gate change and
the v4 on-disk migration above, the REST surface, CLI, and cluster config are
unchanged.
- **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.