Serve build-recorded default compact and explicit full catalogs, then materialize canonical analysis handlers on first use. Keep transport, Evidence delivery, profile identity and caller choices consistent. Share sealed MCP server ownership between eager and deferred lifecycle paths. Validate 125 focused cases, schema/contract repeats, negative controls, builds/static/docs checks and latest-main integration. Record 72 real SDK task trials with independent Rust metadata oracles and whole-family resource bounds; disclose shifted first-RPC cost and unchanged catalog tokens. Qualify the real Rizin small-target debug lifecycle separately. Automated review findings addressed; Codex review unavailable due repository usage limits.
700 lines
24 KiB
TypeScript
700 lines
24 KiB
TypeScript
import { AsyncLocalStorage } from "node:async_hooks";
|
|
|
|
import {
|
|
isCallToolResult,
|
|
isJSONRPCErrorResponse,
|
|
isJSONRPCRequest,
|
|
isJSONRPCNotification,
|
|
isJSONRPCResultResponse,
|
|
type CallToolResult,
|
|
type Implementation,
|
|
type Icon,
|
|
type JSONRPCMessage,
|
|
type McpServerOptions,
|
|
type RegisteredTool,
|
|
type RegisteredPrompt,
|
|
type PromptCallback,
|
|
type RequestId,
|
|
type ScopeChallengeHandler,
|
|
type StandardSchemaWithJSON,
|
|
type ToolAnnotations,
|
|
type ToolCallback,
|
|
type Transport,
|
|
type TransportSendOptions,
|
|
} from "@modelcontextprotocol/server";
|
|
|
|
import { analysisErrorProjectionSchema } from "../contracts/errorSchemas.js";
|
|
import {
|
|
presentAdvertisedInputSchema,
|
|
type CompactInputSchemaPresentation,
|
|
} from "../contracts/compactInputSchema.js";
|
|
import type { createCatalogIdentity } from "../catalogIdentity.js";
|
|
import { transformAdvertisedInputJsonSchema } from "../contracts/toolSchemaMetadata.js";
|
|
import type { EvidenceWriter } from "../application/investigation/InvestigationRecordPort.js";
|
|
import {
|
|
AnalysisInputError,
|
|
AnalysisResourceConstraintError,
|
|
} from "../domain/analysisErrorCore.js";
|
|
import { projectAnalysisError } from "../domain/analysisErrorProjection.js";
|
|
import { createEvidence } from "../domain/evidence.js";
|
|
import { jsonObjectSchema, type JsonValue } from "../domain/jsonValue.js";
|
|
import {
|
|
mcpResponseOverflowProjection,
|
|
type ToolResultDelivery,
|
|
} from "./ToolResultDelivery.js";
|
|
import { toolReturnsTargetContent } from "../contracts/toolEffects.js";
|
|
import {
|
|
carriesTargetContentMarker,
|
|
encodeToolResult,
|
|
measureMcpResultWithinBudget,
|
|
TARGET_CONTENT_RESULT_META,
|
|
} from "./toolResultEncoding.js";
|
|
import { RequestTrackingMcpServer } from "./RequestTrackingMcpServer.js";
|
|
|
|
export type { CompactInputSchemaPresentation } from "../contracts/compactInputSchema.js";
|
|
|
|
const currentRequestSignal = new AsyncLocalStorage<AbortSignal>();
|
|
|
|
/** Signal for the completion request currently being dispatched by the SDK. */
|
|
export const currentMcpRequestSignal = (): AbortSignal | undefined =>
|
|
currentRequestSignal.getStore();
|
|
|
|
/** A Zod schema whose standard interface exposes a JSON Schema projection. */
|
|
const hasAdvertisedInputJsonSchema = (value: unknown): boolean => {
|
|
if (typeof value !== "object" || value === null) return false;
|
|
const standard = Reflect.get(value, "~standard");
|
|
if (typeof standard !== "object" || standard === null) return false;
|
|
const jsonSchema = Reflect.get(standard, "jsonSchema");
|
|
return (
|
|
typeof jsonSchema === "object" &&
|
|
jsonSchema !== null &&
|
|
Reflect.get(jsonSchema, "input") !== undefined
|
|
);
|
|
};
|
|
|
|
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,
|
|
requestId: RequestId,
|
|
): JSONRPCMessage => {
|
|
if (
|
|
!isJSONRPCResultResponse(message) ||
|
|
message.id !== requestId ||
|
|
!isRecord(message.result) ||
|
|
!Array.isArray(message.result.tools)
|
|
)
|
|
return message;
|
|
return {
|
|
...message,
|
|
result: {
|
|
...message.result,
|
|
tools: message.result.tools.map((tool: unknown) => {
|
|
if (!isRecord(tool)) return tool;
|
|
const { outputSchema: _outputSchema, ...advertisedTool } = tool;
|
|
return advertisedTool;
|
|
}),
|
|
},
|
|
} as JSONRPCMessage;
|
|
};
|
|
|
|
/** Release every transport-owned reference for a cancelled/completed request. */
|
|
export const forgetPendingMcpRequest = (
|
|
pendingTools: Map<RequestId, string>,
|
|
pendingToolLists: Set<RequestId>,
|
|
pendingCompletions: Map<RequestId, AbortController>,
|
|
id: RequestId,
|
|
): void => {
|
|
pendingTools.delete(id);
|
|
pendingToolLists.delete(id);
|
|
pendingCompletions.get(id)?.abort();
|
|
pendingCompletions.delete(id);
|
|
};
|
|
import type { PromptCompletionSource } from "./promptCompletion.js";
|
|
|
|
/** Standard-schema registration boundary shared with deferred SDK bindings. */
|
|
export interface StandardToolRegistration {
|
|
readonly title?: string;
|
|
readonly description?: string;
|
|
readonly inputSchema?: StandardSchemaWithJSON;
|
|
readonly outputSchema?: StandardSchemaWithJSON;
|
|
readonly annotations?: ToolAnnotations;
|
|
readonly icons?: Icon[];
|
|
readonly scopeChallenge?: ScopeChallengeHandler;
|
|
readonly _meta?: Record<string, unknown>;
|
|
}
|
|
|
|
/** Bind oversized MCP error recovery to this server's Evidence ledger. */
|
|
export class EvidenceMcpServerCore extends RequestTrackingMcpServer {
|
|
private identity: ReturnType<typeof createCatalogIdentity> | undefined;
|
|
|
|
/** Composition supplies identity without importing the canonical schema graph. */
|
|
get catalogIdentity(): ReturnType<typeof createCatalogIdentity> {
|
|
if (this.identity === undefined)
|
|
throw new Error("MCP catalog identity has not been configured");
|
|
return this.identity;
|
|
}
|
|
|
|
/** Attach the process-owned session only after lazy runtime construction. */
|
|
attachRuntime(
|
|
recordEvidence: EvidenceWriter["recordEvidence"] | undefined,
|
|
delivery: ToolResultDelivery,
|
|
identity: ReturnType<typeof createCatalogIdentity>,
|
|
): void {
|
|
if (
|
|
this.identity !== undefined &&
|
|
this.identity.digests.combined_sha256 !== identity.digests.combined_sha256
|
|
)
|
|
throw new Error("MCP catalog identity does not match runtime");
|
|
this.recordEvidence = recordEvidence;
|
|
this.delivery = delivery;
|
|
this.identity = identity;
|
|
}
|
|
|
|
protected promptCompletion: PromptCompletionSource | undefined;
|
|
bindPromptCompletion(
|
|
completion: PromptCompletionSource,
|
|
): PromptCompletionSource {
|
|
this.promptCompletion = completion;
|
|
return completion;
|
|
}
|
|
|
|
registerCanonicalPrompt<Args extends StandardSchemaWithJSON>(
|
|
name: string,
|
|
config: { title?: string; description?: string; argsSchema: Args },
|
|
callback: PromptCallback<Args>,
|
|
): RegisteredPrompt {
|
|
return super.registerPrompt(name, config, callback);
|
|
}
|
|
private readonly compactPresentation:
|
|
| CompactInputSchemaPresentation
|
|
| undefined;
|
|
|
|
constructor(
|
|
info: Implementation,
|
|
options: McpServerOptions,
|
|
private recordEvidence: EvidenceWriter["recordEvidence"] | undefined,
|
|
public delivery: ToolResultDelivery,
|
|
compactPresentation: CompactInputSchemaPresentation | undefined = undefined,
|
|
private readonly omitOutputSchemas = false,
|
|
identity?: ReturnType<typeof createCatalogIdentity>,
|
|
) {
|
|
super(info, options);
|
|
this.compactPresentation =
|
|
compactPresentation === undefined
|
|
? undefined
|
|
: { budgetBytes: compactPresentation.budgetBytes };
|
|
this.identity = identity;
|
|
}
|
|
|
|
/**
|
|
* Compact advertised input schemas and optionally omit output-schema
|
|
* advertisements. Canonical parsers and contracts remain registered; the
|
|
* output-schema-off path validates successful structured results locally.
|
|
* The signatures mirror the SDK's registrations so call sites keep their
|
|
* inferred handler argument types.
|
|
*/
|
|
override registerTool<
|
|
OutputArgs extends StandardSchemaWithJSON,
|
|
InputArgs extends StandardSchemaWithJSON | undefined = undefined,
|
|
>(
|
|
name: string,
|
|
config: {
|
|
title?: string;
|
|
description?: string;
|
|
inputSchema?: InputArgs;
|
|
outputSchema?: OutputArgs;
|
|
annotations?: ToolAnnotations;
|
|
icons?: Icon[];
|
|
scopeChallenge?: ScopeChallengeHandler;
|
|
_meta?: Record<string, unknown>;
|
|
},
|
|
callback: ToolCallback<InputArgs>,
|
|
): RegisteredTool {
|
|
const budget = this.compactPresentation?.budgetBytes;
|
|
const inputSchema =
|
|
budget !== undefined &&
|
|
config !== undefined &&
|
|
hasAdvertisedInputJsonSchema(config.inputSchema)
|
|
? transformAdvertisedInputJsonSchema(
|
|
config.inputSchema as Parameters<
|
|
typeof transformAdvertisedInputJsonSchema
|
|
>[0],
|
|
(projected) =>
|
|
presentAdvertisedInputSchema(projected, { budgetBytes: budget }),
|
|
)
|
|
: undefined;
|
|
const registeredConfig =
|
|
inputSchema === undefined ? config : { ...config, inputSchema };
|
|
return this.bindTool(name, registeredConfig as never, callback as never);
|
|
}
|
|
|
|
protected bindTool(
|
|
name: string,
|
|
config: StandardToolRegistration,
|
|
callback: ToolCallback<StandardSchemaWithJSON | undefined>,
|
|
): RegisteredTool {
|
|
return super.registerTool(name, config as never, callback as never);
|
|
}
|
|
|
|
/** Preserve the SDK transport lifecycle while retaining oversized errors. */
|
|
override connect(transport: Transport): Promise<void> {
|
|
return super.connect(
|
|
new ErrorEvidenceTransport(
|
|
transport,
|
|
() => this.recordEvidence,
|
|
() => this.delivery,
|
|
this.omitOutputSchemas,
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class ErrorEvidenceTransport implements Transport {
|
|
onclose: Transport["onclose"];
|
|
onerror: Transport["onerror"];
|
|
onmessage: Transport["onmessage"];
|
|
readonly hasPerRequestStream?: boolean;
|
|
private readonly pendingTools = new Map<RequestId, string>();
|
|
private readonly pendingToolLists = new Set<RequestId>();
|
|
private readonly pendingCompletions = new Map<RequestId, AbortController>();
|
|
|
|
private get recordEvidence(): EvidenceWriter["recordEvidence"] | undefined {
|
|
return this.currentEvidenceWriter();
|
|
}
|
|
|
|
private get delivery(): ToolResultDelivery {
|
|
return this.currentDelivery();
|
|
}
|
|
|
|
constructor(
|
|
private readonly transport: Transport,
|
|
private readonly currentEvidenceWriter: () =>
|
|
| EvidenceWriter["recordEvidence"]
|
|
| undefined,
|
|
private readonly currentDelivery: () => ToolResultDelivery,
|
|
private readonly omitOutputSchemas: boolean,
|
|
) {
|
|
if (transport.hasPerRequestStream !== undefined)
|
|
this.hasPerRequestStream = transport.hasPerRequestStream;
|
|
transport.onmessage = (message, extra) => {
|
|
if (
|
|
isJSONRPCRequest(message) &&
|
|
message.method === "tools/call" &&
|
|
typeof message.params?.["name"] === "string"
|
|
)
|
|
this.pendingTools.set(message.id, message.params["name"]);
|
|
if (isJSONRPCRequest(message) && message.method === "tools/list")
|
|
this.pendingToolLists.add(message.id);
|
|
if (isJSONRPCRequest(message) && message.method === "completion/complete")
|
|
this.pendingCompletions.set(message.id, new AbortController());
|
|
// The SDK suppresses replies after cancellation, so send() cannot release
|
|
// this bookkeeping. Keep transport authentication outside retained data.
|
|
if (
|
|
isJSONRPCNotification(message) &&
|
|
message.method === "notifications/cancelled"
|
|
) {
|
|
const id = message.params?.["requestId"];
|
|
if (typeof id === "string" || typeof id === "number")
|
|
forgetPendingMcpRequest(
|
|
this.pendingTools,
|
|
this.pendingToolLists,
|
|
this.pendingCompletions,
|
|
id,
|
|
);
|
|
}
|
|
const completion =
|
|
isJSONRPCRequest(message) && message.method === "completion/complete"
|
|
? this.pendingCompletions.get(message.id)
|
|
: undefined;
|
|
if (completion === undefined) this.onmessage?.(message, extra);
|
|
else
|
|
currentRequestSignal.run(completion.signal, () =>
|
|
this.onmessage?.(message, extra),
|
|
);
|
|
};
|
|
transport.onerror = (error) => this.onerror?.(error);
|
|
transport.onclose = () => {
|
|
this.pendingTools.clear();
|
|
this.pendingToolLists.clear();
|
|
for (const completion of this.pendingCompletions.values())
|
|
completion.abort();
|
|
this.pendingCompletions.clear();
|
|
this.onclose?.();
|
|
};
|
|
}
|
|
|
|
get sessionId(): string | undefined {
|
|
return this.transport.sessionId;
|
|
}
|
|
set sessionId(value: string | undefined) {
|
|
this.transport.sessionId = value;
|
|
}
|
|
|
|
start(): Promise<void> {
|
|
return this.transport.start();
|
|
}
|
|
|
|
async close(): Promise<void> {
|
|
try {
|
|
await this.transport.close();
|
|
} finally {
|
|
this.pendingTools.clear();
|
|
this.pendingToolLists.clear();
|
|
for (const completion of this.pendingCompletions.values())
|
|
completion.abort();
|
|
this.pendingCompletions.clear();
|
|
}
|
|
}
|
|
|
|
setProtocolVersion(version: string): void {
|
|
this.transport.setProtocolVersion?.(version);
|
|
}
|
|
|
|
setSupportedProtocolVersions(versions: string[]): void {
|
|
this.transport.setSupportedProtocolVersions?.(versions);
|
|
}
|
|
|
|
async send(
|
|
message: JSONRPCMessage,
|
|
options?: TransportSendOptions,
|
|
): Promise<void> {
|
|
const id =
|
|
"id" in message &&
|
|
(typeof message.id === "string" || typeof message.id === "number")
|
|
? message.id
|
|
: undefined;
|
|
const tool = id === undefined ? undefined : this.pendingTools.get(id);
|
|
const omitToolSchemas =
|
|
this.omitOutputSchemas &&
|
|
id !== undefined &&
|
|
this.pendingToolLists.has(id);
|
|
try {
|
|
const recovered = this.recoverProtocolError(message, tool);
|
|
const advertised = omitToolSchemas
|
|
? omitOutputSchemasFromToolsList(recovered, id)
|
|
: recovered;
|
|
if (
|
|
isJSONRPCResultResponse(advertised) &&
|
|
isCallToolResult(advertised.result) &&
|
|
advertised.result.isError === true
|
|
) {
|
|
// Keep the complete typed JSON in content. Error projections are not
|
|
// success outputSchema data; oversized recovery must consume their
|
|
// private structured carrier before it is removed from the wire.
|
|
const { structuredContent: _diagnostic, ...result } = advertised.result;
|
|
// Errors the SDK builds itself (input validation, a thrown handler)
|
|
// bypass ToolResultDelivery; mark them for target-content tools too.
|
|
const marked =
|
|
tool !== undefined &&
|
|
toolReturnsTargetContent(tool) &&
|
|
!carriesTargetContentMarker(result)
|
|
? {
|
|
...result,
|
|
_meta: { ...result._meta, ...TARGET_CONTENT_RESULT_META },
|
|
}
|
|
: result;
|
|
await this.transport.send({ ...advertised, result: marked }, options);
|
|
} else await this.transport.send(advertised, options);
|
|
} finally {
|
|
if (!isJSONRPCRequest(message) && id !== undefined) {
|
|
forgetPendingMcpRequest(
|
|
this.pendingTools,
|
|
this.pendingToolLists,
|
|
this.pendingCompletions,
|
|
id,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
private recoverError(
|
|
message: JSONRPCMessage,
|
|
tool: string | undefined,
|
|
): JSONRPCMessage {
|
|
if (
|
|
!isJSONRPCResultResponse(message) ||
|
|
!isCallToolResult(message.result) ||
|
|
message.result.isError !== true
|
|
)
|
|
return message;
|
|
const budget = this.delivery.resultBudgetBytes;
|
|
// Count the provenance marker exactly as the projected error did, and keep
|
|
// it on the replacement: the retained original may hold target content.
|
|
const targetContent =
|
|
carriesTargetContentMarker(message.result) ||
|
|
(tool !== undefined && toolReturnsTargetContent(tool));
|
|
// SDK validation failures have no structured carrier. Size their existing
|
|
// text-only result without serializing the complete JSON-RPC message.
|
|
const isStructured = message.result.structuredContent !== undefined;
|
|
const structured = isStructured
|
|
? jsonObjectSchema.parse(message.result.structuredContent)
|
|
: undefined;
|
|
const sdkResultWithMarker =
|
|
!isStructured &&
|
|
targetContent &&
|
|
!carriesTargetContentMarker(message.result)
|
|
? {
|
|
...message.result,
|
|
_meta: {
|
|
...message.result._meta,
|
|
...TARGET_CONTENT_RESULT_META,
|
|
},
|
|
}
|
|
: 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"])
|
|
: undefined;
|
|
// Retain the complete original response, including SDK validation text.
|
|
// The SDK error contains its rendered issue, not a separate argument bag;
|
|
// transport authentication and caller arguments are never added here.
|
|
const evidence = createEvidence(
|
|
undefined,
|
|
{ id: "rea-mcp", name: "REA MCP delivery", version: "1" },
|
|
{
|
|
operation: "mcp_tool_error",
|
|
parameters: { tool_name: operation },
|
|
result:
|
|
sdkDiagnostic === undefined
|
|
? retainedResult
|
|
: {
|
|
boundary: "sdk-input-validation",
|
|
diagnostic_preview: sdkDiagnostic.slice(0, 256),
|
|
diagnostic_truncated: sdkDiagnostic.length > 256,
|
|
},
|
|
rawResult: originalResult,
|
|
},
|
|
);
|
|
const recorded = this.recordEvidence?.(evidence);
|
|
const retained = recorded !== undefined && recorded.ok;
|
|
const projected = mcpResponseOverflowProjection({
|
|
completed: false,
|
|
constraint: encoded.constraint,
|
|
effectiveResponseBytes: this.delivery.maximumResponseBytes,
|
|
nextStep: retained
|
|
? "Export the retained original error through export_evidence_bundle to a caller-selected path. The connection remains usable."
|
|
: encoded.constraint === "string-length"
|
|
? "The original error could not be retained by this server. Use complete CLI JSON output when the analysis completed. Repeating the unchanged MCP call fails the same way."
|
|
: "The original error could not be retained by this server. Restart REA with a larger REA_MCP_MAX_RESPONSE_BYTES and a matching client receive buffer, then retry.",
|
|
});
|
|
const replacement = this.transportConstraintResult(
|
|
operation,
|
|
projected.reason,
|
|
projected.remediationAction,
|
|
{
|
|
boundary: "mcp-response",
|
|
...(originalError?.success
|
|
? { original_error_code: originalError.data.code }
|
|
: {}),
|
|
result_budget_bytes: budget,
|
|
response_bytes_at_least: encoded.bytesAtLeast,
|
|
constraint: encoded.constraint,
|
|
...(sdkDiagnostic === undefined
|
|
? {}
|
|
: {
|
|
diagnostic_preview: sdkDiagnostic.slice(0, 256),
|
|
diagnostic_truncated: sdkDiagnostic.length > 256,
|
|
}),
|
|
...(retained
|
|
? {
|
|
evidence_reference: {
|
|
kind: "retained-evidence",
|
|
evidence_id: evidence.evidence_id,
|
|
},
|
|
}
|
|
: { retention: recorded === undefined ? "unavailable" : "failed" }),
|
|
...(recorded !== undefined && !recorded.ok
|
|
? { retention_error: projectAnalysisError(recorded.error) }
|
|
: {}),
|
|
},
|
|
targetContent,
|
|
);
|
|
return {
|
|
...message,
|
|
result: replaceToolResultPayload(message.result, replacement),
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Encode a transport-constraint error inside the MCP text budget.
|
|
* The complete SDK diagnostic stays in retained Evidence. Its character
|
|
* preview can still expand past the budget when quotes and backslashes are
|
|
* escaped into that text, so shorten only the preview and keep the reference.
|
|
*/
|
|
private transportConstraintResult(
|
|
operation: string,
|
|
reason: string,
|
|
remediationAction: string,
|
|
limits: Readonly<Record<string, JsonValue>>,
|
|
targetContent: boolean,
|
|
): CallToolResult {
|
|
const deliver = (
|
|
reportedLimits: Readonly<Record<string, JsonValue>>,
|
|
): CallToolResult =>
|
|
this.delivery.toErrorToolResult(
|
|
new AnalysisResourceConstraintError(
|
|
operation,
|
|
"transport",
|
|
reason,
|
|
reportedLimits,
|
|
{ remediationAction },
|
|
),
|
|
targetContent,
|
|
);
|
|
const delivered = deliver(limits);
|
|
const preview = limits.diagnostic_preview;
|
|
if (delivered.content.length > 0 || typeof preview !== "string")
|
|
return delivered;
|
|
let low = 0;
|
|
let high = preview.length - 1;
|
|
let fitted: CallToolResult | undefined;
|
|
let fittedLength = -1;
|
|
while (low <= high) {
|
|
const length = Math.floor((low + high) / 2);
|
|
const candidate = deliver({
|
|
...limits,
|
|
diagnostic_preview: preview.slice(0, length),
|
|
diagnostic_truncated: true,
|
|
});
|
|
if (candidate.content.length > 0) {
|
|
fitted = candidate;
|
|
fittedLength = length;
|
|
low = length + 1;
|
|
} else high = length - 1;
|
|
}
|
|
if (fitted !== undefined && fittedLength > 0) return fitted;
|
|
const { diagnostic_preview: _preview, ...withoutPreview } = limits;
|
|
const omitted = deliver({
|
|
...withoutPreview,
|
|
diagnostic_truncated: preview.length > 0,
|
|
});
|
|
return omitted.content.length > 0 || fitted === undefined
|
|
? omitted
|
|
: fitted;
|
|
}
|
|
|
|
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.",
|
|
},
|
|
};
|
|
}
|
|
}
|