Files
reverse-engineering-anything/src/server/toolResultEncoding.ts
T
Kaoru0822andClaude Opus 5.5 7b51121f7c fix(mcp): measure tool errors against their text-only delivery
Since #1277 an error result reaches the client without structuredContent,
but toErrorToolResult and the transport recovery still budgeted it as
structured JSON plus escaped text. An error whose text fit between about
half and all of the response budget was replaced by resource_constraint,
and without Evidence retention the original diagnostic was lost.

Count only the escaped text for errors. The boundary, process-global and
real Ghidra large-result probes now use diagnostics that exceed the
text-only budget.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 14:03:12 +00:00

67 lines
2.3 KiB
TypeScript

import { constants as bufferConstants } from "node:buffer";
import type { JsonValue } from "../domain/jsonValue.js";
import { bufferedJsonParts, jsonParts } from "../domain/jsonSerialization.js";
export const MCP_RESULT_STRING_LIMIT = bufferConstants.MAX_STRING_LENGTH - 1024;
const RESULT_ENVELOPE_BYTES =
Buffer.byteLength(
JSON.stringify({
content: [{ type: "text", text: "" }],
structuredContent: null,
}),
) - 4;
const TEXT_RESULT_ENVELOPE_BYTES = Buffer.byteLength(
JSON.stringify({ content: [{ type: "text", text: "" }] }),
);
/**
* MCP representations a result carries on the wire. Successful results repeat
* the JSON as structured content and escaped text; error results carry text only.
*/
export type ToolResultRepresentations = "structured-and-text" | "text";
/** Result of encoding the complete repeated MCP representations within a wire budget. */
export type ToolResultEncoding =
| { readonly ok: true; readonly text: string; readonly bytes: number }
| {
readonly ok: false;
readonly bytesAtLeast: number;
readonly codeUnitsAtLeast: number;
readonly constraint: "receive-buffer" | "string-length";
};
/** Account for the delivered JSON representations before allocating the complete text. */
export const encodeToolResult = (
value: JsonValue,
budgetBytes: number,
representations: ToolResultRepresentations = "structured-and-text",
): ToolResultEncoding => {
const structured = representations === "structured-and-text";
const parts: string[] = [];
let bytes = structured ? RESULT_ENVELOPE_BYTES : TEXT_RESULT_ENVELOPE_BYTES;
let codeUnits = bytes;
for (const part of bufferedJsonParts(jsonParts(value))) {
const escapedPart = JSON.stringify(part);
bytes +=
(structured ? Buffer.byteLength(part) : 0) +
Buffer.byteLength(escapedPart) -
2;
codeUnits += (structured ? part.length : 0) + escapedPart.length - 2;
if (bytes > budgetBytes || codeUnits > MCP_RESULT_STRING_LIMIT)
return {
ok: false,
bytesAtLeast: bytes,
codeUnitsAtLeast: codeUnits,
constraint:
codeUnits > MCP_RESULT_STRING_LIMIT
? "string-length"
: "receive-buffer",
};
parts.push(part);
}
return { ok: true, text: parts.join(""), bytes };
};