import type { EvidenceMcpServer } from "./EvidenceMcpServer.js"; import type { BinarySessionPort } from "../application/binary/BinarySessionPort.js"; import type { ProviderAvailability } from "../application/AnalysisProvider.js"; import type { JebAnalysisAvailability } from "../application/jeb/JebAnalysisPort.js"; import { AnalysisError } from "../domain/analysisErrorBase.js"; import { buildCapabilityInventory, compactToolAvailability, } from "../application/CapabilityInventory.js"; import { toolContract } from "../contracts/toolContracts.js"; import type { ClientFeatureAvailability, ToolAvailability, } from "../contracts/toolOutputSchemaPrimitives.js"; import { jsonObjectSchema, type JsonValue } from "../domain/jsonValue.js"; import { createServerIdentity } from "../serverIdentity.js"; import { mcpClientMetadata } from "./mcpClientMetadata.js"; import type { AvailabilityPolicy } from "../application/CapabilityInventory.js"; import { toolRegistrationOptions } from "./toolRegistrationOptions.js"; import { err } from "../domain/result.js"; import { CURRENT_ENTRYPOINT_PATH } from "../process/CurrentEntrypointPath.js"; /** Provider-owned live probes used by session discovery. */ export interface SessionAvailabilityProbes { readonly androidAnalysisAvailability: ( signal: AbortSignal, ) => Promise; readonly adbAnalysisAvailability?: ( signal: AbortSignal, ) => Promise; readonly apktoolAnalysisAvailability?: ( signal: AbortSignal, ) => Promise; readonly jebAnalysisAvailability?: ( signal: AbortSignal, ) => Promise; } /** Inputs required to register the binary session status tool. */ export interface SessionStatusToolOptions extends SessionAvailabilityProbes { readonly server: EvidenceMcpServer; readonly session: BinarySessionPort; readonly contract: ReturnType>; readonly startedAt: string; readonly availabilityPolicy: () => AvailabilityPolicy; } /** Register the read-only provider and target status operation. */ export const registerSessionStatusTool = ( options: SessionStatusToolOptions, ): void => { const { server, session, contract, startedAt, availabilityPolicy, androidAnalysisAvailability, adbAnalysisAvailability, apktoolAnalysisAvailability, jebAnalysisAvailability, } = options; server.registerTool( contract.name, toolRegistrationOptions(contract), async (input, context) => { const { client, clientFeatures, protocolVersion } = mcpClientMetadata( context, server.server, ); let androidAvailability: ProviderAvailability; let adbAvailability: ProviderAvailability | undefined; let apktoolAvailability: ProviderAvailability | undefined; let jebAvailability: JebAnalysisAvailability | undefined; let status: JsonValue; try { const [android, adb, apktool, jeb] = await Promise.all([ androidAnalysisAvailability(context.mcpReq.signal), adbAnalysisAvailability?.(context.mcpReq.signal) ?? undefined, apktoolAnalysisAvailability?.(context.mcpReq.signal) ?? undefined, jebAnalysisAvailability?.(context.mcpReq.signal) ?? undefined, ] as const); androidAvailability = android; adbAvailability = adb; apktoolAvailability = apktool; jebAvailability = jeb; status = await session.status({ signal: context.mcpReq.signal }); } catch (cause) { if (!(cause instanceof AnalysisError)) throw cause; return server.delivery.toCallToolResult(err(cause), contract); } const statusObject = jsonObjectSchema.parse(status); const toolAvailability = buildCapabilityInventory( status, { ...availabilityPolicy(), androidAnalysisAvailability: androidAvailability, ...(adbAvailability === undefined ? {} : { adbAnalysisAvailability: adbAvailability }), ...(apktoolAvailability === undefined ? {} : { apktoolAnalysisAvailability: apktoolAvailability }), ...(jebAvailability === undefined ? {} : { jebAnalysisAvailability: jebAvailability }), }, clientFeatures, ); const serverIdentity = createServerIdentity({ startedAt, commandPath: CURRENT_ENTRYPOINT_PATH, catalog: server.catalogIdentity, expected: { ...(input.expected_package_version === undefined ? {} : { package_version: input.expected_package_version }), ...(input.expected_catalog_digest === undefined ? {} : { catalog_digest: input.expected_catalog_digest }), ...(input.expected_server_path === undefined ? {} : { server_path: input.expected_server_path }), }, ...(client === undefined ? {} : { client }), ...(protocolVersion === undefined ? {} : { protocolVersion }), }); return server.delivery.toCallToolResult( { ok: true, value: projectSessionStatus({ status: statusObject, toolAvailability, serverIdentity, clientFeatures, }), }, contract, ); }, ); }; const projectSessionStatus = (options: { readonly status: Readonly< Record >; readonly toolAvailability: readonly ToolAvailability[]; readonly serverIdentity: ReturnType; readonly clientFeatures: ClientFeatureAvailability; }) => jsonObjectSchema.parse({ ...options.status, tool_availability: compactToolAvailability(options.toolAvailability), client_features: options.clientFeatures, server_identity: options.serverIdentity, });