Files
reverse-engineering-anything/src/server/toolResultEncoding.ts
T
Shubham AhujaandClaude Opus 5.5 3e7c630223 fix(mcp): count the error flag in error result budgets (#2848)
Error results reach the wire as text plus `isError: true`, but the
text-only envelope omitted the flag. An error up to 15 bytes above the
result budget passed both the delivery and transport checks and was sent
as the original diagnostic instead of taking the oversized-error recovery
path.

Rename the error representation to "error-text" and size its envelope
with the flag, so ToolResultDelivery and the error transport share the
complete delivered shape. Successful structured results are unchanged.

Fixes #2557

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 23:06:55 +08:00

100 lines
3.4 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 ERROR_RESULT_ENVELOPE_BYTES = Buffer.byteLength(
JSON.stringify({ content: [{ type: "text", text: "" }], isError: true }),
);
/**
* Result metadata marking content that originates from the analysis target.
* Hosts and hooks can read it to treat the result as untrusted data.
*/
export const TARGET_CONTENT_RESULT_META = Object.freeze({
provenance: Object.freeze({
untrusted_content: true,
source: "analysis_target",
}),
});
/** Whether a delivered tool result already carries the untrusted target-content marker. */
export const carriesTargetContentMarker = (result: {
readonly _meta?: unknown;
}): boolean => {
const meta = result._meta;
if (typeof meta !== "object" || meta === null) return false;
const provenance: unknown = Reflect.get(meta, "provenance");
return (
typeof provenance === "object" &&
provenance !== null &&
Reflect.get(provenance, "untrusted_content") === true
);
};
// `,"_meta":{...}` added to the delivered result object.
const TARGET_CONTENT_META_BYTES =
Buffer.byteLength(JSON.stringify({ _meta: TARGET_CONTENT_RESULT_META })) - 1;
/**
* MCP representations a result carries on the wire. Successful results repeat
* the JSON as structured content and escaped text; error results carry text
* only, plus their `isError` flag.
*/
export type ToolResultRepresentations = "structured-and-text" | "error-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",
targetContent = false,
): ToolResultEncoding => {
const structured = representations === "structured-and-text";
const parts: string[] = [];
let bytes =
(structured ? RESULT_ENVELOPE_BYTES : ERROR_RESULT_ENVELOPE_BYTES) +
(targetContent ? TARGET_CONTENT_META_BYTES : 0);
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 };
};