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 onlyserver.test.ts:441references the field, and that test asserts registration acceptance — not SDK enforcement. Safe. - R-3 (downstream consumer): Audit cross-checked —
OutputSchema.safeParseappears only insrc/__tests__/domains/router/tools.test.ts(test code parsing handler returns directly, not via SDK). Tests are unaffected.
Proceeding to implement.