Skip to content

orb(mcp): derive the remote server's tool descriptions and categories from `@loopover/contract #9656

Description

@JSONbored

⚠️ Definition of Done: this issue must be completed in full, in a single PR. Do not split this
work across multiple PRs, and do not defer any Deliverable below to a follow-up issue. A PR that
satisfies only some of the Deliverables, stubs a required test, or leaves a checkbox
partially-done does NOT resolve this issue and will be closed.

Context

#9518's requirement 4 said, verbatim: "Same for the remote's MCP_TOOL_CATEGORIES map — derived,
not hand-listed (its sync test converts to a registry meta-test)."
That did not land. The map is
still at src/mcp/server.ts:1068, still 112 hand-typed entries, still typed
Record<string, McpToolCategory> — an index signature, so MCP_TOOL_CATEGORIES[name] at
src/mcp/server.ts:1217 yields undefined for an unlisted name and the tool ships
_meta: { category: undefined } with no compile error. That is exactly the anti-pattern
packages/loopover-contract/src/tool-definition.ts:6-8 was written to condemn ("metagraphed keeps
its output schemas in a TOOL_OUTPUT_SCHEMAS[name] lookup, where a typo'd key silently drops the
schema and nothing fails"). test/unit/mcp-tool-categories.test.ts pins the map against the
registered tool set but never against the contract's own category field.

The same file also hand-declares a description literal at every register(...) call site, while
the contract carries a description for each of those tools. 35 of them have already diverged.
Some are cosmetic (em dash vs --); many are materially different claims about what the tool does:

tool remote (src/mcp/server.ts) contract
loopover_preflight_pr "Preflight a planned PR for lane correctness, duplicate risk, linked issues, and review burden." "Preflight planned pull-request metadata against the repo's lane, duplicate clusters, linked-issue policy, test evidence..."
loopover_simulate_open_pr_pressure "Simulate how opening another PR affects a repo's review-queue pressure..." "Rank what-if scenarios for easing a repo's open-PR pressure from already-computed queue-health metadata..."
loopover_check_test_evidence "Classify whether a planned change's changed files carry enough test evidence, from path metadata alone..." "Classify how well a change's tests actually cover it, from changed PATHS and test names only..."
loopover_preflight_current_branch "Analyze current-branch metadata supplied by a local MCP wrapper and return PR readiness." "Analyze the current git branch and return PR readiness. Sends metadata only."

This matters because the two texts reach different audiences: the remote server's tools/list serves
the inline literal, while listToolDefinitions() — which feeds buildOpenAIToolSpecs /
buildAnthropicToolSpecs / buildAgentToolsIndex
(packages/loopover-contract/src/agent-specs.ts:25-80) and, per #9526, the .well-known catalogs —
serves the contract's. The stdio server already serves the contract's
(packages/loopover-mcp/bin/loopover-mcp.ts:771), so the same tool name is described two different
ways depending on which LoopOver server you asked.

There is also a third category vocabulary: McpToolCategory (src/mcp/server.ts:1059) and
MCP_TOOL_CATEGORY_IDS (:1063) restate the contract's TOOL_CATEGORIES
(packages/loopover-contract/src/tool-definition.ts:16) with the same seven members in a different
order.

Requirements

  1. src/mcp/server.ts's register wrapper takes description and _meta.category from the tool's
    contract entry. Every inline description: literal in a register(...) config is deleted.
  2. MCP_TOOL_CATEGORIES is deleted. Consumers that import it move to the contract
    (getToolContract(name).category or a projection over listToolDefinitions()).
    test/unit/mcp-tool-categories.test.ts is rewritten to assert the wire _meta.category of every
    registered tool equals its contract category.
  3. McpToolCategory becomes an alias of the contract's ToolCategory; MCP_TOOL_CATEGORY_IDS stays
    (display order is not in the contract) but is asserted by test to be a permutation of
    TOOL_CATEGORIES with no extra and no missing member.
  4. The 35 divergences are resolved by editing the contract entry, not by deleting the better text.
    For each of the 35, the surviving description must be the one that states more about what the tool
    reads and returns; where the remote's literal is the richer one, its text moves into the contract
    so the stdio server and the agent-spec projections gain it too. No tool ends this PR with a
    description shorter or less specific than the longer of its two current texts.
  5. A new exported pure check in scripts/lib/validate-mcp/invariants.ts
    checkAdvertisedDescriptions(expected, listed) — returns a failure per tool whose advertised
    description differs from the projection, and validateSurface in
    test/contract/validate-mcp.test.ts runs it for all three servers.

⚠️ Required pattern: mirror registerStdioTool
(packages/loopover-mcp/bin/loopover-mcp.ts:752-778), which looks the contract up by name, throws
if there is no entry, and fills every advertised field from it. Keeping MCP_TOOL_CATEGORIES as a
"derived" object literal generated at module load, introducing a second name-keyed description map,
or resolving the 35 divergences by silently taking whichever text is shorter, do NOT satisfy this
issue.

Deliverables

  • Zero description: literals remain inside register(...) configs in src/mcp/server.ts
  • MCP_TOOL_CATEGORIES and its export are gone; src/mcp/server.ts:1217 reads the category from
    the contract; test/unit/mcp-tool-categories.test.ts asserts wire _meta.category equals the
    contract category for every registered tool
  • McpToolCategory is ToolCategory re-exported; a test asserts MCP_TOOL_CATEGORY_IDS is a
    permutation of TOOL_CATEGORIES
  • All 35 divergent descriptions reconciled in @loopover/contract, each keeping the more
    specific of the two texts
  • checkAdvertisedDescriptions exported from scripts/lib/validate-mcp/invariants.ts,
    unit-tested for a match, a whitespace-only difference, and a materially different string
  • validateSurface runs it for remote, stdio and miner and npm run validate:mcp is green

All Deliverables above are required in a single PR. A PR that satisfies only some of them — for
example deleting the inline descriptions so the servers agree, while leaving 35 tools described by
the shorter of the two texts — does not resolve this issue.

Test Coverage Requirements

99%+ Codecov patch coverage, branch-counted, applies: src/mcp/server.ts and
packages/loopover-contract/src/** are both in coverage.include (vitest.config.ts:55-100). Both
arms of every conditional you add to the register wrapper (contract found vs missing) need a test,
including a named regression test proving a tool registered without a contract entry fails loudly
rather than advertising _meta: { category: undefined }. scripts/lib/validate-mcp/invariants.ts is
outside coverage.include — cover its new branches in test/unit/validate-mcp-helpers.test.ts
anyway.

Expected Outcome

One description and one category per tool, stored once in @loopover/contract, served identically by
the remote server's tools/list, the stdio server's tools/list, the loopover-mcp tools CLI and
the OpenAI/Anthropic agent-spec projections — and validate:mcp fails the moment any of them drifts.

Links & Resources

Metadata

Metadata

Assignees

No one assigned

    Labels

    gittensor:bugGittensor-scored bug fix — scores a 0.05x multiplier.help wantedExtra attention is needed

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions