Files
reverse-engineering-anything/src/server/ToolResultDelivery.ts
T

261 lines
9.5 KiB
TypeScript

import type { CallToolResult } from "@modelcontextprotocol/server";
import { STDIO_DEFAULT_MAX_BUFFER_SIZE } from "@modelcontextprotocol/server";
import type { ToolContract } from "../contracts/toolContractTypes.js";
import type { EvidenceWriter } from "../application/investigation/InvestigationRecordPort.js";
import type { Evidence } from "../domain/evidence.js";
import { safeParseEvidence } from "../domain/evidenceValidation.js";
import {
projectAnalysisError,
type AnalysisErrorProjection,
} from "../domain/analysisErrorProjection.js";
import type { AnalysisError } from "../domain/analysisErrorBase.js";
import { jsonValueSchema, type JsonValue } from "../domain/jsonValue.js";
import type { Result } from "../domain/result.js";
import { AnalysisResourceConstraintError } from "../domain/analysisErrorCore.js";
import {
MCP_TRANSPORT_EVIDENCE_DISCOVERY,
MCP_TRANSPORT_EVIDENCE_EXPORT,
} from "../domain/analysisErrorPresentation.js";
import {
nodeStringCeilingRecovery,
stdioResponseBudgetRecovery,
} from "../config/mcpResponseBudget.js";
import {
encodeToolResult,
MCP_RESULT_STRING_LIMIT,
NODE_MAX_STRING_LENGTH,
TARGET_CONTENT_RESULT_META,
} from "./toolResultEncoding.js";
/** Project a response-budget refusal without labeling REA's reserved string limit as Node's ceiling. */
export const mcpResponseOverflowProjection = (input: {
readonly completed: boolean;
readonly constraint: "receive-buffer" | "string-length";
readonly effectiveResponseBytes: number;
readonly nextStep: string;
}): { readonly reason: string; readonly remediationAction: string } => {
const facts =
input.constraint === "string-length"
? nodeStringCeilingRecovery(
MCP_RESULT_STRING_LIMIT,
NODE_MAX_STRING_LENGTH,
)
: stdioResponseBudgetRecovery(
input.effectiveResponseBytes,
MCP_RESULT_STRING_LIMIT,
NODE_MAX_STRING_LENGTH,
);
const summary =
input.constraint === "string-length"
? input.completed
? "The operation completed, but its complete MCP response exceeds REA's reserved single-string limit."
: "The tool failed, but its complete error response exceeds REA's reserved single-string limit."
: input.completed
? "The operation completed, but its complete MCP response exceeds the stdio response budget. The analysis result was not truncated."
: "The tool failed, but its complete error response exceeds the MCP response budget.";
return {
reason: `${summary} ${facts} ${input.nextStep}`,
remediationAction: `${input.nextStep} ${facts}`,
};
};
/**
* A summary projection has already used `detail: "summary"`.
* Export still writes the retained bundle outside one MCP frame.
*/
const evidenceBundleSummary = (value: JsonValue): boolean => {
const payload =
typeof value === "object" &&
value !== null &&
!Array.isArray(value) &&
"result" in value
? value.result
: value;
return (
typeof payload === "object" &&
payload !== null &&
!Array.isArray(payload) &&
payload.kind === "evidence-bundle-summary"
);
};
/** Immutable encoding and recovery policy shared by one MCP server. */
export class ToolResultDelivery {
readonly maximumResponseBytes: number;
readonly resultBudgetBytes: number;
constructor(
maximumResponseBytes: number,
private readonly nativeContinuation: (
evidence: Evidence,
) => Readonly<Record<string, JsonValue>> = () => ({}),
) {
this.maximumResponseBytes = maximumResponseBytes;
this.resultBudgetBytes = maximumResponseBytes - 1024;
Object.freeze(this);
}
/** Serialize a plain application result or its actionable error. */
toCallToolResult(
result: Result<JsonValue, AnalysisError>,
contract: ToolContract,
recovery: Readonly<Record<string, JsonValue>> = {},
): CallToolResult {
return result.ok
? this.successResult(
contract.kind === "session" ? { result: result.value } : result.value,
contract,
recovery,
)
: this.toErrorToolResult(result.error, contract.returnsTargetContent);
}
/** Deliver Evidence using the producer's explicit recording acknowledgment. */
toEvidenceToolResult(
evidence: Evidence,
contract: ToolContract,
recorded: Result<unknown, AnalysisError> | undefined,
): CallToolResult {
return recorded !== undefined && !recorded.ok
? this.toErrorToolResult(recorded.error, contract.returnsTargetContent)
: this.successResult(evidence, contract, {
evidence_id: evidence.evidence_id,
...(recorded === undefined
? {}
: {
evidence_reference: {
kind: "retained-evidence",
evidence_id: evidence.evidence_id,
},
...this.nativeContinuation(evidence),
}),
});
}
/**
* Project an error without allocating oversized repeated MCP text.
* `targetContent` is the failing tool's `returnsTargetContent`: a failure
* can carry target-derived text (a message, captured output, or a partial
* observation), so it is marked exactly like that tool's successful results.
*/
toErrorToolResult(
error: AnalysisError,
targetContent: boolean,
): CallToolResult {
return this.projectedErrorResult(
projectAnalysisError(error),
targetContent,
);
}
/** Retain valid partial Evidence and reference it only after recording succeeds. */
toPartialErrorToolResult(
error: AnalysisError,
targetContent: boolean,
recordEvidence?: EvidenceWriter["recordEvidence"],
): CallToolResult {
if (recordEvidence === undefined)
return this.toErrorToolResult(error, targetContent);
const partial = safeParseEvidence(error.partialObservation);
if (!partial.ok) return this.toErrorToolResult(error, targetContent);
const recorded = recordEvidence(partial.value);
if (!recorded.ok) return this.toErrorToolResult(error, targetContent);
const projected = projectAnalysisError(error);
return this.projectedErrorResult(
{
...projected,
details: {
...projected.details,
partial_observation: {
kind: "retained-evidence",
evidence_id: partial.value.evidence_id,
},
},
},
targetContent,
);
}
private projectedErrorResult(
projected: AnalysisErrorProjection,
targetContent: boolean,
): CallToolResult {
const structuredContent = { error: projected };
// Errors reach the wire as text only; the structured copy is a private
// carrier the transport removes after its own budget check.
const encoded = encodeToolResult(
structuredContent,
this.resultBudgetBytes,
"error-text",
targetContent,
);
// The transport retains an oversized projected error before replacing it
// with a recoverable delivery constraint, using this same budget.
return {
content: encoded.ok ? [{ type: "text", text: encoded.text }] : [],
structuredContent,
isError: true,
...(targetContent ? { _meta: TARGET_CONTENT_RESULT_META } : {}),
};
}
private successResult(
value: JsonValue,
contract: ToolContract,
recovery: Readonly<Record<string, JsonValue>> = {},
): CallToolResult {
const encoded = encodeToolResult(
value,
this.resultBudgetBytes,
"structured-and-text",
contract.returnsTargetContent,
);
if (encoded.ok)
return {
content: [{ type: "text", text: encoded.text }],
structuredContent: value,
...(contract.returnsTargetContent
? { _meta: TARGET_CONTENT_RESULT_META }
: {}),
};
const retainedEvidence = recovery.evidence_reference !== undefined;
const nextStep =
recovery.committed_unknown !== undefined
? "Export this session with export_evidence_bundle to inspect the committed unknown and its mutation Evidence, or page list_unknowns using the reported identity. Do not repeat the completed mutation."
: recovery.continuation !== undefined
? "Call inspect_analysis_view with the supplied continuation arguments to read retained analysis. Follow coverage.next_offset for subsequent pages; reduce the page limit if needed, or export the complete retained Evidence."
: retainedEvidence
? "Export retained evidence to consume the complete analysis, or use complete CLI JSON output."
: contract.name === "get_evidence_bundle"
? evidenceBundleSummary(value)
? MCP_TRANSPORT_EVIDENCE_EXPORT
: MCP_TRANSPORT_EVIDENCE_DISCOVERY
: "Use complete CLI JSON output to consume the analysis.";
const projected = mcpResponseOverflowProjection({
completed: true,
constraint: encoded.constraint,
effectiveResponseBytes: this.maximumResponseBytes,
nextStep,
});
return this.toErrorToolResult(
new AnalysisResourceConstraintError(
contract.name,
"transport",
projected.reason,
{
boundary: "mcp-response",
default_receive_buffer_bytes: STDIO_DEFAULT_MAX_BUFFER_SIZE,
result_budget_bytes: this.resultBudgetBytes,
max_string_code_units: MCP_RESULT_STRING_LIMIT,
response_bytes_at_least: encoded.bytesAtLeast,
response_code_units_at_least: encoded.codeUnitsAtLeast,
constraint: encoded.constraint,
...recovery,
},
{ remediationAction: projected.remediationAction },
),
contract.returnsTargetContent,
);
}
}