Skip to content

feat(mcp): in-server MCP surface (omnigraph-mcp crate) + authoring controls (RFC-003)#182

Open
ragnorc wants to merge 19 commits into
mainfrom
ragnorc/omnigraph-mcp-crate
Open

feat(mcp): in-server MCP surface (omnigraph-mcp crate) + authoring controls (RFC-003)#182
ragnorc wants to merge 19 commits into
mainfrom
ragnorc/omnigraph-mcp-crate

Conversation

@ragnorc

@ragnorc ragnorc commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

Summary

Ships the in-server MCP (Model Context Protocol) surface for Omnigraph (RFC-003): a per-graph endpoint POST /graphs/{id}/mcp that lets an MCP agent (Claude Code/Desktop, Cursor, the OpenAI Responses mcp tool) 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 .gq authoring controls, the correct-by-design review fixes, and a merge with main.

What's in it

omnigraph-mcp crate — owns the rmcp Streamable-HTTP transport, the McpBackend trait seam, and a fail-closed McpHostPolicy derived from the bound socket (from_bind after TcpListener::bind). Dependency direction is server → mcp (the trait inverts it, so the crate names no omnigraph type). Crate-level transport conformance lives in omnigraph-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_query below 24 exposed queries; a stored_query_list + stored_query_run meta pair above). Structured output (structuredContent) on every result. Every tool delegates to the existing run_query / run_mutate / run_ingest / authorize paths — no new business logic.

.gq authoring controls (source-carried, content-addressed; zero cluster plumbing):

  • @mcp(expose: <bool>, tool_name: "<name>") — MCP presentation: visibility on the agent surface + the tool id.
  • per-parameter @description("…") → the tool input-schema property description.
  • @instruction folded into the tool description so an agent reads it in tools/list.

Authorizationtools/list is a faithful relaxation of the per-call gate via permits_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 one read(None) gate; branch-arg tools relax. expose is presentation, not authorization — Cedar invoke_query (+ the inner read / change) governs who may call.

Correct-by-design review fixes (this branch)

  • Tool-name contract at loadfrom_specs validates every published tool name ([A-Za-z0-9_-], ≤64) and reserves the full surface-generated set (built-ins plus the stored_query_list / stored_query_run meta pair), so a malformed or reserved name fails boot loudly instead of producing a client-rejected catalog or a silent threshold-crossing shadow.
  • Stored-query catalog gate unifiedGET /queries is now invoke_query-gated (was read), 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.
  • Host allow-list covers the full loopback set (127.0.0.1 / ::1 / localhost) so an IPv6-loopback Host isn't rejected; the MCP-Protocol-Version header is validated on non-initialize requests (documented + tested).

Docs & skill

User: new docs/user/operations/mcp.md (client guide) + server.md / index.md; .gq annotations in queries/index.md; v0.8.0 release notes. Dev: RFC-003 aligned + testing.md. Skill: a dedicated skills/omnigraph/references/mcp.md plus SKILL.md / server-policy / stored-queries updates. openapi.json regenerated.

Merge with main

Merged origin/main (14 commits). One conflict — compiler/parser.rs (main's NanoErrorCompilerError rename vs this branch's @mcp / param-description parser additions), resolved by keeping the new parsing with the renamed error type. The CLI queries list change (#280, surfacing @description / @instruction) auto-merged cleanly alongside this branch's mcp_expose / tool_name columns.

Full workspace gate green; cargo tree -p omnigraph-server -e normal | grep rmcp shows rmcp only under omnigraph-mcp.


Note

Medium Risk
New agent-facing HTTP surface and intentional GET /queries authorization change can break clients lacking invoke_query; auth still flows through existing Cedar and bearer middleware.

Overview
Adds a per-graph MCP endpoint (POST /graphs/{id}/mcp) via new crate omnigraph-mcp (stateless Streamable HTTP, McpBackend seam, fail-closed Host/Origin from the bound socket). The server implements OmnigraphMcpBackend: 13 built-in tools plus schema/branches resources, delegating to existing run_query / run_mutate / run_ingest and Cedar authorize paths—no parallel business logic.

.gq authoring moves MCP presentation into source: @mcp(expose, tool_name), per-param @description, and @instruction folded into tool descriptions. Registry load validates tool names, reserves built-in/meta tool names, and exposes only via QueryRegistry::exposed(). param_json_schema in omnigraph-api-types feeds OpenAPI and MCP inputs, with a coercer superset test.

Policy: permits_on_any_branch / authorize_any_branch so tools/list relaxes branch-scoped tools without hiding callable ones; fixed branchless reads stay faithful. invoke_query_request() gates stored-query catalog and invoke on REST and MCP—GET /queries switches from read to invoke_query and lists only exposed queries (documented REST behavior change).

Reviewed by Cursor Bugbot for commit 4d4c216. Bugbot is set up for automated code reviews on this repo. Configure here.

Greptile Summary

This PR adds the in-server MCP surface for graph access. The main changes are:

  • New omnigraph-mcp crate for Streamable HTTP transport.
  • Per-graph POST /graphs/{id}/mcp routing in the server.
  • MCP tools and resources backed by existing graph handlers and Cedar gates.
  • .gq authoring controls for MCP exposure, tool names, and parameter descriptions.
  • Stored-query catalog and documentation updates for the new surface.

Confidence Score: 4/5

This is close, but the public MCP Host policy should be fixed before merging.

  • The MCP route is mounted under the existing auth and graph-resolution path.
  • Stored-query and built-in tool calls reuse the existing authorization and handler paths.
  • A public bind can currently disable Host checking when no public host list is configured.

crates/omnigraph-mcp/src/transport.rs

Security Review

The new public MCP route can disable Host checking on non-loopback binds when no public host list is configured. This should be fixed so the new read/write surface has a compulsory host policy.

Important Files Changed

Filename Overview
crates/omnigraph-mcp/src/transport.rs Adds the MCP transport router, body limit, Host policy, and Origin guard; the public-bind Host default needs follow-up.
crates/omnigraph-server/src/mcp.rs Adds the server MCP backend for built-in tools, resources, and stored-query projection.
crates/omnigraph-server/src/lib.rs Mounts the MCP route under the per-graph server routing and passes host-policy inputs from the bound socket.
crates/omnigraph-server/src/queries.rs Moves MCP stored-query presentation controls into parsed query source metadata.
crates/omnigraph-compiler/src/query/parser.rs Parses the new query-level MCP annotation and parameter descriptions.

Fix All in Claude Code

Reviews (10): Last reviewed commit: "Merge branch 'main' into ragnorc/omnigra..." | Re-trigger Greptile

Greptile also left 1 inline comment on this PR.

Context used:

  • Context used - AGENTS.md (source)
  • Context used - CLAUDE.md (source)

@ragnorc ragnorc requested a review from aaltshuler as a code owner June 10, 2026 19:51

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread docs/dev/rfc-003-mcp-server-surface.md Outdated

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Comment thread docs/dev/rfc-003-mcp-server-surface.md Outdated
Comment on lines +157 to +159
- **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.]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Comment thread docs/dev/rfc-003-mcp-server-surface.md Outdated
Comment thread docs/dev/rfc-003-mcp-server-surface.md Outdated
@ragnorc

ragnorc commented Jun 11, 2026

Copy link
Copy Markdown
Contributor Author

Addressed the valid review findings as correct-by-construction fixes (commit 2ade97f) so a from-scratch build can't reintroduce them:

The "missing server.md MCP section" finding was stale — reviewers saw commit 94bbf67, before the docs commit added it.

Comment thread docs/dev/rfc-003-mcp-server-surface.md
ragnorc added 3 commits June 13, 2026 11:47
…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.
@ragnorc ragnorc force-pushed the ragnorc/omnigraph-mcp-crate branch from 2ade97f to 61c18b6 Compare June 13, 2026 13:20

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ragnorc has reached the 50-review limit for trial accounts. To continue receiving code reviews, upgrade your plan.

Comment thread docs/dev/rfc-003-mcp-server-surface.md Outdated
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.

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ragnorc has reached the 50-review limit for trial accounts. To continue receiving code reviews, upgrade your plan.

Comment thread docs/dev/rfc-003-mcp-server-surface.md Outdated
Comment thread docs/dev/rfc-003-mcp-server-surface.md
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.

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ragnorc has reached the 50-review limit for trial accounts. To continue receiving code reviews, upgrade your plan.

ragnorc added 6 commits June 16, 2026 16:44
…/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.
Comment thread crates/omnigraph-server/src/mcp.rs
Comment thread crates/omnigraph-server/src/mcp.rs
ragnorc added 3 commits June 17, 2026 16:04
…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.
Comment thread crates/omnigraph-server/src/mcp.rs
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.
@ragnorc ragnorc changed the title docs(rfc-003): canonical MCP implementation blueprint (omnigraph-mcp crate) feat(mcp): in-server MCP surface (omnigraph-mcp crate) + authoring controls (RFC-003) Jun 19, 2026

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Fix All in Cursor

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

Comment thread crates/omnigraph-server/src/mcp.rs
Comment thread crates/omnigraph-server/src/mcp.rs
ragnorc added 4 commits June 20, 2026 17:09
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).
Folds in v0.7.1 (release #290 + optimize/write-path/internal-table-compaction
fixes #288/#291/#297) under the MCP branch.

Conflict resolutions (5 files):
- crates/omnigraph-server/Cargo.toml: take main's 0.7.1 path-dep constraints;
  keep our omnigraph-mcp dep (bumped to 0.7.1) + http dep.
- crates/omnigraph-server/src/handlers.rs: keep our server_list_queries
  doc-comment (exposed @mcp(expose) subset, invoke_query-gated) — it supersedes
  main's pre-@mcp(expose) text, since this branch adds the per-query expose flag.
- docs/user/operations/server.md: keep our GET /queries description
  (invoke_query gate + @mcp(expose) exposure) over main's read-gated/list-all text.
- docs/dev/index.md: keep both in-flight RFC rows; renumber this branch's tenancy
  RFC 013 -> 014 (rfc-014-tenancy-cells.md) since main now owns RFC-013
  (rfc-013-write-path-latency.md). Title + index link updated; link-check green.
- openapi.json: regenerated from merged source (OMNIGRAPH_UPDATE_OPENAPI=1) — now
  info.version 0.7.1 with our invoke_query/@mcp schema.

Coherence: omnigraph-mcp bumped 0.7.0 -> 0.7.1 to match the workspace; Cargo.lock
updated. cargo build --workspace green; server/mcp/api-types/compiler suites green
(schema_routes.rs reopen-after-apply flakes under parallel IO on a near-full disk,
passes single-threaded — a pre-existing main test, unchanged by the merge).
The tenancy-model RFC (added on this branch by 0f58329/c43b81d, renumbered to
014 in the merge) is independent of the MCP surface this PR ships. Remove the
file and its dev-index row so the PR's net diff is MCP-only. The RFC content is
preserved in git history (recover via `git show 0f58329:docs/dev/rfc-013-tenancy-cells.md`)
and can be cherry-picked onto its own branch to continue separately.

Doc-link check green (62 links, 59 docs).
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).
Comment on lines +74 to +75
} else if public_hosts.is_empty() {
None

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 security Host checks disabled

When the server binds a non-loopback address, serve() currently passes an empty public_hosts list. This branch turns that into allowed_hosts: None, and mcp_router then calls disable_allowed_hosts(). That makes the new public MCP endpoint accept any Host value. The Origin guard does not cover non-browser MCP clients because requests without an Origin are allowed, so a public deployment has no compulsory host allow-list for this new graph read/write surface. Please keep the Host policy default-deny on public binds unless an explicit public host list is configured.

Context Used: AGENTS.md (source)

Fix in Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant