From 4193bc5bf85793bf9d8ee391e37df7de2f6c17ca Mon Sep 17 00:00:00 2001 From: Wade <160517347+Sriram-PR@users.noreply.github.com> Date: Sat, 24 Oct 2026 01:00:42 +1100 Subject: [PATCH] fix(captures): advertise offline capture inspection without a browser provider (#6725) --- .../webNetworkCaptureToolContracts.ts | 2 +- .../boundary/mcp/webNetworkCaptureMcp.test.ts | 80 ++++++++++++++++++- 2 files changed, 79 insertions(+), 3 deletions(-) diff --git a/src/contracts/webNetworkCaptureToolContracts.ts b/src/contracts/webNetworkCaptureToolContracts.ts index 17370b27b4..787e023d18 100644 --- a/src/contracts/webNetworkCaptureToolContracts.ts +++ b/src/contracts/webNetworkCaptureToolContracts.ts @@ -17,7 +17,7 @@ export const WEB_NETWORK_CAPTURE_TOOL_CONTRACTS = [ ...toolContractMetadata("inspect_web_network_capture"), description: "Explicit record_ordinals also decode retained protobuf/gRPC bodies whose declared content type is recognized. Optional protobuf selects exact body_pointers, framing, and a caller-supplied local FileDescriptorSet with message_type. Native gRPC and gRPC-web framing remain distinct. Wire fields are flat byte-range facts; ambiguous readings stay candidates, compressed payloads stay unknown. Page retained decoding with inspect_analysis_view kind protobuf, collection bodies/frames/fields, exact batch selectors and fields. Large text and schema values require explicit fields selection in those pages. Request detail summary for capture identity and counts; then page retained records with inspect_analysis_view kind capture (preview or complete), avoiding repeated decoding. Default detail complete preserves existing inline delivery. Inspect retained local HAR 1.2, native mitmproxy, Fiddler SAZ or aioquic 1.5.0 JSON qlog 0.3 events without contacting recorded URLs. Returns original ordinals, pointers/byte ranges/archive member names, preserved producer fields, precise numeric representations and exact exposed binary bytes inline. Excludes known transport credentials and explicitly marked values. Decodes every record before optional ordinal selection; malformed or oversized captures return no partial success. Duplicate JSON members are invalid; prototype-named members such as __proto__ are preserved as ordinary own JSON members without prototype mutation. Qlog reports trace-scoped STREAM interval unions and endpoint loss declarations; it does not recover bodies, prove network loss or correlate PCAP. Other qlog layouts are unsupported. Uses pinned upstream parsers, owned temporary files and bounded offline processes. SAZ preserves HTTP fields, XML metadata and encoded body/message bytes without replay or HAR conversion; encrypted archives are unsupported. Native mitmproxy requires caller-supplied mitmdump 12.2.3 on Linux. Browser transaction IDs, runtime attribution and original bytes of HAR Unicode text remain unknown.", - kind: "browser-provider", + kind: "application", inputSchema: inspectWebNetworkCaptureInputSchema.extend({ detail: analysisDetailSchema, }), diff --git a/tests/boundary/mcp/webNetworkCaptureMcp.test.ts b/tests/boundary/mcp/webNetworkCaptureMcp.test.ts index 4bdf06f394..5b5cf75902 100644 --- a/tests/boundary/mcp/webNetworkCaptureMcp.test.ts +++ b/tests/boundary/mcp/webNetworkCaptureMcp.test.ts @@ -2,16 +2,22 @@ import { parseMcpToolError } from "../../fixtures/mcpToolError.js"; import { Client, InMemoryTransport } from "@modelcontextprotocol/client"; import { Ajv2020 } from "ajv/dist/2020.js"; import { expect, it, onTestFinished } from "vitest"; +import { z } from "zod"; import { WebNetworkCaptureService } from "../../../src/application/WebNetworkCaptureService.js"; import { toolContract } from "../../../src/contracts/toolContracts.js"; +import { + expandToolAvailability, + sessionProvider, +} from "../../../src/contracts/toolOutputSchemaPrimitives.js"; import { parseEvidence } from "../../../src/domain/evidence.js"; import { ok } from "../../../src/domain/result.js"; import { webNetworkCaptureSchema } from "../../../src/domain/webNetworkCapture.js"; import { createServer } from "../../../src/server/createServer.js"; import { createTestBinarySession } from "../../fixtures/binarySession.js"; +import { createTestMcp } from "../../support/mcp/mcpFixture.js"; -it("publishes valid SDK schemas and retains historical inline Evidence with the supplied session", async () => { - const service = new WebNetworkCaptureService({ +const syntheticCaptureService = () => + new WebNetworkCaptureService({ inspect: () => Promise.resolve( ok({ @@ -31,6 +37,9 @@ it("publishes valid SDK schemas and retains historical inline Evidence with the }), ), }); + +it("publishes valid SDK schemas and retains historical inline Evidence with the supplied session", async () => { + const service = syntheticCaptureService(); const session = createTestBinarySession(() => { throw new Error("Binary provider must not start"); }); @@ -102,3 +111,70 @@ it("publishes valid SDK schemas and retains historical inline Evidence with the expect(JSON.stringify(invalidSensitive)).not.toContain("secret"); expect(ajv.validate(inputSchema, { ...args, mysecret: true })).toBe(false); }); + +it.each([false, true])( + "advertises offline capture inspection without a browser provider (browser load failure %s)", + async (failed) => { + const session = createTestBinarySession(() => { + throw new Error("Binary provider must not start"); + }); + const mcp = createTestMcp(); + onTestFinished(async () => { + await mcp.close(); + await session.close(); + }); + const client = await mcp.connect( + createServer( + { kind: "session", session }, + { + webNetworkCapture: syntheticCaptureService(), + ...(failed + ? { + optionalProviderLoadFailures: { + browserObservation: { + providerId: "rea-cdp-browser", + reason: "synthetic browser import failure", + }, + }, + } + : {}), + }, + ), + ); + const status = await client.callTool({ + name: "binary_session", + arguments: {}, + }); + const availability = expandToolAvailability( + z.object({ result: sessionProvider }).parse(status.structuredContent) + .result.tool_availability, + ); + const entry = (name: string) => + availability.find((tool) => tool.name === name); + expect(entry("inspect_web_network_capture")).toMatchObject({ + available: true, + reason: "available", + }); + expect(entry("list_browser_targets")).toMatchObject({ + available: false, + reason: failed ? "provider_unavailable" : "provider_missing", + }); + + const response = await client.callTool({ + name: "inspect_web_network_capture", + arguments: { capture_path: "/capture.har", format: "har" }, + }); + expect(response.isError).not.toBe(true); + const capture = webNetworkCaptureSchema.parse( + toolContract("inspect_web_network_capture").outputSchema.parse( + response.structuredContent, + ).normalized_result, + ); + expect(capture).toMatchObject({ format: "har", total_records: 0 }); + const live = await client.callTool({ + name: "list_browser_targets", + arguments: { cdp_endpoint: "http://127.0.0.1:9222" }, + }); + expect(live.isError).toBe(true); + }, +);