* fix(mcp): name response-byte and startup ceilings in recovery Name REA_MCP_MAX_RESPONSE_BYTES, its effective value, the 10485760 byte floor, and process restart when the stdio response budget is hit, and state that the setting does not raise Node's string ceiling. Name fixed MCP startup phase budgets when those deadlines fire, and keep Codex's 30 second startup figure as client guidance. Co-authored-by: morluto <morluto@users.noreply.github.com> * fix(mcp): distinguish string ceilings and keep dossier recovery Name REA's reserved string limit separately from Node's ceiling, restore detail:summary discovery for an oversized complete evidence bundle, and apply the doctor deadline sentence when binary_session itself times out. Co-authored-by: morluto <morluto@users.noreply.github.com> * fix(mcp): keep retained SDK errors inside the text budget Quoted SDK validation diagnostics expand when recovery JSON is escaped into MCP text. Shorten only that preview so the retained-evidence reference still arrives as parseable error content. Co-authored-by: morluto <morluto@users.noreply.github.com> * fix(mcp): keep export advice when a summary exceeds the response budget An oversized get_evidence_bundle summary no longer repeats detail: summary. export_evidence_bundle still writes the retained bundle outside one MCP frame, including when a larger byte budget cannot raise the string ceiling. Co-authored-by: morluto <morluto@users.noreply.github.com> * style(mcp): format the SDK diagnostic preview return oxfmt rejected the fitted-preview ternary in EvidenceMcpServer.ts, which failed Static checks and the CI required gate. Co-authored-by: morluto <morluto@users.noreply.github.com> --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: morluto <morluto@users.noreply.github.com>
275 lines
8.0 KiB
JavaScript
275 lines
8.0 KiB
JavaScript
import { readFileSync } from "node:fs";
|
|
import { performance } from "node:perf_hooks";
|
|
|
|
import { Client } from "@modelcontextprotocol/client";
|
|
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
|
|
|
|
const RSS_SAMPLE_INTERVAL_MS = 10;
|
|
const MAXIMUM_PROFILE_BUFFER_BYTES = 2 * 1_024 * 1_024;
|
|
const MAXIMUM_PROFILE_MODULES = 10_000;
|
|
|
|
/**
|
|
* Measure the first usable MCP catalog through the installed process boundary.
|
|
*/
|
|
export async function measureMcpStartup({
|
|
command,
|
|
args,
|
|
environment,
|
|
policy,
|
|
}) {
|
|
const transport = new StdioClientTransport({
|
|
command,
|
|
args,
|
|
env: environment,
|
|
stderr: "pipe",
|
|
});
|
|
let stderr = "";
|
|
transport.stderr?.on("data", (chunk) => {
|
|
stderr = boundedAppend(stderr, chunk.toString());
|
|
});
|
|
const rss = createLinuxRssSampler(() => transport.pid);
|
|
const client = new Client({
|
|
name: "package-startup-probe",
|
|
version: "1.0.0",
|
|
});
|
|
const startedAt = performance.now();
|
|
rss.start();
|
|
try {
|
|
await withFixedPhaseBudget(
|
|
"MCP initialize phase budget",
|
|
policy.initializeBudgetMs,
|
|
"ms",
|
|
() => client.connect(transport, { timeout: policy.initializeBudgetMs }),
|
|
);
|
|
const initializedAt = performance.now();
|
|
const tools = await withFixedPhaseBudget(
|
|
"MCP tools/list phase budget",
|
|
policy.toolsListBudgetMs,
|
|
"ms",
|
|
() =>
|
|
client.listTools(undefined, {
|
|
timeout: policy.toolsListBudgetMs,
|
|
}),
|
|
);
|
|
const listedAt = performance.now();
|
|
rss.stop();
|
|
const measurement = {
|
|
process_started_to_initialized_ms: elapsed(startedAt, initializedAt),
|
|
initialized_to_tools_list_ms: elapsed(initializedAt, listedAt),
|
|
process_started_to_tools_list_ms: elapsed(startedAt, listedAt),
|
|
listed_tool_count: tools.tools.length,
|
|
rss: rss.result(),
|
|
};
|
|
assertStartupBudgets(measurement, policy);
|
|
return measurement;
|
|
} catch (cause) {
|
|
const detail = cause instanceof Error ? cause.message : String(cause);
|
|
throw new Error(
|
|
`packaged MCP startup probe failed: ${detail}${stderr.length === 0 ? "" : `\n${stderr}`}`,
|
|
{ cause },
|
|
);
|
|
} finally {
|
|
rss.stop();
|
|
await Promise.allSettled([client.close(), transport.close()]);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Profile ESM file loading in a separate diagnostic child.
|
|
*/
|
|
export async function profileMcpModuleLoading({
|
|
command,
|
|
args,
|
|
environment,
|
|
policy,
|
|
packageName,
|
|
}) {
|
|
const profile = createEsmModuleProfileCollector(packageName);
|
|
const transport = new StdioClientTransport({
|
|
command,
|
|
args,
|
|
env: { ...environment, NODE_DEBUG: "esm" },
|
|
stderr: "pipe",
|
|
});
|
|
transport.stderr?.on("data", (chunk) => profile.append(chunk.toString()));
|
|
const client = new Client({
|
|
name: "package-module-profile",
|
|
version: "1.0.0",
|
|
});
|
|
try {
|
|
await withFixedPhaseBudget(
|
|
"MCP doctor startup deadline",
|
|
policy.doctorDeadlineMs,
|
|
"ms",
|
|
() =>
|
|
client.connect(transport, {
|
|
timeout: policy.doctorDeadlineMs,
|
|
}),
|
|
);
|
|
await withFixedPhaseBudget(
|
|
"MCP tools/list phase budget",
|
|
policy.toolsListBudgetMs,
|
|
"ms",
|
|
() =>
|
|
client.listTools(undefined, {
|
|
timeout: policy.toolsListBudgetMs,
|
|
}),
|
|
);
|
|
return profile.result();
|
|
} finally {
|
|
await Promise.allSettled([client.close(), transport.close()]);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Parse Linux procfs VmRSS text into bytes.
|
|
*/
|
|
export const parseLinuxVmRssBytes = (status) => {
|
|
const match = /^VmRSS:\s+(\d+)\s+kB$/mu.exec(status);
|
|
return match === null ? null : Number(match[1]) * 1_024;
|
|
};
|
|
|
|
/**
|
|
* Collect unique file-backed ESM module URLs from Node's diagnostic stream.
|
|
*/
|
|
export const createEsmModuleProfileCollector = (packageName = "rea-agents") => {
|
|
const modules = new Set();
|
|
let buffer = "";
|
|
let debugLines = 0;
|
|
let truncated = false;
|
|
const consume = (line) => {
|
|
if (line.startsWith("ESM ")) debugLines += 1;
|
|
for (const match of line.matchAll(/file:\/\/\/[^\s'",}\])]+/gu)) {
|
|
if (modules.size >= MAXIMUM_PROFILE_MODULES) {
|
|
truncated = true;
|
|
break;
|
|
}
|
|
modules.add(match[0]);
|
|
}
|
|
};
|
|
return {
|
|
append(chunk) {
|
|
buffer += chunk;
|
|
if (Buffer.byteLength(buffer) > MAXIMUM_PROFILE_BUFFER_BYTES) {
|
|
buffer = buffer.slice(-MAXIMUM_PROFILE_BUFFER_BYTES);
|
|
truncated = true;
|
|
}
|
|
const lines = buffer.split("\n");
|
|
buffer = lines.pop() ?? "";
|
|
for (const line of lines) consume(line);
|
|
},
|
|
result() {
|
|
if (buffer.length > 0) consume(buffer);
|
|
const values = [...modules];
|
|
const packageSegment = `/node_modules/${packageName}/`;
|
|
return {
|
|
supported: true,
|
|
method: "node-debug-esm",
|
|
unique_file_modules: values.length,
|
|
unique_package_modules: values.filter((value) =>
|
|
value.includes(packageSegment),
|
|
).length,
|
|
unique_dependency_modules: values.filter(
|
|
(value) =>
|
|
value.includes("/node_modules/") && !value.includes(packageSegment),
|
|
).length,
|
|
debug_lines: debugLines,
|
|
truncated,
|
|
};
|
|
},
|
|
};
|
|
};
|
|
|
|
const createLinuxRssSampler = (pid) => {
|
|
if (process.platform !== "linux")
|
|
return {
|
|
start() {},
|
|
stop() {},
|
|
result: () => ({
|
|
supported: false,
|
|
method: null,
|
|
peak_bytes: null,
|
|
}),
|
|
};
|
|
let peakBytes = null;
|
|
let timer;
|
|
const sample = () => {
|
|
const currentPid = pid();
|
|
if (currentPid === null) return;
|
|
try {
|
|
const current = parseLinuxVmRssBytes(
|
|
readFileSync(`/proc/${String(currentPid)}/status`, "utf8"),
|
|
);
|
|
if (current !== null && (peakBytes === null || current > peakBytes))
|
|
peakBytes = current;
|
|
} catch {
|
|
// The short-lived child may exit between resolving its PID and reading.
|
|
}
|
|
};
|
|
return {
|
|
start() {
|
|
sample();
|
|
timer = setInterval(sample, RSS_SAMPLE_INTERVAL_MS);
|
|
timer.unref();
|
|
},
|
|
stop() {
|
|
if (timer !== undefined) clearInterval(timer);
|
|
sample();
|
|
},
|
|
result: () => ({
|
|
supported: true,
|
|
method: "linux-proc-status-vmrss-sampled",
|
|
peak_bytes: peakBytes,
|
|
}),
|
|
};
|
|
};
|
|
|
|
/** Same sentence as `fixedMcpPhaseBudgetRecovery`; this script cannot import that TypeScript module. */
|
|
export const fixedMcpStartupPhaseBudgetRecovery = (phase, value, unit) =>
|
|
`REA's fixed ${phase} is ${String(value)} ${unit} and this release has no runtime override.`;
|
|
|
|
const phaseTimeout = (cause) => {
|
|
const message = cause instanceof Error ? cause.message : String(cause);
|
|
return (
|
|
message.includes("Request timed out") ||
|
|
message.includes("Maximum total timeout exceeded") ||
|
|
message.includes("timed out")
|
|
);
|
|
};
|
|
|
|
const withFixedPhaseBudget = async (phase, value, unit, operation) => {
|
|
try {
|
|
return await operation();
|
|
} catch (cause) {
|
|
if (!phaseTimeout(cause)) throw cause;
|
|
const detail = cause instanceof Error ? cause.message : String(cause);
|
|
throw new Error(
|
|
`${detail} ${fixedMcpStartupPhaseBudgetRecovery(phase, value, unit)}`,
|
|
{ cause },
|
|
);
|
|
}
|
|
};
|
|
|
|
const assertStartupBudgets = (measurement, policy) => {
|
|
if (measurement.process_started_to_initialized_ms > policy.initializeBudgetMs)
|
|
throw new Error(
|
|
`packaged MCP exceeded the initialize phase budget. ${fixedMcpStartupPhaseBudgetRecovery("MCP initialize phase budget", policy.initializeBudgetMs, "ms")}`,
|
|
);
|
|
if (measurement.initialized_to_tools_list_ms > policy.toolsListBudgetMs)
|
|
throw new Error(
|
|
`packaged MCP exceeded the tools/list phase budget. ${fixedMcpStartupPhaseBudgetRecovery("MCP tools/list phase budget", policy.toolsListBudgetMs, "ms")}`,
|
|
);
|
|
if (
|
|
measurement.process_started_to_tools_list_ms > policy.firstCatalogBudgetMs
|
|
)
|
|
throw new Error(
|
|
`packaged MCP exceeded the first-catalog phase budget. ${fixedMcpStartupPhaseBudgetRecovery("MCP first-catalog phase budget", policy.firstCatalogBudgetMs, "ms")}`,
|
|
);
|
|
};
|
|
|
|
const elapsed = (startedAt, completedAt) =>
|
|
Math.round((completedAt - startedAt) * 100) / 100;
|
|
|
|
const boundedAppend = (current, chunk) =>
|
|
`${current}${chunk}`.slice(-64 * 1_024);
|