Skip to content

mcp: advertise the contract's defaulted annotations and title on all three servers #9655

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

@loopover/contract gives every tool a title and an annotations posture, and
projectToolDefinitions (packages/loopover-contract/src/tool-definition.ts:129-142) applies
DEFAULT_ANNOTATIONS = { readOnlyHint: true, destructiveHint: false } so the projection every
consumer reads always carries a complete, defaulted pair. 24 tools declare a non-default posture —
e.g. packages/loopover-contract/src/tools/agent.ts:645
({ readOnlyHint: false, destructiveHint: true }, loopover_delete_branch),
admin-config.ts:93 and :161, maintainer.ts:401.

None of the three servers advertises that projection:

  • Remote (src/mcp/server.ts:1214-1219): the register wrapper forwards { ...config, _meta }
    and each register(...) config only sets description/inputSchema/outputSchema. There is not
    a single tool annotations or tool title in the file (the nine title: hits are input-schema
    fields and prompt titles). So the server that actually performs the GitHub writes advertises no
    destructiveHint at all, and an MCP client that gates confirmation on destructiveHint sees
    nothing for loopover_delete_branch / loopover_close_pr / loopover_file_issue.
  • Stdio (packages/loopover-mcp/bin/loopover-mcp.ts:774): passes
    ...(contract.annotations ? { annotations: contract.annotations } : {}) — the raw
    Partial<ToolAnnotations>, not the defaulted projection. A tool declaring
    annotations: { readOnlyHint: false } (maintainer.ts:109, :433, :622, :658, :720)
    advertises only readOnlyHint, and a tool with no annotations key advertises none — both
    disagree with what listToolDefinitions() publishes for the same tool.
  • Miner (packages/loopover-miner/bin/loopover-miner-mcp.ts:219-429): every registerTool
    config is { description, inputSchema, outputSchema } — no title, no annotations.

Nothing catches it: diffToolSets compares name sets only, and checkAdvertisedShape
(scripts/lib/validate-mcp/invariants.ts:36-45) checks that a description is non-empty and that both
schemas are object-typed. So one contract entry produces three different advertised tools.

Requirements

  1. All three servers advertise title and annotations taken from the contract's projection
    (listToolDefinitions() / projectToolDefinitions), so the defaults are applied exactly once and
    every server advertises the same complete { readOnlyHint, destructiveHint } pair.
  2. scripts/lib/validate-mcp/invariants.ts gains a new exported pure check —
    checkAdvertisedMetadata(expected: readonly McpToolDefinition[], listed: readonly ListedTool[])
    returning one failure string per tool whose advertised title or annotations differs from the
    projection, and ListedTool is widened with the title/annotations fields it needs.
  3. validateSurface in test/contract/validate-mcp.test.ts runs the new check for all three
    servers alongside diffToolSets and checkAdvertisedShape.
  4. No tool's declared posture changes in this PR: the fix is to advertise what the contract already
    says, not to re-decide any readOnlyHint/destructiveHint value.

⚠️ Required pattern: mirror registerStdioTool in
packages/loopover-mcp/bin/loopover-mcp.ts:752-778 — a single registration helper per server that
looks the tool up in the contract and fills every advertised field from it. Adding
annotations/title literals inline at individual register(...) call sites in
src/mcp/server.ts, or building a second name-keyed annotations/title lookup map next to
MCP_TOOL_CATEGORIES, does NOT satisfy this issue.

Deliverables

  • src/mcp/server.ts's register wrapper resolves the tool's projected definition and passes
    title and annotations to baseRegister, with no per-call-site literals
  • packages/loopover-mcp/bin/loopover-mcp.ts's registerStdioTool passes the defaulted
    annotations (so a tool with no annotations key advertises
    { readOnlyHint: true, destructiveHint: false }) rather than contract.annotations raw
  • packages/loopover-miner/bin/loopover-miner-mcp.ts passes title and annotations on all 11
    registerTool configs
  • checkAdvertisedMetadata exported from scripts/lib/validate-mcp/invariants.ts, unit-tested
    in test/unit/validate-mcp-helpers.test.ts for: matching metadata (no failures), a mismatched
    title, a mismatched readOnlyHint, a mismatched destructiveHint, and a tool advertising no
    annotations at all
  • validateSurface in test/contract/validate-mcp.test.ts calls it for remote, stdio and miner
  • A test asserting loopover_delete_branch is advertised with destructiveHint: true by both
    the remote and the stdio server

All Deliverables above are required in a single PR. A PR that satisfies only some of them — for
example fixing the remote server's registration without adding checkAdvertisedMetadata, so the
stdio server's undefaulted annotations stays unnoticed — does not resolve this issue.

Test Coverage Requirements

99%+ Codecov patch coverage, branch-counted, applies to every file you touch here except the
scripts/** and test/** ones: src/mcp/server.ts, packages/loopover-mcp/bin/** and
packages/loopover-miner/bin/** are all listed in coverage.include (vitest.config.ts:55-100) —
packages/loopover-mcp/bin/loopover-mcp.ts is called out there explicitly as deliberately graded.
Both arms of every conditional you add (annotations present vs absent; title present vs absent) need
a test. scripts/lib/validate-mcp/invariants.ts is outside coverage.include, so its five new
branches are not Codecov-gated — cover them anyway in test/unit/validate-mcp-helpers.test.ts, one
case per failure string.

Expected Outcome

A client calling tools/list against the remote, stdio or miner server gets byte-identical title
and annotations for a given tool name, matching what listToolDefinitions() publishes; and
npm run validate:mcp fails if any server ever drifts from the projection again.

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