MCP Tool Catalog via #[onto_mcp_tool] Proc-Macro + Inventory

Accepted

ontoref
ontoref-daemon exposes 33 MCP tools via the rmcp ToolRouter in crates/ontoref-daemon/src/mcp/mod.rs. The authoritative tool implementation lives in each tool struct's `ToolBase`/`AsyncTool` impls (name

Context

ontoref-daemon exposes 33 MCP tools via the rmcp ToolRouter in crates/ontoref-daemon/src/mcp/mod.rs. The authoritative tool implementation lives in each tool struct's `ToolBase`/`AsyncTool` impls (name, description, schema). However, the agent-facing tool catalog returned by `ontoref_help` was a hand-typed JSON literal (~100 lines, mod.rs:1129-1226) duplicating every tool's name, description, and parameter shape. This is the same failure mode ADR-007 documents as the original motivation for `#[onto_api]`: the previous-session bug where `insert_mcp_ctx` listed 15 tools while the router had 27. ADR-007 fixed that drift for the HTTP API surface (inventory-collected `ApiRouteEntry`) but did not extend the pattern to MCP. As of 2026-04-26 the manifest claim 'daemon exposes 33 MCP tools' is also a hand-maintained string in `.ontology/manifest.ncl`. With the rate of MCP tool additions through 2026-Q1 (qa, bookmarks, actions, config, ontology extensions all added in separate sessions), drift was becoming inevitable.

Decision

Every MCP tool struct in ontoref-daemon must carry `#[onto_mcp_tool(name, description, category, params)]`. The proc-macro (in `crates/ontoref-derive`) emits `inventory::submit!(ontoref_ontology::McpToolEntry{...})` at link time and leaves the annotated struct unchanged — the existing `ToolBase` and `AsyncTool` impls are untouched. A new pure function `ontoref_daemon::mcp::catalog()` walks `inventory::iter::<McpToolEntry>()`, sorts by name, and returns `Vec<&'static McpToolEntry>`. `HelpTool::invoke` now serializes `catalog()` instead of holding a hand-typed JSON literal. `McpToolEntry` lives in `ontoref-ontology` next to `ApiRouteEntry` and reuses `ApiParam` for parameter metadata — both are protocol surfaces, the type is generic. `tool_router()`'s compile-time `with_async_tool::<T>()` list is left as-is; the Rust type system requires the type list at compile time and a separate macro architecture would be needed to derive it from inventory (out of scope for this ADR).

Constraints

  • Hard Every MCP tool struct in ontoref-daemon must carry #[onto_mcp_tool(...)]
  • Soft The number of #[onto_mcp_tool] annotations must equal the number of with_async_tool::<T>() calls in tool_router()

Alternatives considered

  • Generate the help JSON from ToolBase::name() + description() directly via reflectionrejected: ToolBase exposes name and description but not parameter metadata in a structured form (input_schema returns a JsonObject blob, not the per-param required/values/note shape that ontoref_help emits). Walking the JSON schema and reverse-engineering the per-param hints is brittle. The inventory entry lets us declare the agent-facing param documentation in a stable, typed shape next to the ToolBase impl.
  • Replace tool_router()'s with_async_tool::<T>() list with macro-driven iteration over inventoryrejected: rmcp's ToolRouter requires the tool type at compile time (generic associated types over each tool's Parameter/Output types). Driving registration from inventory entries — which are runtime values of `&'static McpToolEntry` — would require either a build.rs that emits the registration list or a macro that takes the full type list as input. Either is feasible but is a larger architectural change with no immediate reliability win beyond what the inventory already provides for the help/catalog surface. Deferred.
  • Skip the macro and write an `inventory::submit!` block manually next to each toolrejected: Eliminates the macro infrastructure but loses the validation that #[onto_mcp_tool] applies (key spelling check, param string parsing reuse). Manual blocks also bypass the file!() capture for source-file traceability that ApiRouteEntry already uses.

Related ADRs

ADR-007 · ADR-001

Was this useful? Rate it
Got something to add? Tell me what you think, what you'd suggest, or whether we should keep exploring this topic.
· reads

We use cookies to help this site function, understand service usage, and support marketing efforts. Cookie Policy for more info.