feat(mcp): page retained incoming reference addresses (#8591)

This commit is contained in:
morluto authored and GitHub committed 2026-11-03 02:13:21 +08:00
1 parent 75c3cab540
commit 55078b9f57
8 files changed
+97 -50

No files matched your search

+6
View File
@@ -292,6 +292,12 @@ limitation and reason IDs index that page's `items.messages` dictionary. Expand
those references to recover exact text, repeated messages, and absent versus
empty optional fields. Failed nodes retain their original error.
Retained `xrefs` Evidence also supports `native-observation` address pages and
`native-context` source caveats. `xrefs` still returns complete Evidence by
default and does not accept `detail`; when its MCP response exceeds the budget,
follow the returned continuation to page the retained result. These addresses
remain untyped; paging does not add reference kinds or establish completeness.
These views reference complete source context through `source_context.digest`
and caveat counts. Read it using the same parent with
`view: {"kind":"native-context","facet":"overview"}`. The other facets are
@@ -121,53 +121,56 @@ const graph = (): NativeCallGraph =>
limitations: ["Graph caveat", "Graph caveat"],
});
it("returns useful caller/callee facts with exact source context identity and partial coverage", async () => {
const source = parent("procedure_callees", [
"ram:0x0010",
"ram:0x0010",
"ram:0x0020",
]);
const before = JSON.stringify(source);
const summary = await project(source, { kind: "summary" });
expect(summary.value).toMatchObject({
kind: "summary",
parent_evidence_id: source.evidence_id,
source_provider: source.provider,
source_parameters: source.parameters,
source_analysis_profile_digest: source.analysis_profile?.digest,
source_context: {
evidence_limitation_count: 1,
result_limitation_count: null,
},
summary: { observations: 3 },
items: {
type: "addresses",
rows: [
{ source_index: 0, address: "ram:0x0010" },
{ source_index: 1, address: "ram:0x0010" },
{ source_index: 2, address: "ram:0x0020" },
],
},
});
expect(summary.evidence.relationship_coverage).toEqual(
source.relationship_coverage,
);
const page = await project(source, {
kind: "native-observation",
offset: 1,
limit: 1,
});
expect(page.value).toMatchObject({
items: { rows: [{ source_index: 1, address: "ram:0x0010" }] },
coverage: { total: 3, next_offset: 2, exhausted: false },
});
expect(page.value).toHaveProperty(
"source_context.digest",
contextDigest(summary.value),
);
expect(JSON.stringify(summary.value)).not.toContain('"analysis_profile":');
expect(JSON.stringify(source)).toBe(before);
});
it.each(["procedure_callees", "xrefs"])(
"pages %s addresses with exact source context and duplicate identity",
async (operation) => {
const source = parent(operation, [
"ram:0x0010",
"ram:0x0010",
"ram:0x0020",
]);
const before = JSON.stringify(source);
const summary = await project(source, { kind: "summary" });
expect(summary.value).toMatchObject({
kind: "summary",
parent_evidence_id: source.evidence_id,
source_provider: source.provider,
source_parameters: source.parameters,
source_analysis_profile_digest: source.analysis_profile?.digest,
source_context: {
evidence_limitation_count: 1,
result_limitation_count: null,
},
summary: { observations: 3 },
items: {
type: "addresses",
rows: [
{ source_index: 0, address: "ram:0x0010" },
{ source_index: 1, address: "ram:0x0010" },
{ source_index: 2, address: "ram:0x0020" },
],
},
});
expect(summary.evidence.relationship_coverage).toEqual(
source.relationship_coverage,
);
const page = await project(source, {
kind: "native-observation",
offset: 1,
limit: 1,
});
expect(page.value).toMatchObject({
items: { rows: [{ source_index: 1, address: "ram:0x0010" }] },
coverage: { total: 3, next_offset: 2, exhausted: false },
});
expect(page.value).toHaveProperty(
"source_context.digest",
contextDigest(summary.value),
);
expect(JSON.stringify(summary.value)).not.toContain('"analysis_profile":');
expect(JSON.stringify(source)).toBe(before);
},
);
it("pages complete context fields and changes its digest when source profile or caveats change", async () => {
const source = parent("procedure_callers", []);
+1 -1
View File
@@ -260,7 +260,7 @@ export const OFFICIAL_TOOL_CONTRACTS = [
),
official(
"xrefs",
"Return analyzed references to a code or data address. Hopper may default to its current cursor; headless providers such as Ghidra require an explicit address. Use to connect strings, globals, selectors, and functions; bare addresses are untyped and indirect references may be incomplete. Use inspect_native_instruction at the target address for typed incoming references where supported.",
"Return analyzed references to a code or data address. Hopper may default to its current cursor; headless providers such as Ghidra require an explicit address. Use to connect strings, globals, selectors, and functions; bare addresses are untyped and indirect references may be incomplete. Page a retained result with inspect_analysis_view and native-observation; use native-context for source caveats. Use inspect_native_instruction at the target address for typed incoming references where supported.",
z.object({ document, address: optionalAddress }),
),
] as const satisfies readonly ToolContract[];
@@ -382,6 +382,7 @@ export const nativeObservationSummaryViewResultSchemas = {
procedure_address: nativeObservationSummaryFor("procedure_address"),
procedure_callees: nativeObservationSummaryFor("procedure_callees"),
procedure_callers: nativeObservationSummaryFor("procedure_callers"),
xrefs: nativeObservationSummaryFor("xrefs"),
resolve_containing_procedure: nativeObservationSummaryFor(
"resolve_containing_procedure",
),
@@ -393,6 +394,7 @@ const nativeObservationSummaryViewResultSchema = z.discriminatedUnion(
nativeObservationSummaryViewResultSchemas.procedure_address,
nativeObservationSummaryViewResultSchemas.procedure_callees,
nativeObservationSummaryViewResultSchemas.procedure_callers,
nativeObservationSummaryViewResultSchemas.xrefs,
nativeObservationSummaryViewResultSchemas.resolve_containing_procedure,
nativeObservationSummaryViewResultSchemas.get_call_graph,
],
@@ -412,6 +414,7 @@ const nativeObservationPageViewResultSchema = z.discriminatedUnion(
nativeObservationPageFor("procedure_address"),
nativeObservationPageFor("procedure_callees"),
nativeObservationPageFor("procedure_callers"),
nativeObservationPageFor("xrefs"),
nativeObservationPageFor("resolve_containing_procedure"),
nativeObservationPageFor("get_call_graph"),
],
@@ -158,6 +158,7 @@ export const nativeObservationItemsSchemas = {
procedure_address: addresses,
procedure_callees: addresses,
procedure_callers: addresses,
xrefs: addresses,
resolve_containing_procedure: resolutions,
get_call_graph: nativeGraphViewItemsSchema,
};
@@ -45,7 +45,8 @@ const readSource = (
): Result<NativeObservation, AnalysisError> => {
switch (parent.operation) {
case "procedure_callees":
case "procedure_callers": {
case "procedure_callers":
case "xrefs": {
const read = readAddresses(parent);
return read.ok
? ok({ operation: parent.operation, result: read.value })
@@ -138,6 +139,7 @@ const observationRows = (
};
case "procedure_callees":
case "procedure_callers":
case "xrefs":
return {
parent_operation: source.operation,
total: source.result.length,
+2
View File
@@ -66,6 +66,7 @@ export const NATIVE_PRIMITIVE_OBSERVATION_OPERATIONS = [
export const NATIVE_OBSERVATION_OPERATIONS = [
...NATIVE_PRIMITIVE_OBSERVATION_OPERATIONS,
"get_call_graph",
"xrefs",
] as const;
export type NativeObservationOperation =
(typeof NATIVE_OBSERVATION_OPERATIONS)[number];
@@ -74,6 +75,7 @@ export const nativeObservationSourceSchemas = {
procedure_address: z.string(),
procedure_callees: nativeAddressListSchema,
procedure_callers: nativeAddressListSchema,
xrefs: nativeAddressListSchema,
resolve_containing_procedure: containingProcedureResolutionSchema,
get_call_graph: nativeCallGraphSchema,
};
@@ -119,6 +119,33 @@ it("delivers native primitive and graph summaries with exact retained parents an
);
expect(rejected.isError).toBe(true);
}
const references = await call("xrefs", { address: "0x401000" });
const source = {
kind: "retained-evidence",
evidence_id: references.evidence_id,
};
const before = harness.calls.length;
const page = await call("inspect_analysis_view", {
source,
view: { kind: "native-observation", offset: 0, limit: 1 },
});
expect(page.normalized_result).toMatchObject({
parent_evidence_id: references.evidence_id,
parent_operation: "xrefs",
items: {
type: "addresses",
rows: [{ source_index: 0, address: "0x401001" }],
},
coverage: { total: 1, exhausted: true },
});
const caveats = await call("inspect_analysis_view", {
source,
view: { kind: "native-context", facet: "limitations" },
});
expect(caveats.normalized_result).toMatchObject({
parent_operation: "xrefs",
});
expect(harness.calls).toHaveLength(before);
} finally {
await harness.close();
}
@@ -177,9 +204,12 @@ it("follows native overflow continuations through MCP without repeating provider
for (const name of [
...NATIVE_PRIMITIVE_OBSERVATION_OPERATIONS,
"get_call_graph",
"xrefs",
] as const) {
const input =
name === "get_call_graph" || name === "resolve_containing_procedure"
name === "get_call_graph" ||
name === "resolve_containing_procedure" ||
name === "xrefs"
? { address: "0x1000" }
: { procedure: "fixture_main" };
const complete = await client.callTool({ name, arguments: input });