Packet — R93 B1 outputSchema SDK Passthrough Envelope Mismatch

Round: R93 debug-sweep Branch: feature/r93-b1-output-schema-envelope Audit: docs/audits/r93-b1-output-schema-envelope-audit.md Contract: docs/contracts/r93-b1-output-schema-envelope-contract.md β task: 6b2da36a-8f1d-4a90-95c1-ac549b3fd60e

§1. Edit list

# File Operation Range / Anchor Description
1 src/server.ts Edit lines 396-412 (the ctx.server.registerTool call inside registerColibriTool) Remove the ...(toolConfig.outputSchema !== undefined ? { outputSchema: toolConfig.outputSchema.shape } : {}) spread. Replace with an explanatory comment that cites the audit.
2 src/__tests__/server.test.ts Edit After the last test in describe('5-stage middleware (InMemoryTransport end-to-end)') (around line 870, before the next describe) Add one new it('declared outputSchema does not trigger SDK -32602', …) test that registers a tool with both inputSchema and outputSchema, drives it through Client.callTool, asserts response.structuredContent.ok === true + payload matches handler return. Test must fail on pre-fix tree (verified locally before commit).

No other files touched. No new files. No migration. No new dependency.

§2. Exact patch for src/server.ts

Before (lines 396-412):

  ctx.server.registerTool(
    name,
    {
      title: toolConfig.title ?? name,
      ...(toolConfig.description !== undefined
        ? { description: toolConfig.description }
        : {}),
      inputSchema: toolConfig.inputSchema.shape,
      ...(toolConfig.outputSchema !== undefined
        ? { outputSchema: toolConfig.outputSchema.shape }
        : {}),
    },
    // The SDK's `registerTool` generics do not perfectly round-trip our
    // wrapped handler's return shape (R-2 in contract §13). We consciously
    // accept an unsafe cast at this one site — lint flags the single line.
    wrappedHandler as never,
  );

After:

  // R93 B1 (2026-05-13) — do NOT forward toolConfig.outputSchema to the SDK.
  // The α middleware wraps every handler result in `{ok, data}` (this file
  // lines 357-361), but the SDK validates structuredContent against the
  // *unwrapped* outputSchema → every outputSchema-declared tool hit MCP
  // -32602. Keeping outputSchema on ColibriToolConfig is intentional: the
  // 8 affected tools (3 δ + 5 θ) still reference the Zod schemas for
  // `z.infer<typeof X>` typing in their `*.ts` modules. See
  // docs/audits/r93-b1-output-schema-envelope-audit.md.
  ctx.server.registerTool(
    name,
    {
      title: toolConfig.title ?? name,
      ...(toolConfig.description !== undefined
        ? { description: toolConfig.description }
        : {}),
      inputSchema: toolConfig.inputSchema.shape,
    },
    // The SDK's `registerTool` generics do not perfectly round-trip our
    // wrapped handler's return shape (R-2 in contract §13). We consciously
    // accept an unsafe cast at this one site — lint flags the single line.
    wrappedHandler as never,
  );

§3. Regression test patch — src/__tests__/server.test.ts

Insert before the closing brace of describe('5-stage middleware (InMemoryTransport end-to-end)') (locate via grep -n "describe('5-stage middleware" and add before that describe’s final });):

  it('outputSchema-declared tool round-trips through the SDK without -32602', async () => {
    // R93 B1 regression — pre-fix this call rejects with MCP -32602 Output
    // validation error because middleware wraps in {ok,data} but the SDK
    // saw the unwrapped outputSchema. The fix drops the SDK passthrough
    // (the Zod schema stays on ColibriToolConfig for type inference).
    const { client, cleanup } = await makeLinkedPair({
      register: (ctx) => {
        registerColibriTool(
          ctx,
          'with_output_schema',
          {
            title: 'with_output_schema',
            description: 'tool whose outputSchema would have triggered -32602',
            inputSchema: z.object({ n: z.number().int() }),
            outputSchema: z.object({ doubled: z.number().int() }),
          },
          ({ n }) => ({ doubled: n * 2 }),
        );
      },
    });
    try {
      const response = await client.callTool({
        name: 'with_output_schema',
        arguments: { n: 7 },
      });
      expect(response.structuredContent).toEqual({
        ok: true,
        data: { doubled: 14 },
      });
      expect(response.isError).toBeFalsy();
    } finally {
      await cleanup();
    }
  });

makeLinkedPair (defined at src/__tests__/server.test.ts:115-177) accepts the register callback for pre-connect tool registration. The new test uses n: 7 → doubled: 14 so the assertion is deterministic and self-evident.

§4. Build/lint/test gates

Per CLAUDE.md §5: run npm run build && npm run lint && npm test from the worktree. All three must exit 0.

Expected test-count delta: +1 (the new regression). Suite count unchanged at 79.

§5. Commit message template

fix(r93-b1): drop outputSchema SDK passthrough in registerColibriTool

The α middleware wraps every handler result in {ok, data} (s17 §6 envelope)
but registerColibriTool was forwarding toolConfig.outputSchema.shape to the
MCP SDK at server.ts:404-406. The SDK validated structuredContent against
the unwrapped schema and emitted -32602 on every call. All 8 tools with
declared outputSchema were unreachable through the MCP wire:

  - router_score, router_fallback, router_stats (δ Phase 1.5)
  - consensus_propose, consensus_vote, consensus_finality,
    consensus_gossip, vrf_eval (θ Phase 3)

This patch removes the outputSchema field from the SDK registerTool config.
The Zod *OutputSchema exports stay in router/tools.ts + consensus/tools.ts
for TypeScript type inference (z.infer<typeof X>) and direct test parsing;
they are no longer enforced at the SDK boundary.

Regression test (server.test.ts InMemoryTransport block) registers a tool
with declared outputSchema and verifies the {ok:true, data:...} envelope
round-trips through Client.callTool without -32602.

Closes R93 B1.

Live audit: docs/audits/r93-b1-output-schema-envelope-audit.md
Contract:   docs/contracts/r93-b1-output-schema-envelope-contract.md
Packet:     docs/packets/r93-b1-output-schema-envelope-packet.md

§6. Risk mitigations (from contract §5)

  • R-1 (test depends on enforcement): rg outputSchema src/__tests__ confirmed only server.test.ts:441 references the field, and that test asserts registration acceptance — not SDK enforcement. Safe.
  • R-3 (downstream consumer): Audit cross-checked — OutputSchema.safeParse appears only in src/__tests__/domains/router/tools.test.ts (test code parsing handler returns directly, not via SDK). Tests are unaffected.

Proceeding to implement.


Back to top

Colibri — documentation-first MCP runtime. Apache 2.0 + Commons Clause.

This site uses Just the Docs, a documentation theme for Jekyll.