File size: 4,358 Bytes
253e783
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
import { isRecord } from "@openclaw/normalization-core/record-coerce";
import { boundedJsonUtf8Bytes, jsonUtf8BytesOrInfinity } from "../infra/json-utf8-bytes.js";
import { truncateUtf8Prefix } from "../utils/utf8-truncate.js";

const MCP_TEXT_CONTENT_MAX_BYTES = 1024 * 1024;
const MCP_TEXT_TRUNCATION_MARKER = "\n[truncated: MCP text content exceeded 1 MB]";

const MCP_INVOKE_PAYLOAD_MAX_BYTES = 20 * 1024 * 1024;
const MCP_PAYLOAD_TRUNCATION_MARKER = "[truncated: MCP result exceeded 20 MB]";

type McpInvokeContentBlock = Record<string, unknown>;

/** Bounds MCP result content before it crosses node.invoke. */
export function boundMcpToolResultPayload(result: {
  content: readonly unknown[];
  structuredContent?: Record<string, unknown>;
  isError?: boolean;
}): {
  content: McpInvokeContentBlock[];
  structuredContent?: Record<string, unknown>;
  isError?: true;
} {
  const payloadMarker = { type: "text" as const, text: MCP_PAYLOAD_TRUNCATION_MARKER };
  const reservedMarkerBytes = jsonUtf8BytesOrInfinity(payloadMarker) + 1;
  const isError = result.isError === true;
  let usedBytes = jsonUtf8BytesOrInfinity({ content: [], ...(isError ? { isError } : {}) });
  let payloadTruncated = false;
  let structuredContent: Record<string, unknown> | undefined;
  if (result.structuredContent) {
    const prefixBytes = Buffer.byteLength(',"structuredContent":');
    const availableBytes = Math.max(
      0,
      MCP_INVOKE_PAYLOAD_MAX_BYTES - usedBytes - prefixBytes - reservedMarkerBytes,
    );
    const measured = boundedJsonUtf8Bytes(result.structuredContent, availableBytes);
    if (measured.complete && measured.bytes <= availableBytes) {
      structuredContent = result.structuredContent;
      usedBytes += prefixBytes + measured.bytes;
    } else {
      payloadTruncated = true;
    }
  }
  const mirroredStructuredContent = structuredContent
    ? JSON.stringify(structuredContent, null, 2)
    : undefined;
  const normalizedBlocks = result.content.filter(
    (block): block is McpInvokeContentBlock =>
      isRecord(block) &&
      (mirroredStructuredContent === undefined ||
        block.type !== "text" ||
        block.text !== mirroredStructuredContent),
  );
  const totalTextBytes = normalizedBlocks.reduce<number>(
    (total, block) =>
      total +
      (block.type === "text" && typeof block.text === "string" ? Buffer.byteLength(block.text) : 0),
    0,
  );
  let remainingTextBytes =
    totalTextBytes > MCP_TEXT_CONTENT_MAX_BYTES
      ? MCP_TEXT_CONTENT_MAX_BYTES - Buffer.byteLength(MCP_TEXT_TRUNCATION_MARKER)
      : MCP_TEXT_CONTENT_MAX_BYTES;
  let markedTruncated = false;
  const textBoundedContent: McpInvokeContentBlock[] = [];
  for (const block of normalizedBlocks) {
    if (block.type !== "text" || typeof block.text !== "string") {
      textBoundedContent.push(block);
      continue;
    }
    if (totalTextBytes <= MCP_TEXT_CONTENT_MAX_BYTES) {
      textBoundedContent.push(block);
      continue;
    }
    if (markedTruncated) {
      continue;
    }
    const text = truncateUtf8Prefix(block.text, remainingTextBytes);
    remainingTextBytes -= Buffer.byteLength(text);
    const blockWasTruncated = text.length < block.text.length;
    if (text || blockWasTruncated) {
      textBoundedContent.push({
        ...block,
        text: blockWasTruncated ? `${text}${MCP_TEXT_TRUNCATION_MARKER}` : text,
      });
    }
    if (blockWasTruncated || remainingTextBytes === 0) {
      if (!blockWasTruncated) {
        textBoundedContent.push({ type: "text", text: MCP_TEXT_TRUNCATION_MARKER.trimStart() });
      }
      markedTruncated = true;
    }
  }
  const content: McpInvokeContentBlock[] = [];
  for (const block of textBoundedContent) {
    const separatorBytes = content.length > 0 ? 1 : 0;
    const availableBytes = Math.max(
      0,
      MCP_INVOKE_PAYLOAD_MAX_BYTES - usedBytes - separatorBytes - reservedMarkerBytes,
    );
    const measured = boundedJsonUtf8Bytes(block, availableBytes);
    if (!measured.complete || measured.bytes > availableBytes) {
      payloadTruncated = true;
      continue;
    }
    content.push(block);
    usedBytes += measured.bytes + separatorBytes;
  }
  if (payloadTruncated) {
    content.push(payloadMarker);
  }
  return {
    content,
    ...(structuredContent ? { structuredContent } : {}),
    ...(isError ? { isError } : {}),
  };
}