Skip to content

The MCP tool bridge never forwards parameters — bridged tools reach MCP clients with no input schema, and the docblock says it does forward one #13271

Closed

Description

@os-project-manager

Found while re-closing the tool.json liveness ledger (refs #13042); unrelated to that card's file surface, so filed rather than fixed.

Measured at 74049254

packages/mcp/src/mcp-server-runtime.ts, registerToolFromDefinition — the method that turns each AIToolDefinition into an MCP tool — calls the SDK like this:

this.mcpServer.registerTool(
  tool.name,
  {
    description: tool.description,
    annotations: {
      destructiveHint: this.isDestructiveTool(tool.name),
      readOnlyHint: this.isReadOnlyTool(tool.name),
      openWorldHint: false,
    },
  },
  async (extra) => { /* reads extra.arguments */ },
);

tool.parameters is never read. git grep -nE "\.(outputSchema|objectName)" -- 'packages/mcp/src/**/*.ts' returns nothing, and the only tool. reads in the file are tool.name (5 sites) and tool.description (1 site).

So a tool bridged by bridgeTools reaches an MCP client with a name and a description and no input schema at all — the client has to guess the argument shape. The handler then reads whatever arrived via extra.arguments without validating it against the schema the definition carries.

Why this reads as a defect rather than a design choice

The docblock immediately above the call says the opposite of what the code does:

Convert JSON Schema parameters to Zod-compatible format for MCP SDK. The MCP SDK registerTool with inputSchema expects a Zod raw shape or AnySchema. Since our tools use JSON Schema, we use the low-level .tool() with a raw callback and pass the JSON Schema as annotations metadata.

The annotations object that is actually passed carries only destructiveHint / readOnlyHint / openWorldHint. Nothing hands the JSON Schema anywhere. The comment describes a workaround that is not implemented, which is why this survived: a reader checking "is parameters handled here?" finds a sentence saying yes.

bridgeTools is live wiring, not dead code — packages/mcp/src/plugin.ts, start(), duck-types aiService.toolRegistry and calls it when an AI service is mounted.

Not a liveness re-grade

tool.parameters stays live: the cloud LLM path reads it (cloud packages/service-ai/src/adapters/vercel-adapter.ts, buildVercelOptions, wraps it with jsonSchema and passes it as the Vercel SDK inputSchema). This card is about the framework MCP surface only, where the same key is dropped. The asymmetry is now recorded in the parameters note in packages/spec/liveness/tool.json.

What a fix would need to decide

  1. Whether the MCP SDK version in use accepts a raw JSON Schema for inputSchema, or needs a JSON-Schema-to-Zod conversion at the boundary.
  2. Whether dropping the schema was ever deliberate (in which case the docblock is the thing to correct, and the drop should be stated as a known limitation of the bridge).

Either way the docblock and the code should stop disagreeing.

Activity

  1. os-project-manager commented on Aug 29, 2026

    @os-project-manager
    CollaboratorAuthor

    Triage (R+33) — first-touch on a bare card ⇒ pm:queue · domain:cli · priority:p1 · type Bug.

    Lands in: packages/mcp/src/mcp-server-runtime.ts (registerToolFromDefinition) ⇒ packages/mcp is domain:cli.

    Type Bug / p1: every tool bridged by bridgeTools reaches an MCP client with a name, a description, and no input schema at all — the client guesses the argument shape, and the handler reads extra.arguments without validating against the schema the definition carries. ⚠️ And it is live wiring, not dead code: plugin.ts's start() duck-types aiService.toolRegistry and calls it whenever an AI service is mounted. ⇒ a published integration surface is shipping unusable tool definitions.

    ⭐ The docblock is why this survived — and it is the sharpest part of the card

    Since our tools use JSON Schema, we use the low-level .tool() with a raw callback and pass the JSON Schema as annotations metadata.

    The annotations object actually passed carries only destructiveHint / readOnlyHint / openWorldHint. Nothing hands the JSON Schema anywhere. ⇒ "a reader checking 'is parameters handled here?' finds a sentence saying yes."

    ⭐ This is the fourth instance this session of one shape: a comment, census, control or coverage table that asserts the very thing it fails to do, so the gap cannot be found by reading (#13230 · #13263 · #13301 · this). ⛔ Recorded, not folded — but it is why the docblock must be corrected in the same change, whichever route is taken.

    ⛔ Scope — establish intent BEFORE plumbing

    The card's two open questions are not equal, and one gates the other:

    1. Was dropping the schema deliberate? If yes ⇒ the fix is the docblock plus a stated known limitation of the bridge, and no code moves. If no ⇒ plumb it.
    2. Only if plumbing: does the MCP SDK version in use accept a raw JSON Schema for inputSchema, or is a JSON-Schema→Zod conversion needed at the boundary?

    ⇒ ⛔ do not start at (2). Answering it first commits to plumbing before anyone has established the drop was accidental.

    ⚠️ Either way the deliverable is the same in one respect — the card's own closing line, ratified as the acceptance criterion: the docblock and the code must stop disagreeing. A PR that fixes the code and leaves the prose, or vice versa, has not closed this.

    ⭐ Correctly NOT a liveness re-grade — ⛔ do not let it become one

    tool.parameters stays live: the cloud LLM path reads it (service-ai/src/adapters/vercel-adapter.ts, buildVercelOptions wraps it with jsonSchema as the Vercel SDK inputSchema). ⇒ this card is about the framework MCP surface only, where the same key is dropped, and the asymmetry is already recorded in packages/spec/liveness/tool.json. ⛔ Nobody may cite this card as evidence that parameters is dead — it is evidence of exactly the opposite, unevenly consumed.

    Zeros are reverse-checked: git grep -nE "\.(outputSchema|objectName)" over packages/mcp/src/** returns nothing, against tool.name (5 sites) and tool.description (1) live in the same file ⇒ real absence.

    Provenance: found while re-closing the tool.json liveness ledger (#13042), correctly filed rather than folded — unrelated file surface. ⛔ Do not fold back into #13042.


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions