Repository navigation
Conversation
Close the MCP rows of the graph access RFC's one set of operations. New read tools diff, branches_diff, branches_merge_base and blobs_stat, and listing on commits_list. New write tools blobs_clear, commits_publish and commits_drop, listed only with --mcp-allow-writes. mutate with a stored mutation's name and no query runs the stored-query route, or its conditional route when if_graph_commit is given; queries returns the route's whole catalog. Each tool calls its route's handler under the same actor, Cedar action, admission and error contract. The change-feed baseline stays a declared byte-stream exception: its route answers an NDJSON snapshot whose cursor is valid only once the snapshot is installed.
|
Recommendation: merge. There are no blocking findings. Two optional findings are below. Correct the discover-graphs inventory sentence in this pull request if you can. Runtime behavior already matches the user guide. WhyAn MCP client must use the same graph operations that HTTP already serves. One handler per operation keeps one authority for the actor, the Cedar action, admission, and the error body. WhatThis change exposes every non-streaming graph operation as an MCP tool. The tool calls that operation's HTTP handler. New read tools are New write tools are
The change-feed baseline stays off MCP. The route returns an NDJSON entity snapshot, and the cursor is valid only after that snapshot is installed. Blob GET, blob PUT, and export stay byte-stream exceptions. HowEach tool resolves the graph and calls the route handler under the same actor.
A stored
ImpactAn agent can diff, stat a blob, clear a blob, publish a commit, drop a commit, and run a stored mutation under the same policy as HTTP. Write tools stay closed until the operator sets The change removes the MCP-only filter that dropped mutations after the handler. The remaining cost is a hand-written tool schema beside each route. Optional findings
Tradeoff and liabilityOne handler per operation is the source of truth. Correctness of that handler comes first. A generated schema would remove description drift, and it can wait. Five more tools stay cheap while each tool calls the handler and the inventory checks the name. The part that rots is an absence reason. The inventory test accepts any non-empty reason. This pull request already carries one false sentence. Bind This change stays on Other open pull requests#1130 adds read-output fields in #1137 adds a CLI-only #1012 edits #892 edits ValidationReviewed head: Local checkout
The same integration command with the default thread stack aborted. CI on this commit:
Clippy evidence is the CI Lint job. The GQT corpus result is the CI result above. Regression proofThis change is a new MCP contract. It has no query-visible bug fix. A LimitationsThe integration loop covers a managed blob stat. The external 302 path is covered by the unit test on a synthetic response. The loop covers history and Inline:
|
The MCP `graphs` tool answers an identity credential from the discovery handler and every other credential from the ordinary listing, but the inventory recorded the discovery row with no MCP tool and a reason that said the opposite. Both rows now bind `graphs`, each naming the credential it serves.
|
Thanks. Answers to the two optional findings:
Main is merged in (6e527c1). Locally, clippy is clean on the four touched packages, and the |
What and why
The accepted graph access RFC (#1017, section "One set of operations") requires every graph operation to have one name and one JSON contract on the CLI, HTTP and MCP, with byte streams as the declared exception. On main, MCP lacked the graph diff, branch diff, merge base, Blob stat and clear, and commit publish and drop tools, refused stored mutations, filtered stored mutations out of
queries, and listed only branch history incommits_list. This PR closes those MCP rows.Every new tool follows the existing pattern: it resolves its graph through the registry and calls the handler (or the handler's body function) its HTTP route calls, under the same actor and Cedar action. Policy, admission, write ownership, the
Omnigraph-If-Actorcheck, error bodies withdisposition, the 1 MiB result bound and the 30 s waits are the routes' own. No HTTP route, body or status changes, andopenapi.jsondoes not drift.Tools
diffGET /graphs/{id}/diffreadOnlyHintbranches_diffGET /graphs/{id}/branches/diffreadOnlyHintbranches_merge_baseGET /graphs/{id}/branches/merge-basereadOnlyHintblobs_statHEAD /graphs/{id}/blob, its headers asblob statJSONreadOnlyHintcommits_list(addslisting)GET /graphs/{id}/commitsreadOnlyHintqueries(whole catalog)GET /graphs/{id}/queriesreadOnlyHintblobs_clearDELETE /graphs/{id}/blob--mcp-allow-writescommits_publishPOST /graphs/{id}/commits/{commit_id}/publish--mcp-allow-writescommits_dropDELETE /graphs/{id}/commits/{commit_id}--mcp-allow-writesmutatewithnameand noqueryPOST /graphs/{id}/queries/{name}, or/queries/{name}/if-graph-commitwithif_graph_commit--mcp-allow-writesArguments are the route's request fields plus
graph. The paging tools (diff,branches_diff) take the route'spage_tokenandlimit;blobs_cleartakes the route'sIf-Matchvalue asif_match. Two tools answer for a route without a JSON body:blobs_statreturns theHEAD /blobheaders as theBlobStatOutputthatomnigraph blob stat --jsonprints, andcommits_dropreturns{}for the route's 204. Both are recorded as restricted bindings in the operation inventory.Stored mutations
Admitted.
mutategiven a stored mutation'snameand noqueryruns it, asomnigraph mutate NAMEdoes, throughserver_invoke_query(withexpect_mutation: true) or, whenif_graph_commitis present, throughserver_invoke_query_if_graph_commit. A value forif_graph_commitalways selects the conditional route that requires it, so a stale value is the route's 412, and an empty or entity-tag value is the route's 400. None of them falls through to an unconditional write. The plain route never receives the header.querystays read-only and still refuses stored mutations.This does not contradict RFC 0003's deferral. That deferral was about dynamic per-query tools, which would list every stored query to every actor before per-query
invoke_queryscope exists, and it is unchanged. Admitting stored mutations adds no capability: a caller allowedchangecan already run any mutation throughmutate, and the stored route is gated more narrowly (invoke_query, thenchange). RFC 0003 is amended in place with a decision-log entry, as phase 3.Declared, not added
The change-feed baseline (
POST /changes/baseline) stays off MCP. The RFC's gap table listed it as a tool, but the route answers an NDJSON entity snapshot, which is an export stream, followed by a terminal cursor that is valid only once that snapshot is installed. A bounded tool result can carry neither the stream nor a cursor without its snapshot, so the baseline falls under the RFC's own byte-stream exception. The inventory, the RFC body and its decision log record this; reviewers may want to confirm that reading. Blob payloads (GET/PUT /blob) andexportremain byte-stream exceptions as before.Tests
cargo test --locked -p omnigraph-mcp -p omnigraph-server -p omnigraph-api-types --lib: 16 + 7 + 218 passed (2 ignored, as on main). New: every tool parses its schema-required arguments and refuses an undeclared one, and write tools are annotated as writes and absent withoutallow_writes(MCP crate); the HEAD-to-blob statmapping for managed, external and malformed answers (server).cargo test --locked -p omnigraph-server --test integration mcp::: 11 passed (10 on the main baseline). Extended owners: the tool list per boot gate (compared against the operation inventory), read parity ofdiff,branches_diff,branches_merge_baseandcommits_listwithlisting(including continuation pages and the routes' refusal bodies), write tools absent without--mcp-allow-writesand their annotations, andqueriesequal to its route. Newmcp_blob_commit_control_and_stored_mutation_tools_equal_their_routes:blobs_statequals the HEAD headers, refusals equal GET's;blobs_clear412, receipt and no-op equalDELETE /blob;commits_publishandcommits_dropequal their routes, including republish, the dropped 404 and the published 409; a stored mutation throughmutatematches the stored route, and stale, empty and entity-tag preconditions refuse as the conditional route does with the manifest unchanged; policy denials of every new tool equal the route's status and body.cargo test --locked -p omnigraph-server --test integration -- openapi:: data_routes::blob: 112 passed, includingopenapi::openapi_spec_is_up_to_date(no drift) and the Blob routes after theDELETE /blobbody extraction.cargo test --locked -p omnigraph-skill: 1 passed.cargo test --locked -p omnigraph-cli --lib skill::: 7 passed.cargo test --locked -p omnigraph-cli --test integration cli_mcp::: 7 passed; its write-tool list now names the three new write tools, which the bridge lists only with--writeagainst a server with--mcp-allow-writes.cargo clippy --locked -p omnigraph-mcp -p omnigraph-server --all-targets -- -D warnings -W clippy::dbg_macro: clean.cargo fmt --all --check,python3 scripts/check-docs.py(391 files) andtyposon the changed files: clean.Docs: the MCP guide's tool tables and write contract, RFC 0003's catalog (amendment and decision log), the graph access RFC's operation tables (decision log), the stored-queries skill reference, the server's
--mcp-allow-writeshelp, andchangelog.d/mcp-every-graph-operation.added.md.Note for the operation inventory
The inventory from #1132 records
discover graphs(GET /graphs/discovery) with no MCP tool. In fact the MCPgraphstool calls the discovery handler for an identity credential and answerslist graphsin the discovery shape. This PR leaves those two rows as they are, so the existing inventory tests keep their meaning. A follow-up can bindgraphsto both rows, each with a note on which shape it answers.