⚠️ 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
packages/loopover-contract/src/tool-definition.ts:14 describes category as something the servers —
plural — already publish over the wire:
/** Categories a tool can belong to, mirroring the ids the servers already advertise as
* `_meta.category` and the stdio CLI groups its `tools` output by. */
Exactly one server does. grep -rn "_meta" src packages --include='*.ts' finds the marker at three
places, and only one of them attaches a category:
- Remote,
src/mcp/server.ts:838: _meta: { category: advertised.category },
- Stdio proxy,
packages/loopover-mcp/bin/loopover-mcp.ts:2570:
_meta: { ...(tool._meta ?? {}), transport: "proxied" }, — this preserves whatever the remote sent,
so proxied tools carry a category by inheritance.
- Stdio local,
packages/loopover-mcp/bin/loopover-mcp.ts:913-928: the registerTool config carries
title, description, inputSchema, outputSchema, annotations — and no _meta.
- Miner,
packages/loopover-miner/bin/loopover-miner-mcp.ts:132-141: same five fields, no _meta.
Two consequences, both observable from a client:
- One stdio server's
tools/list is internally inconsistent. In gateway mode the ~110 proxied tools
carry _meta.category and the ~110 locally-registered ones do not, so a client that groups by category
renders half the surface as uncategorised — from a single server, for tools whose categories are
declared side by side in the same registry file.
- The miner server publishes no categories at all, even though every one of its entries declares one
(packages/loopover-contract/src/tools/miner.ts, miner-ops.ts).
src/mcp/server.ts:814 states the intent that is only half-delivered: the category "rides along as MCP
_meta.category, exposed in tools/list for clients (and mirrored by the CLI tools command)". The CLI
half works — STDIO_TOOL_DESCRIPTORS (packages/loopover-mcp/bin/loopover-mcp.ts:819) reads the category
straight from the contract — so only MCP clients see the gap.
Nothing catches it. checkAdvertisedMetadata (scripts/lib/validate-mcp/invariants.ts) compares title,
description, readOnlyHint and destructiveHint, and ListedTool does not even model _meta. That
check exists because "three servers could -- and did -- serve three different descriptions, titles and
annotation postures from one contract entry"; category is the one projected field it was never extended
to cover.
Requirements
registerStdioTool (packages/loopover-mcp/bin/loopover-mcp.ts:897) and registerMinerTool
(packages/loopover-miner/bin/loopover-miner-mcp.ts:126) must advertise
_meta: { category: advertised.category }, taken from the same projectToolDefinition(contract) result
they already use for title/description/annotations — not from a second lookup and not from a
literal.
ListedTool in scripts/lib/validate-mcp/invariants.ts must model _meta, and
checkAdvertisedMetadata must fail when a listed tool's _meta.category is absent or differs from the
registry's projected category, in the same "advertises X, registry says Y" style as the existing
title/description/annotation failures.
- The new check must run on all three surfaces, because
checkAdvertisedMetadata is already called for
each of them from validateSurface (test/contract/validate-mcp.test.ts:164).
- What must NOT change: the stdio proxy's
_meta merge at packages/loopover-mcp/bin/loopover-mcp.ts:2570
must keep setting transport: "proxied" and must keep preserving any other _meta key the remote sent.
- What must NOT change: the CLI
tools command's grouping, which already reads the contract directly, and
the TOOL_CATEGORIES ordering in packages/loopover-contract/src/tool-definition.ts:22.
⚠️ Required pattern: mirror src/mcp/server.ts:831-841, where _meta is spread from the projected
definition alongside title/description/annotations in one place. What does NOT satisfy this issue:
(a) adding _meta at each individual registerStdioTool/registerMinerTool call site instead of in the
shared wrapper — that is the per-call-site restatement the contract package exists to remove; (b)
introducing a new _meta key (e.g. auth or locality) that only one server sends, which recreates the
defect one field over; (c) shipping the registration change without extending checkAdvertisedMetadata,
which leaves the next regression invisible.
Deliverables
All Deliverables above are required in a single PR. A PR that satisfies only some of them — for
example adding _meta to the stdio server but not the miner, or adding it to both without the
checkAdvertisedMetadata extension — does not resolve this issue.
Test Coverage Requirements
This repo enforces 99%+ Codecov patch coverage, branch-counted. vitest.config.ts's
coverage.include covers packages/loopover-mcp/bin/**/*.ts (line 120) and
packages/loopover-miner/bin/**/*.ts (line 100), so those two touched paths are measured and gated.
scripts/lib/validate-mcp/invariants.ts is not listed in coverage.include, so Codecov does not
gate the invariant change — the unit tests for it are still mandatory here, they are simply not what
the patch gate measures. Every branch added to checkAdvertisedMetadata (absent _meta, absent
_meta.category, mismatched _meta.category, agreeing _meta.category) needs a test.
Expected Outcome
Any MCP client asking any of LoopOver's three servers for tools/list gets the same _meta.category the
registry declares, for every tool — instead of categories on the remote's tools and on the stdio server's
proxied half only — and a server that stops sending it fails validate:mcp.
Links & Resources
packages/loopover-contract/src/tool-definition.ts:14 — the claim that the servers advertise _meta.category
src/mcp/server.ts:838 — the only registration that actually does
packages/loopover-mcp/bin/loopover-mcp.ts:913 — registerStdioTool's config, no _meta
packages/loopover-miner/bin/loopover-miner-mcp.ts:132 — registerMinerTool's config, no _meta
packages/loopover-mcp/bin/loopover-mcp.ts:2570 — the proxy's _meta merge, which must keep working
scripts/lib/validate-mcp/invariants.ts — ListedTool and checkAdvertisedMetadata
Context
packages/loopover-contract/src/tool-definition.ts:14describescategoryas something the servers —plural — already publish over the wire:
Exactly one server does.
grep -rn "_meta" src packages --include='*.ts'finds the marker at threeplaces, and only one of them attaches a category:
src/mcp/server.ts:838:_meta: { category: advertised.category },packages/loopover-mcp/bin/loopover-mcp.ts:2570:_meta: { ...(tool._meta ?? {}), transport: "proxied" },— this preserves whatever the remote sent,so proxied tools carry a category by inheritance.
packages/loopover-mcp/bin/loopover-mcp.ts:913-928: theregisterToolconfig carriestitle,description,inputSchema,outputSchema,annotations— and no_meta.packages/loopover-miner/bin/loopover-miner-mcp.ts:132-141: same five fields, no_meta.Two consequences, both observable from a client:
tools/listis internally inconsistent. In gateway mode the ~110 proxied toolscarry
_meta.categoryand the ~110 locally-registered ones do not, so a client that groups by categoryrenders half the surface as uncategorised — from a single server, for tools whose categories are
declared side by side in the same registry file.
(
packages/loopover-contract/src/tools/miner.ts,miner-ops.ts).src/mcp/server.ts:814states the intent that is only half-delivered: the category "rides along as MCP_meta.category, exposed in tools/list for clients (and mirrored by the CLItoolscommand)". The CLIhalf works —
STDIO_TOOL_DESCRIPTORS(packages/loopover-mcp/bin/loopover-mcp.ts:819) reads the categorystraight from the contract — so only MCP clients see the gap.
Nothing catches it.
checkAdvertisedMetadata(scripts/lib/validate-mcp/invariants.ts) comparestitle,description,readOnlyHintanddestructiveHint, andListedTooldoes not even model_meta. Thatcheck exists because "three servers could -- and did -- serve three different descriptions, titles and
annotation postures from one contract entry";
categoryis the one projected field it was never extendedto cover.
Requirements
registerStdioTool(packages/loopover-mcp/bin/loopover-mcp.ts:897) andregisterMinerTool(
packages/loopover-miner/bin/loopover-miner-mcp.ts:126) must advertise_meta: { category: advertised.category }, taken from the sameprojectToolDefinition(contract)resultthey already use for
title/description/annotations— not from a second lookup and not from aliteral.
ListedToolinscripts/lib/validate-mcp/invariants.tsmust model_meta, andcheckAdvertisedMetadatamust fail when a listed tool's_meta.categoryis absent or differs from theregistry's projected
category, in the same "advertises X, registry says Y" style as the existingtitle/description/annotation failures.
checkAdvertisedMetadatais already called foreach of them from
validateSurface(test/contract/validate-mcp.test.ts:164)._metamerge atpackages/loopover-mcp/bin/loopover-mcp.ts:2570must keep setting
transport: "proxied"and must keep preserving any other_metakey the remote sent.toolscommand's grouping, which already reads the contract directly, andthe
TOOL_CATEGORIESordering inpackages/loopover-contract/src/tool-definition.ts:22.Deliverables
registerStdioTooladvertises_meta: { category }fromprojectToolDefinition(contract).registerMinerTooladvertises_meta: { category }from theadvertisedvalue it already computesat
packages/loopover-miner/bin/loopover-miner-mcp.ts:131.checkAdvertisedMetadatainscripts/lib/validate-mcp/invariants.tsreports a failure for a listedtool whose
_meta.categoryis missing, and for one whose_meta.categorydiffers from the registry's.test/unit/validate-mcp-helpers.test.ts(do not create a second file forthese helpers) covering both new branches: a listed tool with no
_metaat all, and one whose_meta.categorydisagrees.tools/listfrom the stdio server and from theminer server carries
_meta.categoryfor every listed tool, matchinggetToolDefinition(name).category.All Deliverables above are required in a single PR. A PR that satisfies only some of them — for
example adding
_metato the stdio server but not the miner, or adding it to both without thecheckAdvertisedMetadataextension — does not resolve this issue.Test Coverage Requirements
This repo enforces 99%+ Codecov patch coverage, branch-counted.
vitest.config.ts'scoverage.includecoverspackages/loopover-mcp/bin/**/*.ts(line 120) andpackages/loopover-miner/bin/**/*.ts(line 100), so those two touched paths are measured and gated.scripts/lib/validate-mcp/invariants.tsis not listed incoverage.include, so Codecov does notgate the invariant change — the unit tests for it are still mandatory here, they are simply not what
the patch gate measures. Every branch added to
checkAdvertisedMetadata(absent_meta, absent_meta.category, mismatched_meta.category, agreeing_meta.category) needs a test.Expected Outcome
Any MCP client asking any of LoopOver's three servers for
tools/listgets the same_meta.categorytheregistry declares, for every tool — instead of categories on the remote's tools and on the stdio server's
proxied half only — and a server that stops sending it fails
validate:mcp.Links & Resources
packages/loopover-contract/src/tool-definition.ts:14— the claim that the servers advertise_meta.categorysrc/mcp/server.ts:838— the only registration that actually doespackages/loopover-mcp/bin/loopover-mcp.ts:913—registerStdioTool's config, no_metapackages/loopover-miner/bin/loopover-miner-mcp.ts:132—registerMinerTool's config, no_metapackages/loopover-mcp/bin/loopover-mcp.ts:2570— the proxy's_metamerge, which must keep workingscripts/lib/validate-mcp/invariants.ts—ListedToolandcheckAdvertisedMetadata