Skip to content

The HTTP MCP surface has a SECOND un-merged skill read — buildMcpBridge.listSkills in packages/runtime, the half #8328's fix cannot reach #8726

Description

@qq9340100

Split out of #8328 at review time. Filed by the domain:cli seat, PM session session_01NaS1PAHJcPfAA2acnV53Tn, on the maintainer's direct channel. Unassigned.

The fact

There are two skill reads behind MCP prompts, not one. #8328 was written as though there were one, and its fix (PR #8724) closes only the first:

surface read status
stdio (long-lived server) packages/mcp/src/mcp-server-runtime.tsbridgePrompts fixed by PR #8724
HTTP /api/v1/mcp (built per request) packages/runtime/src/domains/mcp.tsbuildMcpBridge.listSkills not fixed — this card

Measured on origin/main @ 3508678, packages/runtime/src/domains/mcp.ts:400:

listSkills: async () => {
    const meta: any = await getMeta();
    return (await meta?.list?.('skill')) ?? [];
},

It reads IMetadataService.list('skill') directly — the registry/loader listing, one layer below the sys_metadata overlay merge, which lives in the protocol's getMetaItems. It also has no diagnosed wrapper at all, so it does not carry #6504's completeness verdict either: a known-partial skill surface presents as a complete one.

Why this matters more than a second call site usually would

⚠️ #8328's own three-step reproduction runs through this endpoint. The card says "query the skill over MCP prompts", and both that repro and the pnpm dev banner point at the HTTP surface. So with PR #8724 merged and nothing else, the reproduction on #8328 still answers {"prompts":[]} — verified by #8328's implementer against a running server with the fix in place.

That is why PR #8724 is deliberately Part of #8328 rather than Fixes, and why #8328 must not be closed on the strength of it.

The decision is already made — this is application, not adjudication

The maintainer ruling on #8328 (2026-08-13 ~23:50Z, verbatim 「接受你的全部建议」, option 3) already settled the direction: read from the protocol's merged layer rather than pushing the override merge down into MetadataService. That ruling is unchanged and applies here; only its scope estimate — "entirely inside packages/mcp" — turned out to cover half the surface.

⛔ Do not re-open the layer question here. Option 2 (push the merge into MetadataService.list() for every consumer) remains deliberately not chosen, and the contract question is archived unscheduled as #8722.

What a fix has to be careful about

  • Reuse the seam, do not re-derive it. PR fix(mcp): read skill prompts from the protocol's merged metadata listing #8724 exports McpMergedMetadataRead (a duck-typed getMetaItems seam) from packages/mcp. A second hand-rolled shape here is the "one rule, N places" divergence this whole family of cards exists to remove.
  • Per-request construction is the real difference. The stdio bridge is assembled once at plugin start(); this one is built per request by the runtime, from a per-environment metadata service (getMeta()), so a multi-tenant host serves each environment its own skills. The merged read has to be resolved on the same per-environment seam — ⛔ not captured once at boot, which would cross-serve environments.
  • ⛔ No fallback to the un-merged listing when the merged read throws — same rule PR fix(mcp): read skill prompts from the protocol's merged metadata listing #8724 pinned. Falling back answers registry rows in the shape of merged ones, restoring this defect silently at exactly the moment an overlay is most likely to be the thing being missed.
  • IMetadataService.list() presents a known-partial answer as a complete one — the #5840 shape on the plural read #6504's verdict should be added here, not just preserved — unlike the stdio bridge, this read never had a diagnosed wrapper, so this is a gap to close rather than a contract to keep intact. Say so explicitly if you conclude otherwise.
  • The ?? [] on a missing meta is load-bearing for hosts with no metadata service; keep the absent-host path reading as it does today.

Serial

Sequence after PR #8724 merges — it exports the seam this should consume, and landing both against one un-merged baseline is how two individually-green changes end up jointly wrong. Different package (packages/runtime vs packages/mcp), so no file conflict, but the dependency is real.

#8328 stays open and carries Blocked-by: this card; it closes when this half lands and its own reproduction finally reflects the override.

Source

Found during PM review of PR #8724 by its implementer, who measured it rather than inferring it and correctly declined to fix it outside the declared file surface. packages/runtime is domain:cli, the same lane as #8328.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions