fix(mcp): guide agents through tool error recovery (#5470)

Add shared, typed input-contract and timeout-state recovery guidance. Normalize SDK input failures within the actual MCP wire budget, preserve full diagnostics and protocol metadata, and retain original Evidence when normalized delivery would exceed the budget.

Fixes #5431.
This commit is contained in:
gensokyo-dev authored and GitHub committed 2026-10-20 21:45:05 +08:00
1 parent c374c2fe72
commit e50a271c72
20 files changed
+1010 -69

No files matched your search

+540
View File
@@ -28,6 +28,33 @@
"required": ["action"],
"additionalProperties": false
},
"recovery": {
"oneOf": [
{
"type": "object",
"properties": {
"kind": { "type": "string", "const": "inspect_input_contract" },
"operation": { "type": "string", "minLength": 1 }
},
"required": ["kind", "operation"],
"additionalProperties": false
},
{
"type": "object",
"properties": {
"kind": { "type": "string", "const": "inspect_timeout_state" },
"operation": { "type": "string", "minLength": 1 },
"timeout_ms": {
"type": "integer",
"exclusiveMinimum": 0,
"maximum": 9007199254740991
}
},
"required": ["kind", "operation", "timeout_ms"],
"additionalProperties": false
}
]
},
"details": {
"type": "object",
"propertyNames": { "type": "string" },
@@ -64,6 +91,33 @@
"required": ["action"],
"additionalProperties": false
},
"recovery": {
"oneOf": [
{
"type": "object",
"properties": {
"kind": { "type": "string", "const": "inspect_input_contract" },
"operation": { "type": "string", "minLength": 1 }
},
"required": ["kind", "operation"],
"additionalProperties": false
},
{
"type": "object",
"properties": {
"kind": { "type": "string", "const": "inspect_timeout_state" },
"operation": { "type": "string", "minLength": 1 },
"timeout_ms": {
"type": "integer",
"exclusiveMinimum": 0,
"maximum": 9007199254740991
}
},
"required": ["kind", "operation", "timeout_ms"],
"additionalProperties": false
}
]
},
"details": {
"type": "object",
"propertyNames": { "type": "string" },
@@ -100,6 +154,33 @@
"required": ["action"],
"additionalProperties": false
},
"recovery": {
"oneOf": [
{
"type": "object",
"properties": {
"kind": { "type": "string", "const": "inspect_input_contract" },
"operation": { "type": "string", "minLength": 1 }
},
"required": ["kind", "operation"],
"additionalProperties": false
},
{
"type": "object",
"properties": {
"kind": { "type": "string", "const": "inspect_timeout_state" },
"operation": { "type": "string", "minLength": 1 },
"timeout_ms": {
"type": "integer",
"exclusiveMinimum": 0,
"maximum": 9007199254740991
}
},
"required": ["kind", "operation", "timeout_ms"],
"additionalProperties": false
}
]
},
"details": {
"type": "object",
"propertyNames": { "type": "string" },
@@ -136,6 +217,33 @@
"required": ["action"],
"additionalProperties": false
},
"recovery": {
"oneOf": [
{
"type": "object",
"properties": {
"kind": { "type": "string", "const": "inspect_input_contract" },
"operation": { "type": "string", "minLength": 1 }
},
"required": ["kind", "operation"],
"additionalProperties": false
},
{
"type": "object",
"properties": {
"kind": { "type": "string", "const": "inspect_timeout_state" },
"operation": { "type": "string", "minLength": 1 },
"timeout_ms": {
"type": "integer",
"exclusiveMinimum": 0,
"maximum": 9007199254740991
}
},
"required": ["kind", "operation", "timeout_ms"],
"additionalProperties": false
}
]
},
"details": {
"type": "object",
"propertyNames": { "type": "string" },
@@ -172,6 +280,33 @@
"required": ["action"],
"additionalProperties": false
},
"recovery": {
"oneOf": [
{
"type": "object",
"properties": {
"kind": { "type": "string", "const": "inspect_input_contract" },
"operation": { "type": "string", "minLength": 1 }
},
"required": ["kind", "operation"],
"additionalProperties": false
},
{
"type": "object",
"properties": {
"kind": { "type": "string", "const": "inspect_timeout_state" },
"operation": { "type": "string", "minLength": 1 },
"timeout_ms": {
"type": "integer",
"exclusiveMinimum": 0,
"maximum": 9007199254740991
}
},
"required": ["kind", "operation", "timeout_ms"],
"additionalProperties": false
}
]
},
"details": {
"type": "object",
"propertyNames": { "type": "string" },
@@ -208,6 +343,33 @@
"required": ["action"],
"additionalProperties": false
},
"recovery": {
"oneOf": [
{
"type": "object",
"properties": {
"kind": { "type": "string", "const": "inspect_input_contract" },
"operation": { "type": "string", "minLength": 1 }
},
"required": ["kind", "operation"],
"additionalProperties": false
},
{
"type": "object",
"properties": {
"kind": { "type": "string", "const": "inspect_timeout_state" },
"operation": { "type": "string", "minLength": 1 },
"timeout_ms": {
"type": "integer",
"exclusiveMinimum": 0,
"maximum": 9007199254740991
}
},
"required": ["kind", "operation", "timeout_ms"],
"additionalProperties": false
}
]
},
"details": {
"type": "object",
"propertyNames": { "type": "string" },
@@ -244,6 +406,33 @@
"required": ["action"],
"additionalProperties": false
},
"recovery": {
"oneOf": [
{
"type": "object",
"properties": {
"kind": { "type": "string", "const": "inspect_input_contract" },
"operation": { "type": "string", "minLength": 1 }
},
"required": ["kind", "operation"],
"additionalProperties": false
},
{
"type": "object",
"properties": {
"kind": { "type": "string", "const": "inspect_timeout_state" },
"operation": { "type": "string", "minLength": 1 },
"timeout_ms": {
"type": "integer",
"exclusiveMinimum": 0,
"maximum": 9007199254740991
}
},
"required": ["kind", "operation", "timeout_ms"],
"additionalProperties": false
}
]
},
"details": {
"type": "object",
"propertyNames": { "type": "string" },
@@ -280,6 +469,33 @@
"required": ["action"],
"additionalProperties": false
},
"recovery": {
"oneOf": [
{
"type": "object",
"properties": {
"kind": { "type": "string", "const": "inspect_input_contract" },
"operation": { "type": "string", "minLength": 1 }
},
"required": ["kind", "operation"],
"additionalProperties": false
},
{
"type": "object",
"properties": {
"kind": { "type": "string", "const": "inspect_timeout_state" },
"operation": { "type": "string", "minLength": 1 },
"timeout_ms": {
"type": "integer",
"exclusiveMinimum": 0,
"maximum": 9007199254740991
}
},
"required": ["kind", "operation", "timeout_ms"],
"additionalProperties": false
}
]
},
"details": {
"type": "object",
"propertyNames": { "type": "string" },
@@ -316,6 +532,33 @@
"required": ["action"],
"additionalProperties": false
},
"recovery": {
"oneOf": [
{
"type": "object",
"properties": {
"kind": { "type": "string", "const": "inspect_input_contract" },
"operation": { "type": "string", "minLength": 1 }
},
"required": ["kind", "operation"],
"additionalProperties": false
},
{
"type": "object",
"properties": {
"kind": { "type": "string", "const": "inspect_timeout_state" },
"operation": { "type": "string", "minLength": 1 },
"timeout_ms": {
"type": "integer",
"exclusiveMinimum": 0,
"maximum": 9007199254740991
}
},
"required": ["kind", "operation", "timeout_ms"],
"additionalProperties": false
}
]
},
"details": {
"type": "object",
"propertyNames": { "type": "string" },
@@ -352,6 +595,33 @@
"required": ["action"],
"additionalProperties": false
},
"recovery": {
"oneOf": [
{
"type": "object",
"properties": {
"kind": { "type": "string", "const": "inspect_input_contract" },
"operation": { "type": "string", "minLength": 1 }
},
"required": ["kind", "operation"],
"additionalProperties": false
},
{
"type": "object",
"properties": {
"kind": { "type": "string", "const": "inspect_timeout_state" },
"operation": { "type": "string", "minLength": 1 },
"timeout_ms": {
"type": "integer",
"exclusiveMinimum": 0,
"maximum": 9007199254740991
}
},
"required": ["kind", "operation", "timeout_ms"],
"additionalProperties": false
}
]
},
"details": {
"type": "object",
"propertyNames": { "type": "string" },
@@ -388,6 +658,33 @@
"required": ["action"],
"additionalProperties": false
},
"recovery": {
"oneOf": [
{
"type": "object",
"properties": {
"kind": { "type": "string", "const": "inspect_input_contract" },
"operation": { "type": "string", "minLength": 1 }
},
"required": ["kind", "operation"],
"additionalProperties": false
},
{
"type": "object",
"properties": {
"kind": { "type": "string", "const": "inspect_timeout_state" },
"operation": { "type": "string", "minLength": 1 },
"timeout_ms": {
"type": "integer",
"exclusiveMinimum": 0,
"maximum": 9007199254740991
}
},
"required": ["kind", "operation", "timeout_ms"],
"additionalProperties": false
}
]
},
"details": {
"anyOf": [
{
@@ -476,6 +773,33 @@
"required": ["action"],
"additionalProperties": false
},
"recovery": {
"oneOf": [
{
"type": "object",
"properties": {
"kind": { "type": "string", "const": "inspect_input_contract" },
"operation": { "type": "string", "minLength": 1 }
},
"required": ["kind", "operation"],
"additionalProperties": false
},
{
"type": "object",
"properties": {
"kind": { "type": "string", "const": "inspect_timeout_state" },
"operation": { "type": "string", "minLength": 1 },
"timeout_ms": {
"type": "integer",
"exclusiveMinimum": 0,
"maximum": 9007199254740991
}
},
"required": ["kind", "operation", "timeout_ms"],
"additionalProperties": false
}
]
},
"details": {
"type": "object",
"propertyNames": { "type": "string" },
@@ -512,6 +836,33 @@
"required": ["action"],
"additionalProperties": false
},
"recovery": {
"oneOf": [
{
"type": "object",
"properties": {
"kind": { "type": "string", "const": "inspect_input_contract" },
"operation": { "type": "string", "minLength": 1 }
},
"required": ["kind", "operation"],
"additionalProperties": false
},
{
"type": "object",
"properties": {
"kind": { "type": "string", "const": "inspect_timeout_state" },
"operation": { "type": "string", "minLength": 1 },
"timeout_ms": {
"type": "integer",
"exclusiveMinimum": 0,
"maximum": 9007199254740991
}
},
"required": ["kind", "operation", "timeout_ms"],
"additionalProperties": false
}
]
},
"details": {
"type": "object",
"propertyNames": { "type": "string" },
@@ -548,6 +899,33 @@
"required": ["action"],
"additionalProperties": false
},
"recovery": {
"oneOf": [
{
"type": "object",
"properties": {
"kind": { "type": "string", "const": "inspect_input_contract" },
"operation": { "type": "string", "minLength": 1 }
},
"required": ["kind", "operation"],
"additionalProperties": false
},
{
"type": "object",
"properties": {
"kind": { "type": "string", "const": "inspect_timeout_state" },
"operation": { "type": "string", "minLength": 1 },
"timeout_ms": {
"type": "integer",
"exclusiveMinimum": 0,
"maximum": 9007199254740991
}
},
"required": ["kind", "operation", "timeout_ms"],
"additionalProperties": false
}
]
},
"details": {
"type": "object",
"propertyNames": { "type": "string" },
@@ -584,6 +962,33 @@
"required": ["action"],
"additionalProperties": false
},
"recovery": {
"oneOf": [
{
"type": "object",
"properties": {
"kind": { "type": "string", "const": "inspect_input_contract" },
"operation": { "type": "string", "minLength": 1 }
},
"required": ["kind", "operation"],
"additionalProperties": false
},
{
"type": "object",
"properties": {
"kind": { "type": "string", "const": "inspect_timeout_state" },
"operation": { "type": "string", "minLength": 1 },
"timeout_ms": {
"type": "integer",
"exclusiveMinimum": 0,
"maximum": 9007199254740991
}
},
"required": ["kind", "operation", "timeout_ms"],
"additionalProperties": false
}
]
},
"details": {
"type": "object",
"propertyNames": { "type": "string" },
@@ -620,6 +1025,33 @@
"required": ["action"],
"additionalProperties": false
},
"recovery": {
"oneOf": [
{
"type": "object",
"properties": {
"kind": { "type": "string", "const": "inspect_input_contract" },
"operation": { "type": "string", "minLength": 1 }
},
"required": ["kind", "operation"],
"additionalProperties": false
},
{
"type": "object",
"properties": {
"kind": { "type": "string", "const": "inspect_timeout_state" },
"operation": { "type": "string", "minLength": 1 },
"timeout_ms": {
"type": "integer",
"exclusiveMinimum": 0,
"maximum": 9007199254740991
}
},
"required": ["kind", "operation", "timeout_ms"],
"additionalProperties": false
}
]
},
"details": {
"type": "object",
"propertyNames": { "type": "string" },
@@ -656,6 +1088,33 @@
"required": ["action"],
"additionalProperties": false
},
"recovery": {
"oneOf": [
{
"type": "object",
"properties": {
"kind": { "type": "string", "const": "inspect_input_contract" },
"operation": { "type": "string", "minLength": 1 }
},
"required": ["kind", "operation"],
"additionalProperties": false
},
{
"type": "object",
"properties": {
"kind": { "type": "string", "const": "inspect_timeout_state" },
"operation": { "type": "string", "minLength": 1 },
"timeout_ms": {
"type": "integer",
"exclusiveMinimum": 0,
"maximum": 9007199254740991
}
},
"required": ["kind", "operation", "timeout_ms"],
"additionalProperties": false
}
]
},
"details": {
"type": "object",
"propertyNames": { "type": "string" },
@@ -692,6 +1151,33 @@
"required": ["action"],
"additionalProperties": false
},
"recovery": {
"oneOf": [
{
"type": "object",
"properties": {
"kind": { "type": "string", "const": "inspect_input_contract" },
"operation": { "type": "string", "minLength": 1 }
},
"required": ["kind", "operation"],
"additionalProperties": false
},
{
"type": "object",
"properties": {
"kind": { "type": "string", "const": "inspect_timeout_state" },
"operation": { "type": "string", "minLength": 1 },
"timeout_ms": {
"type": "integer",
"exclusiveMinimum": 0,
"maximum": 9007199254740991
}
},
"required": ["kind", "operation", "timeout_ms"],
"additionalProperties": false
}
]
},
"details": {
"type": "object",
"propertyNames": { "type": "string" },
@@ -728,6 +1214,33 @@
"required": ["action"],
"additionalProperties": false
},
"recovery": {
"oneOf": [
{
"type": "object",
"properties": {
"kind": { "type": "string", "const": "inspect_input_contract" },
"operation": { "type": "string", "minLength": 1 }
},
"required": ["kind", "operation"],
"additionalProperties": false
},
{
"type": "object",
"properties": {
"kind": { "type": "string", "const": "inspect_timeout_state" },
"operation": { "type": "string", "minLength": 1 },
"timeout_ms": {
"type": "integer",
"exclusiveMinimum": 0,
"maximum": 9007199254740991
}
},
"required": ["kind", "operation", "timeout_ms"],
"additionalProperties": false
}
]
},
"details": {
"type": "object",
"propertyNames": { "type": "string" },
@@ -764,6 +1277,33 @@
"required": ["action"],
"additionalProperties": false
},
"recovery": {
"oneOf": [
{
"type": "object",
"properties": {
"kind": { "type": "string", "const": "inspect_input_contract" },
"operation": { "type": "string", "minLength": 1 }
},
"required": ["kind", "operation"],
"additionalProperties": false
},
{
"type": "object",
"properties": {
"kind": { "type": "string", "const": "inspect_timeout_state" },
"operation": { "type": "string", "minLength": 1 },
"timeout_ms": {
"type": "integer",
"exclusiveMinimum": 0,
"maximum": 9007199254740991
}
},
"required": ["kind", "operation", "timeout_ms"],
"additionalProperties": false
}
]
},
"details": {
"type": "object",
"propertyNames": { "type": "string" },
+18 -1
View File
@@ -90,13 +90,30 @@ metadata in the supported JSON payloads (`parameters`, `raw_result`,
Tool execution failures return `isError: true` and the complete canonical
`{ "error": ... }` diagnostic as JSON in text `content`. Read that diagnostic
for the error code, target details, partial observations, and remediation.
for the error code, target details, partial observations, remediation, and the
optional bounded `recovery` next step. `recovery.kind=inspect_input_contract`
names the rejected operation: re-read that operation's advertised input schema
(call `tools/list` if the catalog may be stale), correct the listed fields, and
do not repeat the unchanged call. `recovery.kind=inspect_timeout_state` names
the operation and the REA server/provider deadline; inspect session/provider
state before retrying, and raise the MCP client's tool deadline only when the
client ended the call before REA returned this response. These hints never
include the complete catalog or caller arguments.
Failures omit `structuredContent`, since each advertised `outputSchema`
describes successful results. Successful replies retain their schema-validated
structured data and matching text projection. Oversized errors are retained
as Evidence before delivery of a bounded diagnostic with an export reference;
if retention is unavailable or fails, the diagnostic reports that reason.
SDK-generated input validation failures are normalized into the same canonical
`invalid_request` text when the bounded diagnostic fits the response budget.
The complete field/type diagnostic is preserved when normalization fits the
response budget, including request-specific rejected fields and reasons. Oversized
SDK diagnostics keep the existing retained-Evidence recovery path. An unknown
tool remains an MCP JSON-RPC `-32602` error, but its message directs the caller
to `tools/list` and an advertised operation instead of repeating the unknown
name.
`get_call_graph` returns a `graph` keyed by traversal depth and a top-level
`limitations` array with deduplicated execution caveats. Each node keeps its
relationship coverage and read-specific `limitations`, alongside observed
+2 -2
View File
@@ -17,7 +17,7 @@ harm_rank,location,trigger,current_copy,user_risk,proposed_replacement,implement
13,src/domain/errors.ts:EvidenceIntegrityError,Evidence validation fails,Raw constructor message,May expose validation internals and lacks recovery,"Evidence is invalid or has changed. Recreate or re-import it, then try again.",implemented,focused assertions passed; full `npm run check` passed
14,src/domain/errors.ts:UnknownRegistryError,Evidence-state mutation fails,"Residual unknown registry mutation failed: {reason}",Exposes internal subsystem and state token,"Missing IDs identify the session and retry; concurrent revision conflicts refresh current state.",implemented,focused assertions passed; full `npm run check` passed
15,src/domain/errors.ts:ProcessCaptureError,Process capture fails,Raw constructor message,May expose process and cleanup internals,"Process capture could not complete. Check command access and limits, then try again.",implemented,focused assertions passed; full `npm run check` passed
16,src/domain/errors.ts:AnalysisTimeoutError,Analysis deadline expires,"Analysis operation timed out after {timeoutMs}ms: {operation}",Exposes internal deadline and operation; weak recovery,"Analysis took too long. Try a smaller request, then run `rea doctor` if it continues.",implemented,focused assertions passed; full `npm run check` passed
16,src/domain/errors.ts:AnalysisTimeoutError,Analysis deadline expires,"REA's {operation} operation reached its {timeoutMs}ms server/provider deadline; the MCP client may have ended the call earlier",Explains the two deadline owners and avoids blind retry,"Inspect session/provider state first. If the MCP client ended the call, raise its tool deadline above {timeoutMs}ms; otherwise retry with a smaller request and do not repeat an unchanged call.",implemented,verified by projection tests and real MCP/Codex CLI timeout-recovery probes
17,src/domain/errors.ts:HopperTimeoutError,Provider deadline expires,"Hopper did not respond within {timeoutMs}ms",Exposes provider and internal deadline,"Analysis took too long. Try a smaller request, then run `rea doctor` if it continues.",implemented,focused assertions passed; full `npm run check` passed
18,src/domain/errors.ts:ProviderSelectionError,Deep-provider selection fails,"Analysis provider selection failed ({reason}) for {requestedProviderId}; candidates: {candidateIds}",Retains non-secret local selection context that needs a safe structured projection,Reason-specific guidance with stable candidate IDs and actionable rejection diagnostics,implemented,focused registry/CLI/MCP assertions passed; full `npm run check` passed
20,src/domain/errors.ts:AnalysisInputError,Parsed analysis input invalid,"Invalid analysis input for {operation}",Exposes operation identifier and lacks correction,"Analysis input is invalid. Check the arguments and try again.",implemented,focused assertions passed; full `npm run check` passed
@@ -114,5 +114,5 @@ Then rerun this installer.",implemented,verified by shell branch inspection
88,install.sh:binary absent,npm exits successfully without executable,the rea command was not installed at local path,Leaks local path and no recovery,"npm completed without installing the rea command. Check the npm global bin directory and PATH, then retry.",implemented,verified by exact absent-installed-command test
89,install.sh:version read,Installed rea executable cannot report its version,could not read the installed REA version.,No recovery instruction,"The installed REA version could not be read. Reinstall the requested version, then retry.",implemented,verified by exact unreadable-installed-version test
90,install.sh:version mismatch,Installed rea reports a different version,installed version ACTUAL does not match EXPECTED.,Low risk; gives exact integrity mismatch,Keep current copy,unchanged,verified by exact installer test
90.5,@modelcontextprotocol/server:tool input validation,Caller sends arguments that violate a declared tool schema,"Input validation error: Invalid arguments for tool TOOL: FIELD: Invalid input: expected TYPE, received TYPE",Low risk; public tool and field names plus exact type correction,Keep SDK-generated copy,unchanged,verified by real in-memory MCP client/server call with wrong argument type
90.5,@modelcontextprotocol/server:tool input validation,Caller sends arguments that violate a declared tool schema,"Canonical invalid_request with the SDK diagnostic plus recovery.kind=inspect_input_contract and a bounded instruction to read the advertised schema or call tools/list",Keeps field/type correction while telling the agent how to recover; diagnostics over 4 KiB stay on the existing retained-error path,Normalize ordinary validation failures and retain oversized diagnostics,verified by real in-memory and stdio MCP client/server calls with wrong argument types
90.6,incur:unknown CLI command,Caller enters an unknown REA command,"'COMMAND' is not a command for 'rea'. Suggested command: rea --help",Low risk; identifies input and exact discovery action,Keep framework-generated copy,unchanged,verified by real packaged dispatcher invocation
1 harm_rank location trigger current_copy user_risk proposed_replacement implementation_status verification_result
17 13 src/domain/errors.ts:EvidenceIntegrityError Evidence validation fails Raw constructor message May expose validation internals and lacks recovery Evidence is invalid or has changed. Recreate or re-import it, then try again. implemented focused assertions passed; full `npm run check` passed
18 14 src/domain/errors.ts:UnknownRegistryError Evidence-state mutation fails Residual unknown registry mutation failed: {reason} Exposes internal subsystem and state token Missing IDs identify the session and retry; concurrent revision conflicts refresh current state. implemented focused assertions passed; full `npm run check` passed
19 15 src/domain/errors.ts:ProcessCaptureError Process capture fails Raw constructor message May expose process and cleanup internals Process capture could not complete. Check command access and limits, then try again. implemented focused assertions passed; full `npm run check` passed
20 16 src/domain/errors.ts:AnalysisTimeoutError Analysis deadline expires Analysis operation timed out after {timeoutMs}ms: {operation} REA's {operation} operation reached its {timeoutMs}ms server/provider deadline; the MCP client may have ended the call earlier Exposes internal deadline and operation; weak recovery Explains the two deadline owners and avoids blind retry Analysis took too long. Try a smaller request, then run `rea doctor` if it continues. Inspect session/provider state first. If the MCP client ended the call, raise its tool deadline above {timeoutMs}ms; otherwise retry with a smaller request and do not repeat an unchanged call. implemented focused assertions passed; full `npm run check` passed verified by projection tests and real MCP/Codex CLI timeout-recovery probes
21 17 src/domain/errors.ts:HopperTimeoutError Provider deadline expires Hopper did not respond within {timeoutMs}ms Exposes provider and internal deadline Analysis took too long. Try a smaller request, then run `rea doctor` if it continues. implemented focused assertions passed; full `npm run check` passed
22 18 src/domain/errors.ts:ProviderSelectionError Deep-provider selection fails Analysis provider selection failed ({reason}) for {requestedProviderId}; candidates: {candidateIds} Retains non-secret local selection context that needs a safe structured projection Reason-specific guidance with stable candidate IDs and actionable rejection diagnostics implemented focused registry/CLI/MCP assertions passed; full `npm run check` passed
23 20 src/domain/errors.ts:AnalysisInputError Parsed analysis input invalid Invalid analysis input for {operation} Exposes operation identifier and lacks correction Analysis input is invalid. Check the arguments and try again. implemented focused assertions passed; full `npm run check` passed
114
115
116
117
118
+4 -1
View File
@@ -1,6 +1,8 @@
import { z } from "zod";
import { analysisErrorRecoverySchema } from "../domain/analysisErrorRecovery.js";
import { jsonValueSchema } from "../domain/jsonValue.js";
import type { AnalysisErrorProjection } from "../domain/analysisErrorProjection.js";
const categorySchema = z.enum([
"invalid_input",
@@ -24,6 +26,7 @@ const common = {
message: z.string().min(1),
retryable: z.boolean(),
remediation: remediationSchema,
recovery: analysisErrorRecoverySchema.optional(),
};
const genericDetails = z.record(z.string(), jsonValueSchema).optional();
const generic = <Code extends string>(code: Code) =>
@@ -76,7 +79,7 @@ export const analysisErrorProjectionSchema = z.discriminatedUnion("code", [
generic("configuration_invalid"),
generic("target_unavailable"),
generic("execution_failure"),
]);
]) as unknown as z.ZodType<AnalysisErrorProjection>;
/** CLI label and input diagnostics surrounding the strict canonical error projection. */
export const analysisCliErrorEnvelopeSchema = z
+10 -1
View File
@@ -82,6 +82,7 @@ export const analysisErrorRemediationAction = (
error.remediationAction !== undefined
)
return error.remediationAction;
if (error instanceof AnalysisTimeoutError) return timeoutRemediation(error);
if (error instanceof HopperTimeoutError)
return error.operation === undefined
? "Review details.launcher and inspect Hopper for ongoing initial analysis or a pending loader, demo, or license dialog. For a large target, increase REA_HOPPER_STARTUP_TIMEOUT_MS in the environment that starts REA and allow at least that long in the MCP client. Resolve retained cleanup before reopening."
@@ -130,7 +131,7 @@ export const analysisErrorRemediationAction = (
if (error instanceof ConfigurationError && error.settings.length > 0)
return `Correct ${[...new Set(error.settings.map(({ setting }) => setting))].join(", ")} in the environment that starts REA (your shell or the MCP client's registration), then rerun.`;
if (error instanceof AnalysisInputError)
return "Correct the listed arguments and retry.";
return `Read the declared input schema or CLI usage for ${error.operation}; if using MCP and the catalog may be stale, call tools/list. Correct the listed fields, then retry. Do not repeat the unchanged call.`;
if (
error instanceof AnalysisOutputError &&
error.remediationAction !== undefined
@@ -254,6 +255,8 @@ export const analysisErrorUserMessage = (error: AnalysisError): string => {
error.userMessage ??
"Analysis input is invalid. Check the arguments and try again."
);
if (error instanceof AnalysisTimeoutError)
return error.userMessage ?? timeoutUserMessage(error);
const hopperMessage = hopperErrorUserMessage(error);
if (hopperMessage !== undefined) return hopperMessage;
if (error.userMessage !== undefined) return error.userMessage;
@@ -383,6 +386,12 @@ const standardErrorMessage = (tag: AnalysisErrorTag): string | undefined => {
return undefined;
};
const timeoutUserMessage = (error: AnalysisTimeoutError): string =>
`REA's ${error.operation} operation reached its ${String(error.timeoutMs)}ms server/provider deadline. If the MCP client timed out before this response, increase its tool deadline above ${String(error.timeoutMs)}ms; otherwise retry with a smaller request after checking session/provider state. Do not repeat an unchanged call while work or cleanup may still be active.`;
const timeoutRemediation = (error: AnalysisTimeoutError): string =>
`Inspect session/provider state for ${error.operation} before retrying. If the MCP client ended the call before REA returned this error, raise its tool deadline above ${String(error.timeoutMs)}ms; otherwise retry with a smaller request. Do not repeat an unchanged call while work or cleanup may still be active.`;
const UNREADABLE_OUTPUT_TAGS: ReadonlySet<AnalysisErrorTag> = new Set([
"AnalysisProtocolError",
"AnalysisOutputError",
@@ -25,6 +25,52 @@ import { ProviderSelectionError } from "./providerSelectionError.js";
import { analysisErrorProjectionSchema } from "../contracts/errorSchemas.js";
import { nativeCallPartialObservationSchema } from "./native/nativeCallPartialObservation.js";
describe("agent recovery metadata", () => {
it("names the input contract for malformed analysis arguments", () => {
const projected = projectAnalysisError(
new AnalysisInputError("inspect_analysis_view", undefined, [
{
path: ["view", "kind"],
reason: "invalid_value",
expected: ["summary", "module"],
},
]),
);
expect(projected).toMatchObject({
recovery: {
kind: "inspect_input_contract",
operation: "inspect_analysis_view",
},
remediation: {
action:
"Read the declared input schema or CLI usage for inspect_analysis_view; if using MCP and the catalog may be stale, call tools/list. Correct the listed fields, then retry. Do not repeat the unchanged call.",
},
});
expect(analysisErrorProjectionSchema.parse(projected)).toEqual(projected);
});
it("separates timeout-state inspection from client deadline recovery", () => {
const projected = projectAnalysisError(
new AnalysisTimeoutError("inspect_binary_layout", 2_500),
);
expect(projected).toMatchObject({
code: "provider_timeout",
recovery: {
kind: "inspect_timeout_state",
operation: "inspect_binary_layout",
timeout_ms: 2_500,
},
message: expect.stringContaining("server/provider deadline"),
remediation: {
action: expect.stringContaining("MCP client"),
},
});
expect(analysisErrorProjectionSchema.parse(projected)).toEqual(projected);
});
});
describe("capability failure retry disposition", () => {
it.each([
{ options: {}, retryable: false, category: "unsupported_provider" },
+21
View File
@@ -1,4 +1,5 @@
import { projectArtifactIntegrityDetails } from "./artifactIntegrity.js";
import type { AnalysisErrorRecovery } from "./analysisErrorRecovery.js";
import { jsonValueSchema, type JsonValue } from "./jsonValue.js";
import { isImmutableJsonSnapshot } from "./immutableJson.js";
@@ -102,6 +103,7 @@ export const projectAnalysisError = (
: jsonValueSchema.parse(partialObservation),
}),
};
const recovery = errorRecovery(error);
return {
code,
category: analysisErrorCategory(error),
@@ -116,10 +118,28 @@ export const projectAnalysisError = (
remediation: {
action: analysisErrorRemediationAction(error),
},
...(recovery === undefined ? {} : { recovery }),
...(Object.keys(details).length === 0 ? {} : { details }),
};
};
const errorRecovery = (
error: AnalysisError,
): AnalysisErrorRecovery | undefined => {
if (error instanceof AnalysisInputError)
return {
kind: "inspect_input_contract",
operation: error.operation,
};
if (error instanceof AnalysisTimeoutError)
return {
kind: "inspect_timeout_state",
operation: error.operation,
timeout_ms: error.timeoutMs,
};
return undefined;
};
const underlyingErrorCode = (
error: AnalysisError,
): AnalysisErrorProjection["code"] => {
@@ -619,5 +639,6 @@ export interface AnalysisErrorProjection extends Readonly<
readonly remediation: Readonly<{
action: string;
}>;
readonly recovery?: AnalysisErrorRecovery;
readonly details?: Readonly<Record<string, JsonValue>>;
}
+36
View File
@@ -0,0 +1,36 @@
import { z } from "zod";
/**
* Small, bounded next-step metadata for agent-facing analysis failures.
*
* The prose recovery remains in `remediation.action`; these fields give a
* caller a stable way to choose the next inspection without embedding a tool
* catalog or caller arguments in every error.
*/
export type AnalysisErrorRecovery =
| {
readonly kind: "inspect_input_contract";
readonly operation: string;
}
| {
readonly kind: "inspect_timeout_state";
readonly operation: string;
readonly timeout_ms: number;
};
export const analysisErrorRecoverySchema: z.ZodType<AnalysisErrorRecovery> =
z.discriminatedUnion("kind", [
z
.object({
kind: z.literal("inspect_input_contract"),
operation: z.string().min(1),
})
.strict(),
z
.object({
kind: z.literal("inspect_timeout_state"),
operation: z.string().min(1),
timeout_ms: z.number().int().positive(),
})
.strict(),
]);
+90 -16
View File
@@ -31,6 +31,67 @@ const firstText = (result: {
return undefined;
};
const expectFittingSdkValidationRecovery = (result: {
readonly isError?: boolean | undefined;
readonly content: readonly {
readonly type: string;
readonly text?: string;
}[];
}): void => {
expect(result.isError).toBe(true);
expect(JSON.parse(firstText(result) ?? "null")).toMatchObject({
error: {
code: "invalid_request",
recovery: {
kind: "inspect_input_contract",
operation: "procedure_pseudo_code",
},
details: {
operation: "procedure_pseudo_code",
issues: [
{
path: [],
reason: "invalid_value",
message: expect.stringContaining("required"),
},
],
},
},
});
};
const expectEscapedSdkDiagnosticRetention = async (
client: Client,
records: InvestigationRecords,
previousEvidenceId: string,
): Promise<void> => {
const quotedKey = '"\\'.repeat(350);
const quoted = await client.callTool({
name: "procedure_pseudo_code",
arguments: { required: "present", [quotedKey]: true },
});
expect(quoted.isError).toBe(true);
expect(JSON.parse(firstText(quoted) ?? "null")).toMatchObject({
error: {
code: "resource_constraint",
details: {
reported_limits: {
evidence_reference: { kind: "retained-evidence" },
},
},
},
});
expect(Buffer.byteLength(JSON.stringify(quoted))).toBeLessThan(4096);
const bundle = records.exportEvidenceBundle();
expect(bundle.records).toHaveLength(2);
const quotedOriginal = bundle.records.find(
(record) => record.evidence_id !== previousEvidenceId,
);
expect(quotedOriginal?.raw_result).toMatchObject({
content: [{ text: expect.stringContaining(quotedKey) }],
});
};
describe("MCP request bookkeeping", () => {
it("releases list, tool, and completion state when cancellation arrives", () => {
const completion = new AbortController();
@@ -55,7 +116,7 @@ describe("MCP request bookkeeping", () => {
describe("Modern protocol error delivery", () => {
it("preserves the modern wire envelope when replacing an oversized error", async () => {
const records = new InvestigationRecords();
const delivery = new ToolResultDelivery(4096);
const delivery = new ToolResultDelivery(16384);
const info = { name: "wire-error-recovery", version: "1" };
const handler = createMcpHandler(() => {
const server = new EvidenceMcpServer(
@@ -92,16 +153,28 @@ describe("Modern protocol error delivery", () => {
});
try {
await client.connect(transport);
const invalidField = `unexpected_${"x".repeat(5000)}_tail`;
const fitting = await client.callTool({
name: "procedure_pseudo_code",
arguments: { unexpected: true },
arguments: { [invalidField]: true },
});
expect(fitting.isError).toBe(true);
expect(firstText(fitting)).toContain("Input validation error");
expect(JSON.parse(firstText(fitting) ?? "null")).toMatchObject({
error: {
code: "invalid_request",
recovery: {
kind: "inspect_input_contract",
operation: "procedure_pseudo_code",
},
details: {
issues: [{ message: expect.stringContaining(invalidField) }],
},
},
});
const recovered = await client.callTool({
name: "procedure_pseudo_code",
arguments: { selected: "value", ["x".repeat(10000)]: true },
arguments: { selected: "value", ["x".repeat(20000)]: true },
});
expect(recovered.isError).toBe(true);
expect(recovered.structuredContent).toBeUndefined();
@@ -137,7 +210,7 @@ describe("Modern protocol error delivery", () => {
});
}
expect(Buffer.byteLength(JSON.stringify(toolResults[1]))).toBeLessThan(
4096,
16384,
);
expect(toolResults[1]?._meta).toMatchObject({
provenance: { untrusted_content: true, source: "analysis_target" },
@@ -203,8 +276,13 @@ describe("SDK validation error delivery", () => {
name: "procedure_pseudo_code",
arguments: { unexpected: true },
});
expect(fitting.isError).toBe(true);
expect(firstText(fitting)).toContain("Input validation error");
expectFittingSdkValidationRecovery(fitting);
await expect(
client.callTool({ name: "missing_tool", arguments: {} }),
).rejects.toThrow(
"Call tools/list, select an advertised tool, and retry",
);
const oversized = await client.callTool({
name: "procedure_pseudo_code",
@@ -260,15 +338,11 @@ describe("SDK validation error delivery", () => {
evidenceReference.evidence_id,
);
const quotedKey = '"\\'.repeat(350);
const quoted = await client.callTool({
name: "procedure_pseudo_code",
arguments: { required: "present", [quotedKey]: true },
});
expect(quoted.isError).toBe(true);
expect(firstText(quoted)).toContain("Input validation error");
expect(Buffer.byteLength(JSON.stringify(quoted))).toBeLessThan(4096);
expect(records.exportEvidenceBundle().records).toHaveLength(1);
await expectEscapedSdkDiagnosticRetention(
client,
records,
evidenceReference.evidence_id,
);
const recovered = await client.callTool({
name: "procedure_pseudo_code",
+125 -25
View File
@@ -2,9 +2,11 @@ import { AsyncLocalStorage } from "node:async_hooks";
import {
isCallToolResult,
isJSONRPCErrorResponse,
isJSONRPCRequest,
isJSONRPCNotification,
isJSONRPCResultResponse,
type CallToolResult,
type Implementation,
type Icon,
type JSONRPCMessage,
@@ -27,7 +29,10 @@ import {
import { CATALOG_IDENTITY, createCatalogIdentity } from "../catalogIdentity.js";
import { transformAdvertisedInputJsonSchema } from "../contracts/toolSchemaMetadata.js";
import type { EvidenceWriter } from "../application/investigation/InvestigationRecordPort.js";
import { AnalysisResourceConstraintError } from "../domain/analysisErrorCore.js";
import {
AnalysisInputError,
AnalysisResourceConstraintError,
} from "../domain/analysisErrorCore.js";
import { projectAnalysisError } from "../domain/analysisErrorProjection.js";
import { createEvidence } from "../domain/evidence.js";
import { jsonObjectSchema } from "../domain/jsonValue.js";
@@ -65,6 +70,39 @@ const hasAdvertisedInputJsonSchema = (value: unknown): boolean => {
const isRecord = (value: unknown): value is Record<string, unknown> =>
typeof value === "object" && value !== null && !Array.isArray(value);
/** Recognize the SDK's argument diagnostic after its wire size is checked. */
const sdkInputValidationDiagnostic = (
text: string | undefined,
tool: string | undefined,
): string | undefined => {
if (text === undefined || tool === undefined) return undefined;
const prefix = `Input validation error: Invalid arguments for tool ${tool}`;
if (!text.startsWith(prefix) || text[prefix.length] !== ":") return undefined;
const diagnostic = text.slice(prefix.length + 1).trimStart();
return diagnostic.length === 0 ? undefined : diagnostic;
};
/** Replace the tool payload after the SDK codec while retaining its envelope. */
const replaceToolResultPayload = (
original: CallToolResult,
replacement: CallToolResult,
): CallToolResult => {
const {
content: _content,
structuredContent: _structuredContent,
isError: _isError,
_meta,
...envelope
} = original;
return {
...envelope,
...replacement,
...(_meta === undefined && replacement._meta === undefined
? {}
: { _meta: { ..._meta, ...replacement._meta } }),
};
};
/** Remove only client-facing output schemas; SDK registrations stay canonical. */
const omitOutputSchemasFromToolsList = (
message: JSONRPCMessage,
@@ -305,7 +343,7 @@ class ErrorEvidenceTransport implements Transport {
id !== undefined &&
this.pendingToolLists.has(id);
try {
const recovered = this.recoverError(message, tool);
const recovered = this.recoverProtocolError(message, tool);
const advertised = omitToolSchemas
? omitOutputSchemasFromToolsList(recovered, id)
: recovered;
@@ -377,16 +415,45 @@ class ErrorEvidenceTransport implements Transport {
},
}
: message.result;
const encoded =
structured !== undefined
? encodeToolResult(structured, budget, "error-text", targetContent)
: measureMcpResultWithinBudget(sdkResultWithMarker, budget);
if (encoded.ok) return message;
const operation = tool ?? "mcp_tool_error";
const originalResult = jsonObjectSchema.parse(message.result);
const sdkDiagnostic = !isStructured
? message.result.content.find((item) => item.type === "text")?.text
: undefined;
let encoded =
structured !== undefined
? encodeToolResult(structured, budget, "error-text", targetContent)
: measureMcpResultWithinBudget(sdkResultWithMarker, budget);
const normalized = encoded.ok
? this.normalizeSdkValidationError(
message,
tool,
targetContent,
sdkInputValidationDiagnostic(sdkDiagnostic, tool),
)
: undefined;
if (
normalized !== undefined &&
isJSONRPCResultResponse(normalized) &&
isCallToolResult(normalized.result)
) {
// The structured diagnostic is a private carrier removed before sending.
// Measure the actual wire result, including its preserved SDK metadata.
const { structuredContent, ...wireResult } = normalized.result;
encoded =
normalized.result.content.length === 0
? encodeToolResult(
jsonObjectSchema.parse(structuredContent),
budget,
"error-text",
targetContent,
)
: measureMcpResultWithinBudget(wireResult, budget);
if (encoded.ok) return normalized;
// JSON escaping and recovery fields can exceed the budget even when
// the original SDK result fits. Retain that original before replacing it.
}
if (encoded.ok) return message;
const operation = tool ?? "mcp_tool_error";
const originalResult = jsonObjectSchema.parse(message.result);
const retainedResult = structured ?? originalResult;
const originalError = isStructured
? analysisErrorProjectionSchema.safeParse(structured?.["error"])
@@ -449,25 +516,58 @@ class ErrorEvidenceTransport implements Transport {
: "Restart REA with a larger REA_MCP_MAX_RESPONSE_BYTES and matching client receive buffer, then retry. The original error could not be retained by this server.",
},
);
// This transport runs after the SDK's wire codec. Replace the tool payload
// while retaining its protocol envelope, including the revision's result
// discriminator and server identity metadata.
const {
content: _content,
structuredContent: _structuredContent,
isError: _isError,
_meta,
...envelope
} = message.result;
const replacement = this.delivery.toErrorToolResult(failure, targetContent);
return {
...message,
result: {
...envelope,
...replacement,
...(_meta === undefined && replacement._meta === undefined
? {}
: { _meta: { ..._meta, ...replacement._meta } }),
result: replaceToolResultPayload(message.result, replacement),
};
}
private normalizeSdkValidationError(
message: JSONRPCMessage,
tool: string | undefined,
targetContent: boolean,
diagnostic: string | undefined,
): JSONRPCMessage | undefined {
if (
tool === undefined ||
diagnostic === undefined ||
!isJSONRPCResultResponse(message) ||
!isCallToolResult(message.result)
)
return undefined;
const error = new AnalysisInputError(
tool,
{
userMessage: `The arguments for MCP tool ${tool} do not match its advertised input schema.`,
},
[{ path: [], reason: "invalid_value", message: diagnostic }],
);
const result = this.delivery.toErrorToolResult(error, targetContent);
return {
...message,
result: replaceToolResultPayload(message.result, result),
};
}
private recoverProtocolError(
message: JSONRPCMessage,
tool: string | undefined,
): JSONRPCMessage {
const recovered = this.recoverError(message, tool);
if (
!isJSONRPCErrorResponse(recovered) ||
tool === undefined ||
recovered.error.code !== -32602 ||
recovered.error.message !== `Tool ${tool} not found`
)
return recovered;
return {
...recovered,
error: {
...recovered.error,
message:
"The requested MCP tool is not advertised by this REA server. Call tools/list, select an advertised tool, and retry; do not repeat the unknown tool name.",
},
};
}
@@ -164,7 +164,18 @@ cliTest(
},
});
expect(parseMcpToolError(response)).toMatchObject({
error: { code: "provider_timeout" },
error: {
code: "provider_timeout",
message: expect.stringContaining("server/provider deadline"),
remediation: {
action: expect.stringContaining("MCP client"),
},
recovery: {
kind: "inspect_timeout_state",
operation: "analyze_javascript_application",
timeout_ms: 1,
},
},
});
const bundle = await getBundle(client);
const partial = bundle.records.find(
@@ -132,7 +132,13 @@ describe("browser and Inspector CLI input diagnostics", () => {
message:
"Analysis input is invalid. Check the arguments and try again.",
retryable: true,
remediation: { action: "Correct the listed arguments and retry." },
remediation: {
action: `Read the declared input schema or CLI usage for ${input.operation}; if using MCP and the catalog may be stale, call tools/list. Correct the listed fields, then retry. Do not repeat the unchanged call.`,
},
recovery: {
kind: "inspect_input_contract",
operation: input.operation,
},
details: {
operation: input.operation,
issues: expect.arrayContaining([input.issue]),
@@ -184,7 +184,14 @@ describe("process CLI input diagnostics", () => {
category: "invalid_input",
message: "Analysis input is invalid. Check the arguments and try again.",
retryable: true,
remediation: { action: "Correct the listed arguments and retry." },
remediation: {
action:
"Read the declared input schema or CLI usage for capture_process_scenario; if using MCP and the catalog may be stale, call tools/list. Correct the listed fields, then retry. Do not repeat the unchanged call.",
},
recovery: {
kind: "inspect_input_contract",
operation: "capture_process_scenario",
},
details: expect.objectContaining({
operation: "capture_process_scenario",
issues: expect.arrayContaining([
+21 -4
View File
@@ -7,6 +7,7 @@ import { parseEvidence } from "../../../src/domain/evidence.js";
import { ok } from "../../../src/domain/result.js";
import { createServer } from "../../../src/server/createServer.js";
import { createTestBinarySession } from "../../fixtures/binarySession.js";
import { parseMcpToolError } from "../../fixtures/mcpToolError.js";
import { assertAdvertisedMcpContracts } from "./mcpContractHarness.js";
const identity = { id: "adb", name: "Android Debug Bridge", version: null };
@@ -391,8 +392,24 @@ it("rejects invalid tool input before reaching the provider", async () => {
arguments: { serial: "bad serial" },
});
expect(rejected.isError).toBe(true);
const text = (rejected.content as { readonly text?: string }[])
.map((part) => part.text ?? "")
.join("");
expect(text).toContain("Invalid arguments for tool inspect_adb_device");
expect(parseMcpToolError(rejected)).toMatchObject({
error: {
code: "invalid_request",
recovery: {
kind: "inspect_input_contract",
operation: "inspect_adb_device",
},
details: {
operation: "inspect_adb_device",
issues: [
{
path: [],
reason: "invalid_value",
message:
"serial: Device serial cannot contain whitespace or quotes",
},
],
},
},
});
});
+10
View File
@@ -245,6 +245,16 @@ const verifySessionAndComparisonTools = async (
},
});
expect(malformedComparison.isError).toBe(true);
expect(parseMcpToolError(malformedComparison)).toMatchObject({
error: {
code: "invalid_request",
recovery: {
kind: "inspect_input_contract",
operation: "compare_web_captures",
},
details: { operation: "compare_web_captures" },
},
});
expect(JSON.stringify(malformedComparison.content)).toContain("types");
const incompleteComparison = await connected.client.callTool({
name: "compare_web_captures",
@@ -101,7 +101,14 @@ cliTest(
code: "invalid_request",
category: "invalid_input",
retryable: true,
remediation: { action: "Correct the listed arguments and retry." },
remediation: {
action:
"Read the declared input schema or CLI usage for capture_browser_scenario; if using MCP and the catalog may be stale, call tools/list. Correct the listed fields, then retry. Do not repeat the unchanged call.",
},
recovery: {
kind: "inspect_input_contract",
operation: "capture_browser_scenario",
},
details: {
operation: "capture_browser_scenario",
issues: [
@@ -160,9 +160,15 @@ it("retains byte-search coverage and rejects malformed pages before creating Evi
arguments: args,
});
expect(invalid.isError).toBe(true);
expect(JSON.stringify(invalid.content)).toContain(
"Input validation error",
);
expect(parseMcpToolError(invalid)).toMatchObject({
error: {
code: "invalid_request",
recovery: {
kind: "inspect_input_contract",
operation: "search_bytes",
},
},
});
}
expect(harness.calls).toHaveLength(calls);
} finally {
+11 -7
View File
@@ -8,13 +8,17 @@ const LARGE_PATTERN_BYTES = 4 * 1024 * 1024;
const inputValidationText = (result: CallToolResult): string => {
expect(result.isError).toBe(true);
const text = result.content.find((item) => item.type === "text");
if (text?.type !== "text") throw new Error("Input error omitted text");
expect(text.text).toMatch(
/^Input validation error: Invalid arguments for tool search_bytes:/u,
);
expect(text.text).not.toMatch(/RangeError|Maximum call stack/i);
return text.text;
const diagnostic = parseMcpToolError(result).error;
expect(diagnostic).toMatchObject({
code: "invalid_request",
recovery: {
kind: "inspect_input_contract",
operation: "search_bytes",
},
});
const text = JSON.stringify(diagnostic);
expect(text).not.toMatch(/RangeError|Maximum call stack/i);
return text;
};
it("validates large search_bytes inputs without breaking stdio MCP", async () => {
+20 -4
View File
@@ -578,10 +578,26 @@ describe("json depth bounds over MCP", () => {
right: input.right,
},
});
const text = JSON.stringify(result.content);
expect(result.isError, text).toBe(true);
expect(text).toContain("Input validation");
expect(text).toContain("maximum nesting depth");
expect(result.isError, JSON.stringify(result.content)).toBe(true);
expect(parseMcpToolError(result)).toMatchObject({
error: {
code: "invalid_request",
recovery: {
kind: "inspect_input_contract",
operation: "compare_process_captures",
},
details: {
operation: "compare_process_captures",
issues: [
{
path: [],
reason: "invalid_value",
message: `left.parameters: JSON value exceeds maximum nesting depth of ${MAX_JSON_DEPTH}`,
},
],
},
},
});
}, 10_000);
});
@@ -77,7 +77,18 @@ it("retains timeout Evidence and recovers on the same MCP server", async () => {
},
});
const timeout = parseMcpToolError(interrupted).error;
expect(timeout.code).toBe("provider_timeout");
expect(timeout).toMatchObject({
code: "provider_timeout",
message: expect.stringContaining("server/provider deadline"),
remediation: {
action: expect.stringContaining("MCP client"),
},
recovery: {
kind: "inspect_timeout_state",
operation: "analyze_web_bundle",
timeout_ms: 1,
},
});
const timeoutDetails = z
.object({ partial_observation: z.unknown() })
.passthrough()