fix(mcp): preserve migrated revisions and valid links

This commit is contained in:
morluto
2026-07-14 02:07:44 +00:00
parent d092d4c97a
commit 4149472439
8 changed files with 52 additions and 14 deletions
+2 -2
View File
@@ -398,8 +398,8 @@ const migrateLegacyWorkspace = (input: unknown): InvestigationWorkspace => {
const migrated = buildWorkspace({
workspaceId: parsed.workspace_id,
name: parsed.name,
revision: parsed.revision + 1,
previousRevisionDigest: parsed.revision_digest,
revision: parsed.revision,
previousRevisionDigest: parsed.previous_revision_digest,
bundle,
runs: runs.map((run) => ({
...run,
+3 -1
View File
@@ -116,7 +116,9 @@ const registerEnhancedTool = (
recordUnknown: registration.recordUnknown,
});
if (!unknowns.ok) return toCallToolResult(unknowns, contract);
return toCallToolResult({ ok: true, value: evidence }, contract);
return toCallToolResult({ ok: true, value: evidence }, contract, {
evidenceResourcesAvailable: recorded !== undefined,
});
}
return toCallToolResult(result, contract);
},
+3 -1
View File
@@ -102,7 +102,9 @@ export const registerEvidenceTools = (
const recorded = options.recordEvidence?.(evidence);
return recorded !== undefined && !recorded.ok
? toCallToolResult(recorded, contract)
: toCallToolResult({ ok: true, value: evidence }, contract);
: toCallToolResult({ ok: true, value: evidence }, contract, {
evidenceResourcesAvailable: recorded !== undefined,
});
},
);
}
+3 -1
View File
@@ -103,7 +103,9 @@ const registerOfficialTool = (
const recorded = registration.recordEvidence?.(evidence);
if (recorded !== undefined && !recorded.ok)
return toCallToolResult(recorded, contract);
return toCallToolResult({ ok: true, value: evidence }, contract);
return toCallToolResult({ ok: true, value: evidence }, contract, {
evidenceResourcesAvailable: recorded !== undefined,
});
}
if (
result.error instanceof AnalysisCapabilityUnavailableError &&
+19 -3
View File
@@ -9,6 +9,10 @@ import type { Result } from "../domain/result.js";
import type { JsonValue } from "../domain/jsonValue.js";
import type { ToolContract } from "../contracts/toolContracts.js";
interface ToolResultOptions {
readonly evidenceResourcesAvailable?: boolean;
}
/**
* Translate an application result into MCP text content.
* Error tags and safe messages remain visible while underlying causes, process
@@ -17,8 +21,11 @@ import type { ToolContract } from "../contracts/toolContracts.js";
export const toCallToolResult = (
result: Result<JsonValue, AnalysisError>,
contract: ToolContract,
options: ToolResultOptions = {},
): CallToolResult =>
result.ok ? successResult(result.value, contract) : errorResult(result.error);
result.ok
? successResult(result.value, contract, options)
: errorResult(result.error);
const errorResult = (error: AnalysisError): CallToolResult => {
const projected = projectAnalysisError(error);
@@ -37,6 +44,7 @@ const errorResult = (error: AnalysisError): CallToolResult => {
const successResult = (
value: JsonValue,
contract: ToolContract,
options: ToolResultOptions,
): CallToolResult => {
const candidate = contract.kind === "session" ? { result: value } : value;
const parsed = contract.outputSchema.safeParse(candidate);
@@ -62,14 +70,22 @@ const successResult = (
return {
content: [
{ type: "text", text: formatValue(value) },
...evidenceResourceLinks(value),
...evidenceResourceLinks(
value,
contract.kind === "session" ||
options.evidenceResourcesAvailable === true,
),
],
structuredContent: candidate,
};
};
const evidenceResourceLinks = (value: JsonValue): CallToolResult["content"] => {
const evidenceResourceLinks = (
value: JsonValue,
available: boolean,
): CallToolResult["content"] => {
if (
!available ||
typeof value !== "object" ||
value === null ||
Array.isArray(value) ||
+7 -5
View File
@@ -155,8 +155,8 @@ describe("persistent cross-version investigation workspace", () => {
});
expect(migrated).toMatchObject({
revision: current.revision + 1,
previous_revision_digest: legacyDigest,
revision: current.revision,
previous_revision_digest: current.previous_revision_digest,
runs: [
{
legacy_request_identity: true,
@@ -174,9 +174,11 @@ describe("persistent cross-version investigation workspace", () => {
throw new TypeError("legacy fixture is not canonical");
await writeFile(input.workspace_path, encodedLegacy, { mode: 0o600 });
await expect(
runCrossVersionInvestigation(input, policy(directory), {
inputRoots: [directory],
}),
runCrossVersionInvestigation(
{ ...input, expected_workspace_revision: current.revision },
policy(directory),
{ inputRoots: [directory] },
),
).resolves.toMatchObject({ ok: true, value: { reused: true } });
await expect(
runCrossVersionInvestigation(
+5
View File
@@ -164,6 +164,11 @@ describe("official Hopper proxy tools", () => {
arguments: VALID_INPUTS[contract.name],
});
expect(result.isError).not.toBe(true);
expect(result.content).not.toEqual(
expect.arrayContaining([
expect.objectContaining({ type: "resource_link" }),
]),
);
}
expect(invocations.map(({ name }) => name)).toEqual(
+10 -1
View File
@@ -61,7 +61,7 @@ describe("tool result projection", () => {
});
});
it("links successful evidence to its session resource URI", () => {
it("links successful evidence only when its session resource exists", () => {
const evidence = createEvidence(
undefined,
{ id: "fixture", name: "Fixture", version: "1" },
@@ -79,6 +79,15 @@ describe("tool result projection", () => {
expect(
toCallToolResult(ok(evidence), evidenceContract).content,
).not.toEqual(
expect.arrayContaining([
expect.objectContaining({ type: "resource_link" }),
]),
);
expect(
toCallToolResult(ok(evidence), evidenceContract, {
evidenceResourcesAvailable: true,
}).content,
).toContainEqual(
expect.objectContaining({
type: "resource_link",