198 lines
7.3 KiB
TypeScript
198 lines
7.3 KiB
TypeScript
import type { McpServer } from "@modelcontextprotocol/server";
|
|
|
|
import type { BinarySessionPort } from "../application/BinarySession.js";
|
|
import {
|
|
compareApplicationVersionsEvidence,
|
|
traceApplicationFeatureEvidence,
|
|
} from "../application/JavaScriptApplicationWorkflowService.js";
|
|
import {
|
|
runControlledReplay,
|
|
type JavaScriptReplayDependencies,
|
|
} from "../application/JavaScriptReplayService.js";
|
|
import { APPLICATION_TOOL_CONTRACTS } from "../contracts/applicationToolContracts.js";
|
|
import { compareApplicationVersionsInputSchema } from "../domain/javascriptApplicationVersionComparisonSchemas.js";
|
|
import { applicationVersionComparisonResultSchema } from "../domain/javascriptApplicationVersionComparisonSchemas.js";
|
|
import { traceApplicationFeatureInputSchema } from "../domain/javascriptFeatureTraceSchemas.js";
|
|
import { parseEvidence, type Evidence } from "../domain/evidence.js";
|
|
import type { Logger } from "../logger.js";
|
|
import {
|
|
controlledReplayInputSchema,
|
|
controlledReplayOutputSchema,
|
|
} from "../domain/javascriptReplay.js";
|
|
import { logToolExecution } from "./toolLogging.js";
|
|
import { toCallToolResult } from "./toolResult.js";
|
|
import { toolRegistrationOptions } from "./toolRegistrationOptions.js";
|
|
import { safeParseToolInput } from "./toolInputValidation.js";
|
|
import { mcpProgressReporter } from "./mcpProgress.js";
|
|
|
|
interface ApplicationToolRegistration {
|
|
readonly logger: Logger;
|
|
readonly recordEvidence: BinarySessionPort["recordEvidence"] | undefined;
|
|
readonly recordEvidenceWithUnknown:
|
|
| BinarySessionPort["recordEvidenceWithUnknown"]
|
|
| undefined;
|
|
readonly replay: JavaScriptReplayDependencies;
|
|
}
|
|
|
|
/** Register provider-neutral JavaScript application graph workflows. */
|
|
export const registerApplicationTools = (
|
|
server: McpServer,
|
|
options: ApplicationToolRegistration,
|
|
): void => {
|
|
const [traceContract, compareContract, replayContract] =
|
|
APPLICATION_TOOL_CONTRACTS;
|
|
server.registerTool(
|
|
traceContract.name,
|
|
toolRegistrationOptions(traceContract),
|
|
async (input) => {
|
|
const parsedInput = safeParseToolInput(
|
|
traceApplicationFeatureInputSchema,
|
|
input,
|
|
traceContract.name,
|
|
);
|
|
if (!parsedInput.ok) return toCallToolResult(parsedInput, traceContract);
|
|
const parsed = parsedInput.value;
|
|
const result = await logToolExecution(
|
|
options.logger,
|
|
traceContract.name,
|
|
() => Promise.resolve(traceApplicationFeatureEvidence(parsed)),
|
|
);
|
|
if (!result.ok) return toCallToolResult(result, traceContract);
|
|
const sources = [parsed.application, ...parsed.native_observations];
|
|
const recorded = recordSources(options.recordEvidence, sources);
|
|
if (!recorded.ok) return toCallToolResult(recorded, traceContract);
|
|
return recordResult(options, traceContract, result.value);
|
|
},
|
|
);
|
|
server.registerTool(
|
|
compareContract.name,
|
|
toolRegistrationOptions(compareContract),
|
|
async (input) => {
|
|
const parsedInput = safeParseToolInput(
|
|
compareApplicationVersionsInputSchema,
|
|
input,
|
|
compareContract.name,
|
|
);
|
|
if (!parsedInput.ok)
|
|
return toCallToolResult(parsedInput, compareContract);
|
|
const parsed = parsedInput.value;
|
|
const result = await logToolExecution(
|
|
options.logger,
|
|
compareContract.name,
|
|
() => Promise.resolve(compareApplicationVersionsEvidence(parsed)),
|
|
);
|
|
if (!result.ok) return toCallToolResult(result, compareContract);
|
|
const sources = [
|
|
parsed.left,
|
|
parsed.right,
|
|
...parsed.left_native_observations,
|
|
...parsed.right_native_observations,
|
|
];
|
|
const recorded = recordSources(options.recordEvidence, sources);
|
|
if (!recorded.ok) return toCallToolResult(recorded, compareContract);
|
|
const comparison = applicationVersionComparisonResultSchema.parse(
|
|
result.value.normalized_result,
|
|
);
|
|
const unknown = comparison.summary.unknown > 0;
|
|
return recordResult(
|
|
options,
|
|
compareContract,
|
|
result.value,
|
|
parsed.unknown_registry_approved === true && unknown,
|
|
);
|
|
},
|
|
);
|
|
server.registerTool(
|
|
replayContract.name,
|
|
toolRegistrationOptions(replayContract),
|
|
async (input, context) => {
|
|
const parsedInput = safeParseToolInput(
|
|
controlledReplayInputSchema,
|
|
input,
|
|
replayContract.name,
|
|
);
|
|
if (!parsedInput.ok) return toCallToolResult(parsedInput, replayContract);
|
|
const parsed = parsedInput.value;
|
|
const result = await logToolExecution(
|
|
options.logger,
|
|
replayContract.name,
|
|
() =>
|
|
runControlledReplay(options.replay, parsed, {
|
|
signal: context.mcpReq.signal,
|
|
progress: mcpProgressReporter(context),
|
|
}),
|
|
);
|
|
if (!result.ok) return toCallToolResult(result, replayContract);
|
|
const output = controlledReplayOutputSchema.parse(result.value);
|
|
if (output.evidence !== null) {
|
|
const sourcesRecorded = recordSources(
|
|
options.recordEvidence,
|
|
output.source_evidence.map((item) => parseEvidence(item)),
|
|
);
|
|
if (!sourcesRecorded.ok)
|
|
return toCallToolResult(sourcesRecorded, replayContract);
|
|
const recorded = options.recordEvidence?.(
|
|
parseEvidence(output.evidence),
|
|
);
|
|
if (recorded !== undefined && !recorded.ok)
|
|
return toCallToolResult(recorded, replayContract);
|
|
}
|
|
return toCallToolResult(result, replayContract, {
|
|
evidenceResourcesAvailable:
|
|
output.evidence !== null && options.recordEvidence !== undefined,
|
|
});
|
|
},
|
|
);
|
|
};
|
|
|
|
const recordSources = (
|
|
recordEvidence: ApplicationToolRegistration["recordEvidence"],
|
|
sources: readonly Evidence[],
|
|
) => {
|
|
for (const source of sources) {
|
|
const recorded = recordEvidence?.(source);
|
|
if (recorded !== undefined && !recorded.ok) return recorded;
|
|
}
|
|
return { ok: true as const, value: null };
|
|
};
|
|
|
|
const recordResult = (
|
|
options: ApplicationToolRegistration,
|
|
contract: (typeof APPLICATION_TOOL_CONTRACTS)[number],
|
|
evidence: Evidence,
|
|
recordUnknown = false,
|
|
) => {
|
|
const recorded = recordUnknown
|
|
? options.recordEvidenceWithUnknown?.(evidence, {
|
|
approved: true,
|
|
question:
|
|
"Which application entities remain unmatched or ambiguous across these versions?",
|
|
severity: "medium",
|
|
domain: "application-version-comparison",
|
|
supporting_evidence_ids: [evidence.evidence_id],
|
|
contradicting_evidence_ids: [],
|
|
required_authority: "shipped-artifact",
|
|
required_confidence: "observed",
|
|
required_environment: null,
|
|
recommended_probes: [
|
|
{
|
|
operation: "analyze_javascript_application",
|
|
rationale:
|
|
"Repeat static reconstruction with complete artifacts and source maps when available.",
|
|
},
|
|
{
|
|
operation: "reconcile_javascript_runtime",
|
|
rationale:
|
|
"Add approved passive runtime Evidence without promoting it to static fact.",
|
|
},
|
|
],
|
|
relationships: [],
|
|
})
|
|
: options.recordEvidence?.(evidence);
|
|
if (recorded !== undefined && !recorded.ok)
|
|
return toCallToolResult(recorded, contract);
|
|
return toCallToolResult({ ok: true, value: evidence }, contract, {
|
|
evidenceResourcesAvailable: recorded !== undefined,
|
|
});
|
|
};
|