[GH-ISSUE #2557] fix(mcp): error results pass the budget check with 15 bytes uncounted #875

Closed
opened 2026-10-15 18:29:12 +00:00 by bsakaguchi · 0 comments
Owner

Originally created by @coygeek on GitHub (Oct 13, 2026).
Original GitHub issue: https://github.com/morluto/rea/issues/2557

Summary

A tool error whose serialized MCP result exceeds the result budget by up to 15 bytes can pass both delivery checks and reach the client as the original error. The accounting omits the top-level isError: true member. The reproduced case exceeds the internal result allocation by one byte but still fits within the default receive limit because REA reserves 1024 bytes for the protocol envelope.

Steps to reproduce

Use the source baseline under Runtime or environment with its locked dependencies installed. Save the following focused test as src/server/errorBudgetVerification.test.ts. It uses the production error projector and MCP server with the pinned SDK client and an in-memory transport; no analysis provider or target is required.

import { Client, InMemoryTransport } from "@modelcontextprotocol/client";
import { STDIO_DEFAULT_MAX_BUFFER_SIZE } from "@modelcontextprotocol/server";
import { expect, it } from "vitest";
import { z } from "zod";
import { AnalysisInputError } from "../domain/analysisErrorCore.js";
import { EvidenceMcpServer } from "./EvidenceMcpServer.js";
import { ToolResultDelivery } from "./toolResult.js";

it("recovers an error one byte above the result budget", async () => {
  const delivery = new ToolResultDelivery(STDIO_DEFAULT_MAX_BUFFER_SIZE);
  const failure = (padding: number) => delivery.toErrorToolResult(
    new AnalysisInputError("verify_budget", undefined, [{
      path: ["name"], reason: "invalid_value", message: "x".repeat(padding),
    }]), true,
  );
  const { structuredContent: _private, ...emptyWire } = failure(0);
  const emptyBytes = Buffer.byteLength(JSON.stringify(emptyWire));
  const original = failure(delivery.resultBudgetBytes + 1 - emptyBytes);
  const server = new EvidenceMcpServer(
    { name: "budget-verifier", version: "1" },
    { capabilities: {} }, undefined, delivery,
  );
  server.registerTool("verify_error_budget", {
    inputSchema: z.object({}),
  }, async () => original);
  const client = new Client({ name: "budget-verifier-client", version: "1" });
  const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
  try {
    await server.connect(serverTransport);
    await client.connect(clientTransport);
    const result = await client.callTool({ name: "verify_error_budget", arguments: {} });
    const text = result.content[0];
    if (text.type !== "text") throw new Error("missing error text");
    const projection = JSON.parse(text.text);
    expect(result.isError).toBe(true);
    expect(result.structuredContent).toBeUndefined();
    expect(projection.error.code).toBe("resource_constraint");
  } finally {
    await client.close();
    await server.close();
  }
}, 20000);

Run the exact test and remove the temporary file afterward.

npm run test:focused -- src/server/errorBudgetVerification.test.ts

Expected behavior

The budget check should include every member of the delivered error result, including its error flag and any target-content metadata. An error result that exceeds the result allocation should take the existing oversized-error recovery path, which returns a resource_constraint diagnostic instead of delivering the original oversized result.

Actual behavior

The focused MCP reproduction received the complete original invalid_request error without structured content. Its serialized result measured 10,484,737 bytes against a 10,484,736-byte result budget. The encoder counted 10,484,722 bytes and accepted it. The recovery assertion failed because the client received invalid_request where resource_constraint was expected. The result stayed below the 10,485,760-byte receive maximum; this reproduction did not cause a receive-buffer failure.

A separate encoder comparison measured 74 bytes for a simple error that serializes to 89 bytes with its error flag, and 151 versus 166 bytes with target-content metadata. A structured-success control measured 124 bytes both ways.

Evidence

  • Expected source: src/server/toolResultEncoding.ts:49-65 defines accounting of delivered representations before allocation; src/server/toolResult.ts:50-54 establishes the result allocation; docs/mcp-contracts.md:101-107 documents the 1024-byte protocol reserve.
  • Failure source: src/server/toolResultEncoding.ts:16-18 and 74-97 omit the error flag; src/server/toolResult.ts:132-151 supplies it after text accounting; src/server/EvidenceMcpServer.ts:269-283 repeats that accounting, and 234-255 preserves the flag when delivering the error.
  • Evidence provenance: observed
  • Local verification: reproduced
  • Reproduction completeness: complete

The focused run exited 1 with the byte-equality and recovery assertions failing. The production case completed in under two seconds.

The 15-byte difference is the serialized top-level member ,"isError":true. Both production checks share the same omission. The oversized-error diagnostic uses an early-stopping response_bytes_at_least lower bound, so this report does not assert that the diagnostic must equal the complete original error size.

Related report #1261 concerns nonconforming structured error content and is closed. Its fix removes the private structured carrier before delivery. This reproduction exercises that corrected text-only delivery and demonstrates a separate accounting omission.

Restoration check

Repeat the production case with the same default maximum and error input. The client should receive the existing resource_constraint recovery diagnostic for the error that is one byte above the result allocation. Compare accounting with the complete serialized error result, with and without target-content metadata; both counts should include the error flag. An error that fits the allocation should still deliver its complete original diagnostic. The structured-success comparison should remain exact, so restoring error accounting does not inflate successful result measurements or restore structured error content.

Runtime or environment

Failure was verified at a2d8b94dd5 and is still present at 8b181716ac by source recheck, package version 6.4.0, Node.js v26.10.0, npm 11.19.1, and the repository's installed locked dependencies. Verification used the SDK's default 10 MiB maximum, production source imports, and linked in-memory MCP transports. Actual stdio framing and a real provider were not exercised.

Triage assessment

  • Impact level: P3
  • Assessment status: supported
  • Impact basis: The observed omission admits an error one byte over the internal result allocation while preserving its complete diagnostic. The 15-byte discrepancy remains within the existing 1024-byte envelope reserve, and the tested client received the error below its receive maximum. No consequential data loss or connection failure was observed.
  • Workaround status: available
  • Workaround basis: The existing 1024-byte reserve absorbs the demonstrated 15-byte discrepancy without changing client configuration, and the client can consume the complete original error in this reproduced case.
Originally created by @coygeek on GitHub (Oct 13, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/2557 ## Summary A tool error whose serialized MCP result exceeds the result budget by up to 15 bytes can pass both delivery checks and reach the client as the original error. The accounting omits the top-level `isError: true` member. The reproduced case exceeds the internal result allocation by one byte but still fits within the default receive limit because REA reserves 1024 bytes for the protocol envelope. ## Steps to reproduce Use the source baseline under Runtime or environment with its locked dependencies installed. Save the following focused test as `src/server/errorBudgetVerification.test.ts`. It uses the production error projector and MCP server with the pinned SDK client and an in-memory transport; no analysis provider or target is required. ```ts import { Client, InMemoryTransport } from "@modelcontextprotocol/client"; import { STDIO_DEFAULT_MAX_BUFFER_SIZE } from "@modelcontextprotocol/server"; import { expect, it } from "vitest"; import { z } from "zod"; import { AnalysisInputError } from "../domain/analysisErrorCore.js"; import { EvidenceMcpServer } from "./EvidenceMcpServer.js"; import { ToolResultDelivery } from "./toolResult.js"; it("recovers an error one byte above the result budget", async () => { const delivery = new ToolResultDelivery(STDIO_DEFAULT_MAX_BUFFER_SIZE); const failure = (padding: number) => delivery.toErrorToolResult( new AnalysisInputError("verify_budget", undefined, [{ path: ["name"], reason: "invalid_value", message: "x".repeat(padding), }]), true, ); const { structuredContent: _private, ...emptyWire } = failure(0); const emptyBytes = Buffer.byteLength(JSON.stringify(emptyWire)); const original = failure(delivery.resultBudgetBytes + 1 - emptyBytes); const server = new EvidenceMcpServer( { name: "budget-verifier", version: "1" }, { capabilities: {} }, undefined, delivery, ); server.registerTool("verify_error_budget", { inputSchema: z.object({}), }, async () => original); const client = new Client({ name: "budget-verifier-client", version: "1" }); const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); try { await server.connect(serverTransport); await client.connect(clientTransport); const result = await client.callTool({ name: "verify_error_budget", arguments: {} }); const text = result.content[0]; if (text.type !== "text") throw new Error("missing error text"); const projection = JSON.parse(text.text); expect(result.isError).toBe(true); expect(result.structuredContent).toBeUndefined(); expect(projection.error.code).toBe("resource_constraint"); } finally { await client.close(); await server.close(); } }, 20000); ``` Run the exact test and remove the temporary file afterward. ```sh npm run test:focused -- src/server/errorBudgetVerification.test.ts ``` ## Expected behavior The budget check should include every member of the delivered error result, including its error flag and any target-content metadata. An error result that exceeds the result allocation should take the existing oversized-error recovery path, which returns a `resource_constraint` diagnostic instead of delivering the original oversized result. ## Actual behavior The focused MCP reproduction received the complete original `invalid_request` error without structured content. Its serialized result measured 10,484,737 bytes against a 10,484,736-byte result budget. The encoder counted 10,484,722 bytes and accepted it. The recovery assertion failed because the client received `invalid_request` where `resource_constraint` was expected. The result stayed below the 10,485,760-byte receive maximum; this reproduction did not cause a receive-buffer failure. A separate encoder comparison measured 74 bytes for a simple error that serializes to 89 bytes with its error flag, and 151 versus 166 bytes with target-content metadata. A structured-success control measured 124 bytes both ways. ## Evidence - Expected source: `src/server/toolResultEncoding.ts:49-65` defines accounting of delivered representations before allocation; `src/server/toolResult.ts:50-54` establishes the result allocation; `docs/mcp-contracts.md:101-107` documents the 1024-byte protocol reserve. - Failure source: `src/server/toolResultEncoding.ts:16-18` and `74-97` omit the error flag; `src/server/toolResult.ts:132-151` supplies it after text accounting; `src/server/EvidenceMcpServer.ts:269-283` repeats that accounting, and `234-255` preserves the flag when delivering the error. - Evidence provenance: observed - Local verification: reproduced - Reproduction completeness: complete The focused run exited 1 with the byte-equality and recovery assertions failing. The production case completed in under two seconds. The 15-byte difference is the serialized top-level member `,"isError":true`. Both production checks share the same omission. The oversized-error diagnostic uses an early-stopping `response_bytes_at_least` lower bound, so this report does not assert that the diagnostic must equal the complete original error size. Related report [#1261](https://github.com/morluto/rea/issues/1261) concerns nonconforming structured error content and is closed. Its fix removes the private structured carrier before delivery. This reproduction exercises that corrected text-only delivery and demonstrates a separate accounting omission. ## Restoration check Repeat the production case with the same default maximum and error input. The client should receive the existing `resource_constraint` recovery diagnostic for the error that is one byte above the result allocation. Compare accounting with the complete serialized error result, with and without target-content metadata; both counts should include the error flag. An error that fits the allocation should still deliver its complete original diagnostic. The structured-success comparison should remain exact, so restoring error accounting does not inflate successful result measurements or restore structured error content. ## Runtime or environment Failure was verified at a2d8b94dd50dab8d966239dd63ab84718aa8b4b4 and is still present at 8b181716aca2cb467be499cba226abfb5ae19141 by source recheck, package version 6.4.0, Node.js v26.10.0, npm 11.19.1, and the repository's installed locked dependencies. Verification used the SDK's default 10 MiB maximum, production source imports, and linked in-memory MCP transports. Actual stdio framing and a real provider were not exercised. ## Triage assessment - Impact level: P3 - Assessment status: supported - Impact basis: The observed omission admits an error one byte over the internal result allocation while preserving its complete diagnostic. The 15-byte discrepancy remains within the existing 1024-byte envelope reserve, and the tested client received the error below its receive maximum. No consequential data loss or connection failure was observed. - Workaround status: available - Workaround basis: The existing 1024-byte reserve absorbs the demonstrated 15-byte discrepancy without changing client configuration, and the client can consume the complete original error in this reproduced case.
bsakaguchi 2026-10-15 18:29:12 +00:00
  • closed this issue
  • added the
    bug
    label
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
morluto/rea#875
No description provided.