fix(captures): advertise offline capture inspection without a browser provider (#6725)

This commit is contained in:
Wade authored and GitHub committed 2026-10-23 22:00:42 +08:00
1 parent 610966b836
commit 4193bc5bf8
2 files changed
+79 -3

No files matched your search

@@ -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,
}),
@@ -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);
},
);