feat(mcp): in-server MCP surface (omnigraph-mcp crate) + authoring controls (RFC-003)#182
feat(mcp): in-server MCP surface (omnigraph-mcp crate) + authoring controls (RFC-003)#182ragnorc wants to merge 16 commits into
Conversation
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 3009564437
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
|
|
||
| - A generic `struct McpService<B>` implements rmcp's `ServerHandler`, delegating each method to `B` after extracting `&Parts` from `ctx.extensions` once (missing → `McpError::internal_error`). `get_info → backend.server_info()`; `initialize`/`ping` use rmcp's defaults. | ||
| - `pub fn mcp_router<B: McpBackend>(backend: B, body_limit: usize) -> axum::Router`: | ||
| - `let svc = StreamableHttpService::new(move || Ok(McpService::new(backend.clone())), Arc::new(LocalSessionManager::default()), config)` with `config = StreamableHttpServerConfig::default().with_stateful_mode(false).with_json_response(true)` (`StreamableHttpServerConfig` is `#[non_exhaustive]` — build from `Default`, mutate via the `with_*` setters; keep the loopback `allowed_hosts` default). |
There was a problem hiding this comment.
Configure allowed hosts for non-loopback MCP deployments
In any remote or cloud deployment, keeping rmcp's default allowed_hosts makes /mcp reject valid clients before bearer auth, because rmcp 1.7 defaults Host validation to loopback authorities only (localhost, 127.0.0.1, ::1). This RFC explicitly targets remotely reachable MCP clients, so the blueprint should require a server-configured allowed-host allowlist (or a documented reverse-proxy Host validation setup) instead of preserving the loopback default.
Useful? React with 👍 / 👎.
| - **Ad-hoc `query`/`mutate` always exposed, Cedar-only**; no `mcp.allow_adhoc`. [Open Q3 → resolved.] | ||
| - **`query`/`mutate` ids only**, no `read`/`change` aliases. [Open Q7 → resolved.] | ||
| - **Per-graph `/mcp` routing**; `graphs_list`/`omnigraph://graphs` **dropped from MCP** (graph discovery via REST `GET /graphs`); no server-scoped MCP tools. [Open Q5 → resolved.] |
There was a problem hiding this comment.
Reconcile the stale rollout with locked decisions
These locked decisions conflict with the still-active Rollout and Testing sections below, which continue to tell implementers to ship graphs_list, read/change aliases, a server-level /mcp, and an mcp.allow_adhoc switch. Since the status says the blueprint only supersedes the older §5 sketches, someone following the Rollout section would build surface area this canonical section says was dropped; please update those later sections or explicitly mark them historical too.
Useful? React with 👍 / 👎.
|
Addressed the valid review findings as correct-by-construction fixes (commit
The "missing |
…crate) Brings RFC-003 to the canonical from-scratch spec for the in-server MCP surface: a dedicated omnigraph-mcp transport crate + McpBackend trait (server -> mcp dep to avoid a Cargo cycle), the verified rmcp 1.7 design (conformance MUSTs for free), backend reuse (do_* / run_query / run_mutate / authorize / api), the 13-tool catalog + Cedar mapping, ParamKind -> JSON Schema, deny-masking + isError split, two resources, per-graph routing (graphs_list stays REST-only), auth + client-compat matrix + OAuth fast-follow, tests, and locked decisions. Build the implementation on this branch from blueprint sections B.1-B.13. The spec is informed by the #157 reference implementation.
Fold the valid #157 review findings into the blueprint as by-design fixes so a from-scratch build cannot reintroduce them: - B.3.1: host/origin policy derived from the server bind + config (loopback -> loopback hosts; non-loopback -> configured public host, else Host-allowlist off with bearer + Origin as the controls). Closes the loopback-only-default footgun that 403s every remote client before bearer auth. - B.6/B.7: stored-query params nested under a `params` object (mirrors POST /queries/{name}), so the branch/snapshot knobs cannot collide with a query param; mutation tools omit `snapshot` + additionalProperties:false, so mutation-against-a-snapshot is unrepresentable. Vector `dim` made intrinsic to the kind, killing the unwrap_or(0) zero-length-array schema. - B.8: one canonical classify(Result<_, ApiError>) for tool errors (5xx -> JSON-RPC, 4xx/409 -> isError), no second mapper to drift; list visibility uses the call-path default-branch authorization, not a branch:None probe that hid branch-scoped-grant tools. - Reconciled the stale Summary count and the section-5-era Rollout/Testing sections (banners pointing to B.12/B.4/B.13).
Rebased onto current main (0.7.0-dev) and reconciled the MCP blueprint
with the code it builds on, replacing the earlier blueprint sketch with a
spec grounded in real interfaces (each EXISTING snippet cited to file:line,
re-read from source). Corrections (delta table in §0):
- ParamKind::Vector is a unit variant; the dimension is
ParamDescriptor.vector_dim: Option<u32>, not an intrinsic struct field.
The JSON-Schema builder now omits minItems/maxItems when the dim is
absent instead of emitting 0.
- Server handlers, auth helpers, and run_query/run_mutate moved from
lib.rs to handlers.rs; stored-query config in config.rs; DTOs in api.rs.
- ingest/load unified: server_ingest calls load_as; a missing branch with
no `from` is a 404, never an implicit fork. The MCP tool is named `load`.
- Client credentials follow the shipped RFC-007 model (OperatorServer { url },
env -> credentials-file chain, no keychain step, no nested auth: block).
- Test references updated for the split server suites (mcp.rs / stored_queries.rs,
not --test server).
- Stored-query declaration source noted as cluster.yaml file-discovery with
omnigraph.yaml as the RFC-008-deprecated legacy surface.
Folds the standalone implementation-spec draft into this file so there is one
MCP RFC; cross-links rfc-005/007/009 now that they are in the corpus.
2ade97f to
61c18b6
Compare
There was a problem hiding this comment.
ragnorc has reached the 50-review limit for trial accounts. To continue receiving code reviews, upgrade your plan.
Rewrites the MCP RFC as a single self-contained specification, validated against the live upstream surfaces it builds on: - MCP protocol revision 2025-11-25 (Streamable HTTP transport requirements, authorization model, tool/resource shapes, SEP-1303 input-validation-as- tool-error, JSON Schema 2020-12 default). - rmcp 1.7.0 (StreamableHttpServerConfig real defaults — stateful_mode defaults true and must be flipped; NeverSessionManager for stateless mode; the `local` feature must stay off; RequestContext.extensions carries the http::request::Parts passthrough; RPITIT ServerHandler). - Tool/server best practice: domain-qualified tool names, explicit annotations, structured output + text mirror, and progressive disclosure (per_query vs meta projection modes) for large stored-query catalogs. Adds a provider compatibility matrix (Claude Code/Desktop/web, Messages API connector, OpenAI Responses API/ChatGPT, Cursor, VS Code, OpenCode) with a phased static-bearer -> OAuth 2.1 + RFC 9728 auth plan and the Claude Code PRM header-override caveat, and a code-mode compatibility section describing the server-side properties that make the surface code-execution-friendly without building client-side machinery.
There was a problem hiding this comment.
ragnorc has reached the 50-review limit for trial accounts. To continue receiving code reviews, upgrade your plan.
Add §15.1 making the multi-graph model explicit (it was implied across
§9/§14/§15): one isolated MCP server per graph at /graphs/{id}/mcp with the
graph id in the URL path; cross-graph discovery is REST-only via GET /graphs
(server-scoped GraphList, default-denied without a server policy); stored-query
registries, tool-name uniqueness, projection mode, and InvokeQuery are all
per-graph; clients configure one connection per graph and the connection name
namespaces the otherwise-identical tool ids.
There was a problem hiding this comment.
ragnorc has reached the 50-review limit for trial accounts. To continue receiving code reviews, upgrade your plan.
…/load)
Re-validated RFC-003 against the merged tree (RFC-009/010/011/012). Folds in
the deltas that landed on main since the RFC was written:
- §15 routing: RFC-011 made the server cluster-only. GraphRouting::Single/Multi
and the `match state.routing()` branch are gone; build_app always nests
per_graph_protected under /graphs/{graph_id}. So /mcp is always
/graphs/{id}/mcp (even a single-graph boot is a one-graph registry keyed by
`default`). §15.1's per-graph model is now the only model. Line refs repointed
to lib.rs:876/929/953/954/148.
- §9 stored queries: param/catalog DTOs moved to omnigraph-api-types (RFC-009),
re-exported via api.rs — citations repointed (lib.rs:355/373/496). The legacy
omnigraph.yaml `queries:` declaration source is removed; cluster.yaml
graphs.<id>.queries is the sole source. Flag the §D5 bridge: cluster boot
forces expose=true/tool_name=None today, so per-query expose/tool_name is a
planned phase (added to §17 deferred).
- §11 load: /ingest -> canonical POST /load (RFC-009 Phase 5); graph_load reuses
run_ingest via server_load (handlers.rs:1320), /ingest is a deprecated alias.
- Mechanical: policy/handler/identity/ApiError line-number drift repointed
(policy:16/251, handlers:313/334/711/645/913, identity:186, lib.rs:280).
No architectural change — RFC-011 confirms the per-graph model; the rest is
re-citation and the expose/tool_name caveat.
An external review pass raised 8 findings; verified 7 valid (2 confirmed
against the engine coercer). Folded them in as class-closing fixes rather than
point patches:
- §9.1 (③④, the headline): the JSON-Schema generator was a second hand-written
copy of the engine's input contract — Blob (base64 vs URI string) and nullable
(explicit null) were two drifts of one class. Move the projection to a single
param_json_schema in omnigraph-api-types (next to ParamKind/ParamDescriptor),
fix Blob -> {"type":"string","format":"uri"} (query_input.rs:449 / api-types:354
say blob-URI string) and nullable -> anyOf[..,null] (query_input.rs:273,296),
and lock it to json_value_to_literal_typed with a schema/engine equivalence
test so any future drift is a CI failure.
- §7/§4 (①): replace the fail-open "empty allowed_origins => skip" with a total
OriginPolicy and a single McpHostPolicy::from_bind constructor (remote default
DenyBrowsers, enforced by origin_guard independent of rmcp's empty-list quirk).
No absent-=>-skip state can be constructed.
- §6/§12/§16 (②): make the non-paginated list seam a stated contract (Vec<T>,
no nextCursor; meta mode bounds large catalogs) and drop the pagination claims
the signature couldn't express.
- §9.3 (⑦): built-in/stored tool-name collision becomes a cluster validate/boot
error (fold built-in names into the registry uniqueness check), not a silent
skip — per the invariants deny-list.
- §9.2 (⑥): stored_query_mode folded into the one per-graph mcp: block (Phase 6),
not a floating key; not configurable until that surface exists.
- §10/§1 (⑧): scope derives from the per-graph mount; server-scoped `health`
becomes graph-scoped `graph_health` (server liveness stays REST /healthz).
- §13 (⑤, doc-only): OpenAI row corrected to the `authorization` field; Phase-1
reachability via static bearer is unchanged.
§17 records the locked decisions; the validation header notes the review pass.
General server/topology/auth/deployment RFC resolving the half-built tenancy ambiguity (cluster-only server vs pooled tenant_id scaffolding). Decision: the cluster is the tenant is the cell — silo the data (own storage/catalog/ policy/tokens), pool the compute (one process : N cells). No row-level pooling (no engine RLS). - §5.1 CellRuntime lifts today's per-cluster runtime into a value. - §5.2/§5.3 AppState holds a CellRegistry; resolve_cell is one new outer middleware hop before auth; the per-graph + Cedar + MCP stack is unchanged. - §5.4 per-cell CellAuth (Static | Oidc TokenVerifier); WorkOS org -> cell 1:1 with per-cell OAuth audience (cross-tenant token replay fails on aud). - §5.5 Cedar stays per-graph/per-cell; default-deny-read becomes safe; no tenant dimension needed. - §5.6 control plane = Cell Registry (metadata only) + provisioning-as-code; cell hot-load is the one safe runtime mutation (cell-granular, not graph). - §5.7 tiered dedicated/pooled/on-prem on one binary; §7 backward-compatible (today's single-cluster server = a one-cell map). MCP (rfc-003) is one consumer, not the driver. Linked from docs/dev/index.md.
… write tier The substrate makes read/write scaling asymmetric: reads are object-store-backed, snapshot-isolated, stateless -> horizontal to N replicas with zero coordination; writes are serialized per (table,branch) by one-winner manifest CAS -> scale by partitioning (branches/graphs/cells), with a single active coordinator per (cell,graph,branch). Adds the CQRS deployment split (read fleet / write tier / maintenance / heavy reads), read-your-write via commit_id/snapshot_id pinning, and gates pooled WRITES (not reads) on closing the cross-process-CAS gap.
…urce projection (RFC-003)
Add the `omnigraph-mcp` crate (stateless Streamable-HTTP transport, `McpBackend`
seam, fail-closed Host/Origin policy) and the server backend projecting built-in
operations and the per-graph stored-query registry as MCP tools + resources over
`POST /graphs/{id}/mcp`. Every tool delegates to the same engine/handler
functions the REST routes use and is gated by the same Cedar `authorize` path;
reads/writes carry structured output.
Includes three correctness fixes from review + live testing:
- tools/list is a faithful relaxation of the per-call gate: a built-in whose
authorization depends on a caller-chosen branch is shown iff the actor could
invoke it on some branch, via PolicyEngine::permits_on_any_branch (capability
probe through the same Cedar authorizer). A fabricated-`main` probe wrongly
hid graph_mutate under the canonical "protect main, write unprotected" policy.
- The stored-query surface honors mode + `expose` on call as well as on list:
resolve_stored_tool is the single membership test, so the meta pair
(stored_query_list/stored_query_run) is callable only in `meta` mode and
stored_query_run resolves exposed-only. An `expose:false` query is unreachable
by name on the agent surface (it stays HTTP/service-callable).
- The loopback Host allow-list is the full set [127.0.0.1, ::1, localhost]
(matches rmcp's default), so an IPv6 loopback `Host: [::1]` is accepted
regardless of which stack the server bound.
The protocol-version contract is documented (initialize negotiates the version
in its body, so the MCP-Protocol-Version header is validated on non-init
requests only) and pinned by a test.
Tests: omnigraph-mcp/tests/standalone.rs, omnigraph-server/tests/mcp.rs,
omnigraph-policy permits_on_any_branch unit test, omnigraph-api-types schema
projection. Full workspace gate green.
…Instruction folding Wire the `.gq` authoring surface that controls how a stored query is projected as an MCP tool. All of it rides in the query source (content-addressed, re-parsed at boot), so there is no cluster.yaml / catalog / serving-snapshot plumbing — and it is orthogonal to Cedar `invoke_query` (presentation, not authorization). - Per-parameter `@description("…")` (leading the variable) → carried on `Param.description`, mapped through `param_descriptor`, and emitted on the outer JSON-Schema property by `param_json_schema`, so it shows up in both the MCP tool input schema and the `GET /queries` catalog. - Query `@mcp(expose: <bool>, tool_name: "<name>")` → parsed into `QueryDecl.mcp`; `StoredQuery::is_exposed()` / `effective_tool_name()` resolve from it. `expose: false` hides a query from the agent surface (`tools/list`, `stored_query_list`, run-by-name) while keeping it HTTP/service-callable. - `@instruction` is folded into the MCP tool description (after `@description`), so the agent-facing how/when-to-use guidance reaches `tools/list`. - Removes the now-dead `RegistrySpec.{expose, tool_name}` fields (server + CLI); `settings.rs` no longer hardcodes `expose: true`. Test helpers express exposure by injecting `@mcp(expose: false)` into the source (the real path). openapi.json regenerated: `ParamDescriptor` gains an optional `description`. Tests: compiler parser (param @description, @mcp parse + duplicate rejection), api-types schema_equivalence (description on the outer property), server mcp (folded description + param docs + @mcp tool rename, list==call). Full workspace gate green.
…0.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.
Bring the MCP feature branch up to date with main (14 commits). One conflict — compiler/parser.rs: main's `NanoError` → `CompilerError` rename vs this branch's `@mcp` / per-param `@description` parser additions; resolved by keeping the new parsing under the renamed error type. The CLI `queries list` change (#280, surfacing `@description`/`@instruction`) auto-merged with this branch's `mcp_expose`/`tool_name` columns.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit fbf455a. Configure here.
| ); | ||
| if !hay.contains(filter) { | ||
| continue; | ||
| } |
There was a problem hiding this comment.
Filter ignores MCP tool name
Low Severity
The stored_query_list filter only matches registry name and description, not tool_name. Summary rows include tool_name, so substring search fails when the @mcp(tool_name: …) override differs from the registry key—agents cannot discover queries by the MCP id the list shows.
Reviewed by Cursor Bugbot for commit fbf455a. Configure here.
Four guards/refactors that convert previously convention-enforced MCP
invariants into ones a future edit can't silently break:
- H1 (loopback Host set): standalone.rs surface-guard asserts our loopback
allow-list is a superset of rmcp's own default set, so an rmcp bump that
adds a loopback form turns red instead of 403'ing that client. Pins the
::1 regression by construction rather than by a hand-kept literal list.
- H2 (one stored-query gate): extract a single invoke_query_request() in
handlers.rs used by GET /queries, POST /queries/{name}, and (imported) the
MCP tools/list + tools/call stored paths. REST and MCP can no longer drift
on which Cedar action governs the catalog. Deletes mcp.rs's local copy.
- H3 (expose chokepoint): tests/mcp.rs source-walk guard bans .lookup( and
registry.iter( in mcp.rs, so the agent surface can only reach stored
queries through exposed()/exposed_by_name() — an @mcp(expose:false) query
cannot leak back into tools/list via the expose-ignoring registry methods.
- H4 (list-gate relaxation lower-bound): a permit-all actor must see every
built-in tool, pinning the other end of the list-gate fix (no callable
tool may be hidden from tools/list).
cargo test -p omnigraph-mcp -p omnigraph-server green (the lone schema_routes
flake was disk-pressure during the clean build; passes in isolation).


Summary
Ships the in-server MCP (Model Context Protocol) surface for Omnigraph (RFC-003): a per-graph endpoint
POST /graphs/{id}/mcpthat lets an MCP agent (Claude Code/Desktop, Cursor, the OpenAI Responsesmcptool) drive a graph directly over stateless Streamable HTTP — reusing the same engine/handler paths and Cedar gates as the REST routes. Lands under v0.8.0.This branch began as a docs-only RFC blueprint; it now carries the full implementation, the
.gqauthoring controls, the correct-by-design review fixes, and a merge withmain.What's in it
omnigraph-mcpcrate — owns thermcpStreamable-HTTP transport, theMcpBackendtrait seam, and a fail-closedMcpHostPolicyderived from the bound socket (from_bindafterTcpListener::bind). Dependency direction isserver → mcp(the trait inverts it, so the crate names no omnigraph type). Crate-level transport conformance lives inomnigraph-mcp/tests/standalone.rs.Server backend (
OmnigraphMcpBackend) — projects 13 built-in tools (query / mutate / load / snapshot / schema / branch / commit / health), the schema + branches as resources, and the stored-query registry as tools (per_querybelow 24 exposed queries; astored_query_list+stored_query_runmeta pair above). Structured output (structuredContent) on every result. Every tool delegates to the existingrun_query/run_mutate/run_ingest/authorizepaths — no new business logic..gqauthoring controls (source-carried, content-addressed; zero cluster plumbing):@mcp(expose: <bool>, tool_name: "<name>")— MCP presentation: visibility on the agent surface + the tool id.@description("…")→ the tool input-schema propertydescription.@instructionfolded into the tool description so an agent reads it intools/list.Authorization —
tools/listis a faithful relaxation of the per-call gate viapermits_on_any_branch/authorize_any_branch(a tool callable on some branch is never hidden; the per-call gate stays authoritative). The list gate is derived from each tool's call shape: fixed-branchless reads (schema_get/branch_list/commit_get) and the resources share oneread(None)gate; branch-arg tools relax.exposeis presentation, not authorization — Cedarinvoke_query(+ the innerread/change) governs who may call.Correct-by-design review fixes (this branch)
from_specsvalidates every published tool name ([A-Za-z0-9_-], ≤64) and reserves the full surface-generated set (built-ins plus thestored_query_list/stored_query_runmeta pair), so a malformed or reserved name fails boot loudly instead of producing a client-rejected catalog or a silent threshold-crossing shadow.GET /queriesis nowinvoke_query-gated (wasread), matching the MCP surface and the endpoint's purpose ("see the menu iff you can order from it"). The one observable REST behavior change — documented in the v0.8.0 release note.127.0.0.1/::1/localhost) so an IPv6-loopbackHostisn't rejected; theMCP-Protocol-Versionheader is validated on non-initializerequests (documented + tested).Docs & skill
User: new
docs/user/operations/mcp.md(client guide) +server.md/index.md;.gqannotations inqueries/index.md;v0.8.0release notes. Dev: RFC-003 aligned +testing.md. Skill: a dedicatedskills/omnigraph/references/mcp.mdplus SKILL.md / server-policy / stored-queries updates.openapi.jsonregenerated.Merge with
mainMerged
origin/main(14 commits). One conflict —compiler/parser.rs(main'sNanoError→CompilerErrorrename vs this branch's@mcp/ param-description parser additions), resolved by keeping the new parsing with the renamed error type. The CLIqueries listchange (#280, surfacing@description/@instruction) auto-merged cleanly alongside this branch'smcp_expose/tool_namecolumns.Full workspace gate green;
cargo tree -p omnigraph-server -e normal | grep rmcpshows rmcp only underomnigraph-mcp.Note
High Risk
Large new agent-facing HTTP surface with auth, policy list/call semantics, and write/load tools reusing production paths; misconfiguration of Origin policy or list/call drift would affect security and operability.
Overview
Introduces
omnigraph-mcp(stateless Streamable-HTTP viarmcp,McpBackendtrait, fail-closed Host/Origin policy from the bound socket) and mergesPOST /graphs/{id}/mcpinto each graph’s Axum group so bearer auth andGraphHandlereach the backend before rmcp runs.The server
OmnigraphMcpBackendprojects 13 built-in tools, schema/branches resources, and stored queries (per-query tools below 24 exposed,stored_query_list/stored_query_runabove), delegating to existingrun_query,run_mutate, andrun_ingestpaths.tools/listusespermits_on_any_branch/authorize_any_branchso branch-scoped tools aren’t hidden when the actor can call them on some branch;call_toolstays authoritative, with stored-query denials masked as unknown tools.Query surface:
@mcp(expose, tool_name)and param@descriptionin.gq; registryexpose/tool_namefields removed.param_json_schemainomnigraph-api-typesfeeds MCP/OpenAPI and is locked to the coercer byschema_equivalencetests.GET /queriesis gated oninvoke_query(aligned with MCP catalog discovery).Reviewed by Cursor Bugbot for commit fbf455a. Bugbot is set up for automated code reviews on this repo. Configure here.
Greptile Summary
This PR ships the in-server MCP surface for Omnigraph (
omnigraph-mcpcrate +OmnigraphMcpBackend): a per-graphPOST /graphs/{id}/mcpendpoint using stateless Streamable HTTP that projects 13 built-in tools, schema/branches resources, and the stored-query registry as MCP tools — all delegating to existing Cedar-gated engine paths with no new business logic.omnigraph-mcpcrate owns thermcpdependency behind aMcpBackendtrait seam; theMcpHostPolicyconstructor is fail-closed (non-loopback bind defaults toDenyBrowsers, not open)..gqauthoring controls (@mcp(expose, tool_name), per-param@description,@instruction) are carried in source; the registry validates tool-name format and reserves the full built-in + meta-pair namespace at boot.GET /queriesis re-gated fromreadtoinvoke_query(the one documented REST behavior change in v0.8.0), aligning catalog discovery with invocation semantics.Confidence Score: 3/5
The Cedar gate wiring is correct and the fail-closed Origin/Host policy is sound, but two defects need attention before the MCP surface is production-ready: the stored-query JSON schema misstates
paramsas optional even when non-nullable params exist, and theGET /queriesauth gate change will silently break existing callers whose token grants onlyread.Two independent correctness issues were found: (1)
stored_query_input_schemaomits the top-levelparamsfromrequiredeven when the query has non-nullable parameters, so agents make schema-valid calls that the engine rejects — the schema contract is wrong. (2) The re-gating ofGET /queriesfromreadtoinvoke_queryis a documented behavior change, but the TypeScript SDK inomnigraph-tsstill documents the endpoint as read-gated; SDK callers holding only areadgrant will receive unexpected 403s on upgrade.crates/omnigraph-server/src/mcp.rs(stored_query_input_schema, lines 552–579) andcrates/omnigraph-server/src/handlers.rs(server_list_queriesauth gate change) need the most attention; the linked TypeScript SDK (omnigraph-ts/packages/sdk/src/resources/queries.ts) also needs a documentation update for theinvoke_queryrequirement.Important Files Changed
paramsis never added to the top-levelrequiredarray instored_query_input_schemaeven when the query has non-nullable params, causing agents to make schema-valid calls that fail at runtime.authorize_any_branch+invoke_query_requesthelpers; re-gatesGET /queriesfromreadtoinvoke_query. The auth gate change is a deliberate behavior break that the linked TypeScript SDK does not reflect yet.origin_guard) andMcpHostPolicy::from_bindconstructor. Loopback vs. public bind logic is correct; DenyBrowsers default for non-loopback with no configured origins is appropriate.InvokeQueryaction (graph-scoped, no branch dimension) andpermits_on_any_branchfor list-time capability probing. Branch-shape enumeration (branchless / protected / unprotected) correctly covers all distinguishable request forms. New tests cover the relaxation semantics.expose/tool_namemoved from spec fields to source@mcp(...)annotations;from_specsnow validates tool names and reserves the full built-in + meta pair namespace at boot.exposed_by_namecorrectly gates unexposed queries from the MCP surface.param_json_schema(single mapping for both OpenAPI catalog and MCP input schemas) anddescriptionfield onParamDescriptor. Nullable handling (anyOf + null) and integer/bigint dual-type schemas match the engine coercer contract documented in the equivalence tests.McpHostPolicyfrom the bound socket address afterTcpListener::bind(preventing the loopback-default fail-open on public binds). Tests skipwith_mcp_host_inputs, correctly getting the loopback-safe default.Sequence Diagram
%%{init: {'theme': 'neutral'}}%% sequenceDiagram participant Agent as MCP Agent participant Transport as McpHostPolicy + origin_guard participant Auth as require_bearer_auth / resolve_graph_handle participant Backend as OmnigraphMcpBackend participant Cedar as PolicyEngine participant Engine as Graph Engine Agent->>Transport: "POST /graphs/{id}/mcp" Transport->>Transport: Check Host header (allowed_hosts) Transport->>Transport: Check Origin header (OriginPolicy) Transport->>Auth: Axum route_layer Auth->>Auth: resolve bearer → ResolvedActor (extensions) Auth->>Auth: resolve graph → GraphHandle (extensions) Auth->>Backend: tools/list or tools/call alt tools/list Backend->>Cedar: authorize / permits_on_any_branch per tool Cedar-->>Backend: allow / deny per built-in Backend->>Cedar: allowed(invoke_query) for stored surface Cedar-->>Backend: can_invoke Backend-->>Agent: filtered Tool list end alt call_tool (built-in) Backend->>Cedar: authorize_request (per-call gate) Cedar-->>Backend: Authz::Allowed / Denied Backend->>Engine: run_query / run_mutate / run_ingest Engine-->>Backend: result Backend-->>Agent: CallToolResult (structuredContent) end alt call_tool (stored query) Backend->>Cedar: authorize invoke_query Cedar-->>Backend: Allowed or Denied→unknown_tool Backend->>Engine: run_query / run_mutate (inner read/change gate) Engine-->>Backend: result Backend-->>Agent: CallToolResult (isError on 4xx) end%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%% sequenceDiagram participant Agent as MCP Agent participant Transport as McpHostPolicy + origin_guard participant Auth as require_bearer_auth / resolve_graph_handle participant Backend as OmnigraphMcpBackend participant Cedar as PolicyEngine participant Engine as Graph Engine Agent->>Transport: "POST /graphs/{id}/mcp" Transport->>Transport: Check Host header (allowed_hosts) Transport->>Transport: Check Origin header (OriginPolicy) Transport->>Auth: Axum route_layer Auth->>Auth: resolve bearer → ResolvedActor (extensions) Auth->>Auth: resolve graph → GraphHandle (extensions) Auth->>Backend: tools/list or tools/call alt tools/list Backend->>Cedar: authorize / permits_on_any_branch per tool Cedar-->>Backend: allow / deny per built-in Backend->>Cedar: allowed(invoke_query) for stored surface Cedar-->>Backend: can_invoke Backend-->>Agent: filtered Tool list end alt call_tool (built-in) Backend->>Cedar: authorize_request (per-call gate) Cedar-->>Backend: Authz::Allowed / Denied Backend->>Engine: run_query / run_mutate / run_ingest Engine-->>Backend: result Backend-->>Agent: CallToolResult (structuredContent) end alt call_tool (stored query) Backend->>Cedar: authorize invoke_query Cedar-->>Backend: Allowed or Denied→unknown_tool Backend->>Engine: run_query / run_mutate (inner read/change gate) Engine-->>Backend: result Backend-->>Agent: CallToolResult (isError on 4xx) endReviews (7): Last reviewed commit: "test(mcp): harden symptomatic MCP fixes ..." | Re-trigger Greptile