mirror of
https://github.com/morluto/rea.git
synced 2026-10-11 21:39:55 +00:00
fix(mcp): provide native evidence paging continuation
Supply schema-validated inspect_analysis_view arguments after completed native function Evidence is recorded and exceeds MCP delivery capacity. Preserve the full canonical result and its provenance. Verify exact source paging, export and following-call usability through real Ghidra stdio MCP. Fixes #1849 Co-authored-by: Minghang Chamling <[email protected]> Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com> Co-authored-by: Kaoru0822 <[email protected]>
This commit is contained in:
co-authored by
factory-droid[bot]
Kaoru0822
parent
7cdb7981c3
commit
1449e8e051
+12
-2
@@ -328,8 +328,18 @@ session. Its exact reference is reported in
|
||||
`details.reported_limits.evidence_reference`; call `inspect_analysis_view` with
|
||||
that reference for a summary, one section/module, or a stable page, or call
|
||||
`export_evidence_bundle` with a destination path. Complete
|
||||
bundle exports stream canonical JSON into an atomically published file. A broad
|
||||
follow-up or `get_evidence_bundle` can also exceed the response budget; exporting
|
||||
bundle exports stream canonical JSON into an atomically published file.
|
||||
|
||||
For oversized native function Evidence, `details.reported_limits.continuation`
|
||||
contains a ready-to-call `inspect_analysis_view` request for the retained
|
||||
pseudocode's first page. Use its `name` and `arguments` directly, then follow
|
||||
`normalized_result.coverage.next_offset` for subsequent pages. The suggested
|
||||
limit is the view contract's default, not a guarantee that arbitrary source or
|
||||
metadata fits; reduce the limit or export when needed. Continuations are
|
||||
offered for completed `analyze_function` results only after session recording is
|
||||
acknowledged; the complete result and its retention reference remain intact.
|
||||
|
||||
A broad follow-up or `get_evidence_bundle` can also exceed the response budget; exporting
|
||||
preserves the complete session without sending it through a single MCP frame.
|
||||
To discover existing records without their large payloads, call
|
||||
`get_evidence_bundle` with `detail: "summary"`. Its `result.records` contain
|
||||
|
||||
@@ -3,12 +3,16 @@ import { STDIO_DEFAULT_MAX_BUFFER_SIZE } from "@modelcontextprotocol/server";
|
||||
|
||||
import type { ToolContract } from "../contracts/toolContractTypes.js";
|
||||
import type { Evidence } from "../domain/evidence.js";
|
||||
import {
|
||||
INSPECT_ANALYSIS_VIEW_OPERATION,
|
||||
inspectAnalysisViewInputSchema,
|
||||
} from "../domain/analysisView/analysisView.js";
|
||||
import {
|
||||
projectAnalysisError,
|
||||
type AnalysisErrorProjection,
|
||||
} from "../domain/analysisErrorProjection.js";
|
||||
import type { AnalysisError } from "../domain/analysisErrorBase.js";
|
||||
import type { JsonValue } from "../domain/jsonValue.js";
|
||||
import { jsonValueSchema, type JsonValue } from "../domain/jsonValue.js";
|
||||
import type { Result } from "../domain/result.js";
|
||||
import { AnalysisResourceConstraintError } from "../domain/analysisErrorCore.js";
|
||||
import {
|
||||
@@ -22,6 +26,24 @@ export const createToolResultDelivery = (
|
||||
): ToolResultDelivery =>
|
||||
new ToolResultDelivery(maximumResponseBytes ?? STDIO_DEFAULT_MAX_BUFFER_SIZE);
|
||||
|
||||
/** Offer a named, schema-validated source page only for retained native analysis. */
|
||||
const nativeEvidenceContinuation = (
|
||||
evidence: Evidence,
|
||||
): Readonly<Record<string, JsonValue>> => {
|
||||
if (evidence.operation !== "analyze_function") return {};
|
||||
const input = inspectAnalysisViewInputSchema.safeParse({
|
||||
source: { kind: "retained-evidence", evidence_id: evidence.evidence_id },
|
||||
view: { kind: "native", facet: "pseudocode", offset: 0 },
|
||||
});
|
||||
if (!input.success) return {};
|
||||
return {
|
||||
continuation: {
|
||||
name: INSPECT_ANALYSIS_VIEW_OPERATION,
|
||||
arguments: jsonValueSchema.parse(input.data),
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
/** Immutable encoding and recovery policy shared by one MCP server. */
|
||||
export class ToolResultDelivery {
|
||||
readonly resultBudgetBytes: number;
|
||||
@@ -61,6 +83,7 @@ export class ToolResultDelivery {
|
||||
kind: "retained-evidence",
|
||||
evidence_id: evidence.evidence_id,
|
||||
},
|
||||
...nativeEvidenceContinuation(evidence),
|
||||
}),
|
||||
});
|
||||
}
|
||||
@@ -122,11 +145,14 @@ export class ToolResultDelivery {
|
||||
structuredContent: value,
|
||||
};
|
||||
const retainedEvidence = recovery.evidence_reference !== undefined;
|
||||
const recoveryAdvice = retainedEvidence
|
||||
? "Export retained evidence to consume the complete analysis, or use complete CLI JSON output."
|
||||
: encoded.constraint === "string-length"
|
||||
? "Use complete CLI JSON output to consume the analysis."
|
||||
: "Use a larger MCP response budget with a matching client receive buffer, or use complete CLI JSON output to consume the analysis.";
|
||||
const recoveryAdvice =
|
||||
recovery.continuation !== undefined
|
||||
? "Call inspect_analysis_view with the supplied continuation arguments to read retained pseudocode. 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."
|
||||
: encoded.constraint === "string-length"
|
||||
? "Use complete CLI JSON output to consume the analysis."
|
||||
: "Use a larger MCP response budget with a matching client receive buffer, or use complete CLI JSON output to consume the analysis.";
|
||||
return this.toErrorToolResult(
|
||||
new AnalysisResourceConstraintError(
|
||||
contract.name,
|
||||
@@ -144,6 +170,9 @@ export class ToolResultDelivery {
|
||||
constraint: encoded.constraint,
|
||||
...recovery,
|
||||
},
|
||||
recovery.continuation === undefined
|
||||
? undefined
|
||||
: { remediationAction: recoveryAdvice },
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { parseMcpToolError } from "../../fixtures/mcpToolError.js";
|
||||
import { Client, InMemoryTransport } from "@modelcontextprotocol/client";
|
||||
import { STDIO_DEFAULT_MAX_BUFFER_SIZE } from "@modelcontextprotocol/server";
|
||||
import { Ajv2020 } from "ajv/dist/2020.js";
|
||||
import { expect, it, onTestFinished } from "vitest";
|
||||
import { z } from "zod";
|
||||
@@ -17,6 +18,7 @@ import {
|
||||
createJavaScriptApplicationNode,
|
||||
} from "../../../src/domain/javascript/javascriptApplicationGraph.js";
|
||||
import { createServer } from "../../../src/server/createServer.js";
|
||||
import { connectGhidraMcp } from "./ghidraMcpHarness.js";
|
||||
import {
|
||||
analysisViewJavaScriptEvidence,
|
||||
analysisViewJavaScriptAnalysisWithSource,
|
||||
@@ -455,6 +457,95 @@ it("retains an oversized native assembly row and permits a subsequent smaller vi
|
||||
await client.ping();
|
||||
});
|
||||
|
||||
it("reads successive native source pages directly from a completed-result transport error", async () => {
|
||||
const pseudocode = "int fixture_add(int a, int b) { return a + b; }\n".repeat(
|
||||
150_000,
|
||||
);
|
||||
const harness = await connectGhidraMcp(
|
||||
"native-continuation",
|
||||
async (operation) => {
|
||||
if (operation !== "analyze_function")
|
||||
throw new Error(`Unexpected provider operation: ${operation}`);
|
||||
return ok({
|
||||
value: {
|
||||
...functionDossierSchema.parse(ghidraFunctionDossier()),
|
||||
pseudocode,
|
||||
},
|
||||
limitations: [],
|
||||
});
|
||||
},
|
||||
);
|
||||
onTestFinished(() => harness.close());
|
||||
const { mcp: client, session, calls } = harness;
|
||||
const failed = await client.callTool({
|
||||
name: "analyze_function",
|
||||
arguments: { procedure: "0x401000" },
|
||||
});
|
||||
expect(failed.isError).toBe(true);
|
||||
const limits = z
|
||||
.object({
|
||||
error: z.object({
|
||||
details: z.object({ reported_limits: jsonObjectSchema }),
|
||||
}),
|
||||
})
|
||||
.parse(parseMcpToolError(failed)).error.details.reported_limits;
|
||||
const reference = z
|
||||
.object({
|
||||
kind: z.literal("retained-evidence"),
|
||||
evidence_id: z.string(),
|
||||
})
|
||||
.parse(limits.evidence_reference);
|
||||
const parent = parseEvidence(session.evidenceById(reference.evidence_id));
|
||||
expect(parent.operation).toBe("analyze_function");
|
||||
expect(parent.parameters).toMatchObject({ procedure: "0x401000" });
|
||||
const call = z
|
||||
.object({
|
||||
name: z.literal("inspect_analysis_view"),
|
||||
arguments: toolContract("inspect_analysis_view").inputSchema,
|
||||
})
|
||||
.parse(limits.continuation);
|
||||
expect(call.arguments.source).toEqual({
|
||||
kind: "retained-evidence",
|
||||
evidence_id: parent.evidence_id,
|
||||
});
|
||||
const first = await client.callTool(call);
|
||||
expect(first.isError).not.toBe(true);
|
||||
const page = toolContract("inspect_analysis_view").outputSchema.parse(
|
||||
first.structuredContent,
|
||||
).normalized_result;
|
||||
expect(page).toMatchObject({
|
||||
parent_evidence_id: parent.evidence_id,
|
||||
item: { text: pseudocode.slice(0, 64) },
|
||||
coverage: { examined: 64, next_offset: 64, exhausted: false },
|
||||
});
|
||||
expect(Buffer.byteLength(JSON.stringify(first))).toBeLessThan(
|
||||
STDIO_DEFAULT_MAX_BUFFER_SIZE,
|
||||
);
|
||||
const second = await client.callTool({
|
||||
name: call.name,
|
||||
arguments: {
|
||||
...call.arguments,
|
||||
view: {
|
||||
kind: "native",
|
||||
facet: "pseudocode",
|
||||
offset: page.coverage.next_offset,
|
||||
limit: 64,
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(second.isError).not.toBe(true);
|
||||
expect(
|
||||
toolContract("inspect_analysis_view").outputSchema.parse(
|
||||
second.structuredContent,
|
||||
).normalized_result,
|
||||
).toMatchObject({ item: { text: pseudocode.slice(64, 128) } });
|
||||
expect(
|
||||
session.evidenceById(parent.evidence_id)?.normalized_result,
|
||||
).toMatchObject({ pseudocode });
|
||||
await client.ping();
|
||||
expect(calls).toEqual(["analyze_function"]);
|
||||
});
|
||||
|
||||
it("returns a JavaScript summary without graph payloads from a retained reference", async () => {
|
||||
const { client, session } = await connect();
|
||||
const parent = analysisViewJavaScriptEvidence();
|
||||
|
||||
Reference in New Issue
Block a user