⚠️ 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
- 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.
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.
validateSurface in test/contract/validate-mcp.test.ts runs the new check for all three
servers alongside diffToolSets and checkAdvertisedShape.
- 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
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
Context
@loopover/contractgives every tool atitleand anannotationsposture, andprojectToolDefinitions(packages/loopover-contract/src/tool-definition.ts:129-142) appliesDEFAULT_ANNOTATIONS = { readOnlyHint: true, destructiveHint: false }so the projection everyconsumer 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:93and:161,maintainer.ts:401.None of the three servers advertises that projection:
src/mcp/server.ts:1214-1219): theregisterwrapper forwards{ ...config, _meta }and each
register(...)config only setsdescription/inputSchema/outputSchema. There is nota single tool
annotationsor tooltitlein the file (the ninetitle:hits are input-schemafields and prompt titles). So the server that actually performs the GitHub writes advertises no
destructiveHintat all, and an MCP client that gates confirmation ondestructiveHintseesnothing for
loopover_delete_branch/loopover_close_pr/loopover_file_issue.packages/loopover-mcp/bin/loopover-mcp.ts:774): passes...(contract.annotations ? { annotations: contract.annotations } : {})— the rawPartial<ToolAnnotations>, not the defaulted projection. A tool declaringannotations: { readOnlyHint: false }(maintainer.ts:109,:433,:622,:658,:720)advertises only
readOnlyHint, and a tool with noannotationskey advertises none — bothdisagree with what
listToolDefinitions()publishes for the same tool.packages/loopover-miner/bin/loopover-miner-mcp.ts:219-429): everyregisterToolconfig is
{ description, inputSchema, outputSchema }— notitle, noannotations.Nothing catches it:
diffToolSetscompares name sets only, andcheckAdvertisedShape(
scripts/lib/validate-mcp/invariants.ts:36-45) checks that a description is non-empty and that bothschemas are object-typed. So one contract entry produces three different advertised tools.
Requirements
titleandannotationstaken from the contract's projection(
listToolDefinitions()/projectToolDefinitions), so the defaults are applied exactly once andevery server advertises the same complete
{ readOnlyHint, destructiveHint }pair.scripts/lib/validate-mcp/invariants.tsgains a new exported pure check —checkAdvertisedMetadata(expected: readonly McpToolDefinition[], listed: readonly ListedTool[])—returning one failure string per tool whose advertised
titleorannotationsdiffers from theprojection, and
ListedToolis widened with thetitle/annotationsfields it needs.validateSurfaceintest/contract/validate-mcp.test.tsruns the new check for all threeservers alongside
diffToolSetsandcheckAdvertisedShape.says, not to re-decide any
readOnlyHint/destructiveHintvalue.Deliverables
src/mcp/server.ts'sregisterwrapper resolves the tool's projected definition and passestitleandannotationstobaseRegister, with no per-call-site literalspackages/loopover-mcp/bin/loopover-mcp.ts'sregisterStdioToolpasses the defaultedannotations (so a tool with no
annotationskey advertises{ readOnlyHint: true, destructiveHint: false }) rather thancontract.annotationsrawpackages/loopover-miner/bin/loopover-miner-mcp.tspassestitleandannotationson all 11registerToolconfigscheckAdvertisedMetadataexported fromscripts/lib/validate-mcp/invariants.ts, unit-testedin
test/unit/validate-mcp-helpers.test.tsfor: matching metadata (no failures), a mismatchedtitle, a mismatchedreadOnlyHint, a mismatcheddestructiveHint, and a tool advertising noannotations at all
validateSurfaceintest/contract/validate-mcp.test.tscalls it for remote, stdio and minerloopover_delete_branchis advertised withdestructiveHint: trueby boththe 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 thestdio server's undefaulted
annotationsstays 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/**andtest/**ones:src/mcp/server.ts,packages/loopover-mcp/bin/**andpackages/loopover-miner/bin/**are all listed incoverage.include(vitest.config.ts:55-100) —packages/loopover-mcp/bin/loopover-mcp.tsis 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.tsis outsidecoverage.include, so its five newbranches are not Codecov-gated — cover them anyway in
test/unit/validate-mcp-helpers.test.ts, onecase per failure string.
Expected Outcome
A client calling
tools/listagainst the remote, stdio or miner server gets byte-identicaltitleand
annotationsfor a given tool name, matching whatlistToolDefinitions()publishes; andnpm run validate:mcpfails if any server ever drifts from the projection again.Links & Resources
packages/loopover-contract/src/tool-definition.ts:37-42, 90-91, 129-142src/mcp/server.ts:1212-1219packages/loopover-mcp/bin/loopover-mcp.ts:752-778packages/loopover-miner/bin/loopover-miner-mcp.ts:219-429scripts/lib/validate-mcp/invariants.ts:8-45,test/contract/validate-mcp.test.ts:157-169