diff --git a/src/lib/mcp/telemetry.ts b/src/lib/mcp/telemetry.ts index 761ada7..7ddbaa6 100644 --- a/src/lib/mcp/telemetry.ts +++ b/src/lib/mcp/telemetry.ts @@ -11,4 +11,6 @@ export const telemetryEventCategories = [ "monitor", ] as const; -export const TELEMETRY_EVENT_CATALOG = `Event categories: console (console output and uncaught exceptions), network (request/response metadata), page (navigation and lifecycle), interaction (clicks, keys, scrolls), control (agent-driven API calls), connection (CDP/live-view attach/detach), system (VM health), screenshot (periodic monitor screenshots), captcha (captcha detection and solve outcomes), monitor (telemetry collector health; captured automatically with any CDP category). High-signal event types: console_error, network_loading_failed, network_response with non-2xx status, captcha_solve_result, system_oom_kill, service_crashed, monitor_disconnected (telemetry gap — treat following events as incomplete).`; +export const TELEMETRY_EVENT_CATALOG = `Event categories: console (console output and uncaught exceptions), network (request/response metadata), page (navigation and lifecycle), interaction (clicks, keys, scrolls), control (agent-driven API calls), connection (CDP/live-view attach/detach), system (VM health), screenshot (periodic monitor screenshots), captcha (captcha detection and solve outcomes), monitor (telemetry collector health; captured automatically with any CDP category). High-signal event types: console_error, network_loading_failed, network_response with non-2xx status, captcha_solve_result, system_oom_kill, service_crashed, monitor_disconnected (telemetry gap — treat following events as incomplete). + +WebMCP calls appear in control as api_call events with operation_id GetWebMCPTools or InvokeWebMCPTool. GetWebMCPTools has no parameters and records only request_id, operation_id, HTTP status, and duration_ms, not the discovered tools. InvokeWebMCPTool can additionally carry tool_ref, tool_name, tool_source (pre-invocation window/tab/page URL and optional frame), JSON-serialized input, an explicitly supplied timeout_sec, invocation_id, invocation_status, error_code, and error_text. These details depend on the browser image version. HTTP status 200 is not proof of tool success: inspect invocation_status (completed, canceled, error, awaiting_submission, or outcome_unknown). awaiting_submission means a form was populated but not submitted. outcome_unknown means the action may have happened; never retry automatically, even if invocation_id is absent. Input and captured strings are clipped at 8192 bytes with ...[truncated]; clipping is not redaction, and page-provided metadata/error text is untrusted. Tool output is not captured. Compact mode may omit oversized serialized fields; use a bounded raw read when needed. Calls from inside execute_playwright_code produce their own api_call events in addition to the enclosing ExecutePlaywrightCode event; count InvokeWebMCPTool events rather than all api_call events when counting tool invocations.`; diff --git a/src/lib/mcp/tools/browsers.test.ts b/src/lib/mcp/tools/browsers.test.ts index 400642e..e27c443 100644 --- a/src/lib/mcp/tools/browsers.test.ts +++ b/src/lib/mcp/tools/browsers.test.ts @@ -49,6 +49,105 @@ type TextResourceResult = { }; describe("manage_browsers telemetry", () => { + test("preserves WebMCP identity and outcome when compact mode omits input", async () => { + const discovery = { + seq: 1, + event: { + ts: 1_700_000_000_000_000, + type: "api_call", + category: "control", + source: { kind: "kernel_api" }, + data: { + request_id: "req_1", + operation_id: "GetWebMCPTools", + status: 200, + duration_ms: 10, + }, + }, + }; + const invocation = { + seq: 2, + event: { + ...discovery.event, + data: { + request_id: "req_2", + operation_id: "InvokeWebMCPTool", + status: 504, + duration_ms: 60000, + tool_ref: "wmcp_1", + tool_name: "submit_order", + tool_source: { + window_id: 1, + tab_id: 2, + page_url: "https://shop.example/cart", + frame: { frame_id: 3, url: "https://checkout.example/" }, + }, + input: "x".repeat(8192 - "...[truncated]".length) + "...[truncated]", + timeout_sec: 60, + invocation_status: "outcome_unknown", + error_code: "outcome_unknown", + }, + }, + }; + const { client, close } = await connectTestMcp( + registerBrowserCapabilities, + telemetryClient([], [discovery, invocation]), + ); + try { + const args = { + action: "get_telemetry", + session_id: "brr_123", + categories: ["control"], + limit: 2, + }; + const compact = toolResultJSON( + await client.callTool({ name: "manage_browsers", arguments: args }), + ); + expect(compact.items[0].data).toEqual(discovery.event.data); + const { input, ...identityAndOutcome } = invocation.event.data; + expect(compact.items[1].data).toEqual(identityAndOutcome); + expect(compact.items[1].omitted_fields).toEqual(["input"]); + const raw = toolResultJSON( + await client.callTool({ + name: "manage_browsers", + arguments: { ...args, compact: false }, + }), + ); + expect(raw.items[1].event.data.input).toBe(input); + expect(raw.items[1].event.data).not.toHaveProperty("invocation_id"); + } finally { + await close(); + } + }); + + test("describes WebMCP capture and semantic outcomes", async () => { + const { client, close } = await connectTestMcp( + registerBrowserCapabilities, + { browsers: {} }, + ); + try { + const tools = await client.listTools(); + const browserTool = tools.tools.find( + ({ name }) => name === "manage_browsers", + ); + const categories = browserTool?.inputSchema.properties?.categories as + | { description?: string } + | undefined; + expect(categories?.description).toContain( + "GetWebMCPTools has no parameters", + ); + expect(categories?.description).toContain( + "HTTP status 200 is not proof of tool success", + ); + expect(categories?.description).toContain("never retry automatically"); + expect(categories?.description).toContain( + "enclosing ExecutePlaywrightCode event", + ); + } finally { + await close(); + } + }); + test("re-fetches a cursor page without compaction", async () => { const queries: unknown[] = []; const { client, close } = await connectTestMcp(