From c83aa379e6e7e4a99093cdb76ba411ab23a1850a Mon Sep 17 00:00:00 2001 From: yummybomb <19238148+yummybomb@users.noreply.github.com> Date: Thu, 16 Jul 2026 17:52:52 +0000 Subject: [PATCH] Add get_telemetry_event for uncompacted single-event reads get_telemetry compacts events to fit an agent context window, which strips exactly the fields sometimes needed as evidence (response bodies carrying error messages, request headers, post data). Add a manage_browsers action that fetches one archived event by seq with its full payload; only screenshot png stays omitted. Event seq values are embedded in record bodies by the instance while the offset param addresses the stream position, so the lookup probes near the target and corrects for the observed drift instead of assuming the two are aligned. --- src/lib/mcp/prompts.ts | 2 +- src/lib/mcp/tools/browsers.ts | 134 ++++++++++++++++++++++++++++------ 2 files changed, 113 insertions(+), 23 deletions(-) diff --git a/src/lib/mcp/prompts.ts b/src/lib/mcp/prompts.ts index 513e9fe..9d74972 100644 --- a/src/lib/mcp/prompts.ts +++ b/src/lib/mcp/prompts.ts @@ -137,7 +137,7 @@ kernel browsers playwright --help When telemetry was captured, it's usually the fastest way to pinpoint a failure — read it before reaching for screenshots or logs. If the session has been deleted, it's the only signal still available: every CLI command in this guide needs a live session. -Start broad: call \`manage_browsers\` with action "get_telemetry", session_id "${session_id}", and no filters. That starts at session creation and returns the first page (up to 100 events); page with \`next_offset\` as \`offset\` while \`has_more\` is true, preserving \`categories\`, \`until\`, and \`order\`. An empty unfiltered read is definitive: nothing was archived. Narrow when the output is too large to scan or you already know where to look: \`categories\` to isolate a signal you've spotted, \`order\` "desc" when the end of the session matters most, \`since\`/\`until\` to bracket a known failing step. Correlate event timestamps with the failing automation step. +Start broad: call \`manage_browsers\` with action "get_telemetry", session_id "${session_id}", and no filters. That starts at session creation and returns the first page (up to 100 events); page with \`next_offset\` as \`offset\` while \`has_more\` is true, preserving \`categories\`, \`until\`, and \`order\`. An empty unfiltered read is definitive: nothing was archived. Narrow when the output is too large to scan or you already know where to look: \`categories\` to isolate a signal you've spotted, \`order\` "desc" when the end of the session matters most, \`since\`/\`until\` to bracket a known failing step. Correlate event timestamps with the failing automation step. Events are compacted to save context: bulky payload fields (headers, bodies, post data, anything oversized) are stripped and listed in \`omitted_fields\`. When an omitted field is the evidence you need — e.g. the response body carrying an API error message — fetch that single event uncompacted with action "get_telemetry_event" and its \`seq\`. **Gotcha: telemetry is opt-in and only covers activity that happened while capture was on.** Archived events survive telemetry being disabled and the session being deleted, so the archive — not the current config — is the ground truth: \`manage_browsers\` action "get" showing no enabled \`telemetry\` categories means capture is off now, not that nothing was recorded. The default bundle (control/connection/system/captcha) also omits the debug-critical categories. To capture new evidence on an active browser, use \`manage_browsers\` action "update" to enable \`telemetry_console\`, \`telemetry_network\`, and \`telemetry_page\`, then reproduce the issue; recreate the browser only if the session has ended. diff --git a/src/lib/mcp/tools/browsers.ts b/src/lib/mcp/tools/browsers.ts index 6166d61..e4ac144 100644 --- a/src/lib/mcp/tools/browsers.ts +++ b/src/lib/mcp/tools/browsers.ts @@ -150,40 +150,99 @@ async function summarizeEmptyTelemetryResult( : "No events matched this query."; } -function compactTelemetryEvent({ seq, event }: TelemetryEnvelope) { +function telemetryEventResponse( + { seq, event }: TelemetryEnvelope, + data: Record | undefined, + omittedFields: string[] | undefined, +) { const { ts, category, type, source, truncated } = event; - const data = "data" in event ? event.data : undefined; + return { + seq, + // Raw ts (Unix microseconds) is kept alongside the readable time so exact + // event boundaries can be fed back as since/until. + ts, + time: new Date(ts / 1000).toISOString(), + category, + type, + source, + ...(data && { data }), + ...(truncated && { truncated }), + ...(omittedFields && { omitted_fields: omittedFields }), + }; +} + +function telemetryEventData(envelope: TelemetryEnvelope) { + const { event } = envelope; + return "data" in event + ? { ...(event.data as Record) } + : undefined; +} + +function compactTelemetryEvent(envelope: TelemetryEnvelope) { + const data = telemetryEventData(envelope); - let compactData: Record | undefined; let omittedFields: string[] | undefined; if (data) { - compactData = { ...(data as Record) }; - for (const [field, value] of Object.entries(compactData)) { + for (const [field, value] of Object.entries(data)) { const alwaysOmit = omittedTelemetryDataFields.has(field); const serialized = alwaysOmit ? undefined : JSON.stringify(value); const oversized = serialized !== undefined && Buffer.byteLength(serialized, "utf8") > maxTelemetryDataFieldBytes; if (alwaysOmit || oversized) { - delete compactData[field]; + delete data[field]; (omittedFields ??= []).push(field); } } } - return { - seq, - // Raw ts (Unix microseconds) is kept alongside the readable time so exact - // event boundaries can be fed back as since/until. - ts, - time: new Date(ts / 1000).toISOString(), - category, - type, - source, - ...(compactData && { data: compactData }), - ...(truncated && { truncated }), - ...(omittedFields && { omitted_fields: omittedFields }), - }; + return telemetryEventResponse(envelope, data, omittedFields); +} + +// Full-payload counterpart to compactTelemetryEvent: only screenshot png is +// dropped (base64 image data is unusable in a text response and can run to +// hundreds of KB), everything else passes through uncompacted. +function rawTelemetryEvent(envelope: TelemetryEnvelope) { + const data = telemetryEventData(envelope); + + let omittedFields: string[] | undefined; + if (data && "png" in data) { + delete data.png; + omittedFields = ["png"]; + } + + return telemetryEventResponse(envelope, data, omittedFields); +} + +async function readBrowserTelemetryEvent( + client: KernelClient, + sessionId: string, + seq: number, +) { + // Event seq values are assigned by the instance and embedded in the record + // body, while offset addresses the underlying stream position; the two run + // at a locally constant drift. Probe near the target, measure the drift + // from what comes back, and re-anchor — one correction normally lands it. + let offset = Math.max(seq - 1, 0); + for (let attempt = 0; attempt < 3; attempt++) { + const page = await client.browsers.telemetry.events(sessionId, { + offset, + limit: 3, + }); + const items = page.getPaginatedItems(); + const match = items.find((item) => item.seq === seq); + if (match) { + return textResponse(JSON.stringify(rawTelemetryEvent(match))); + } + const first = items[0]; + if (!first) break; // past the stream tail; nothing to measure drift from + const corrected = Math.max(offset + (seq - first.seq), 0); + if (corrected === offset) break; // clamped at the stream head + offset = corrected; + } + return errorResponse( + `Error: no archived telemetry event with seq ${seq}. Use get_telemetry to find event seq values.`, + ); } type BrowserTelemetryReadParams = { @@ -331,15 +390,23 @@ export function registerBrowserCapabilities(server: McpServer) { // manage_browsers -- Manage browser sessions and read archived telemetry server.tool( "manage_browsers", - 'Manage browser sessions and their archived telemetry. Use "list" to choose an existing session, "create" before browser control, "update" to change supported session settings, "get" for full details, "get_telemetry" to diagnose active or deleted sessions, and "delete" when finished.', + 'Manage browser sessions and their archived telemetry. Use "list" to choose an existing session, "create" before browser control, "update" to change supported session settings, "get" for full details, "get_telemetry" to diagnose active or deleted sessions, "get_telemetry_event" to fetch one event\'s full payload, and "delete" when finished.', { action: z - .enum(["create", "update", "list", "get", "get_telemetry", "delete"]) + .enum([ + "create", + "update", + "list", + "get", + "get_telemetry", + "get_telemetry_event", + "delete", + ]) .describe("Operation to perform."), session_id: z .string() .describe( - "Browser session ID. Required for update, get, get_telemetry, and delete actions.", + "Browser session ID. Required for update, get, get_telemetry, get_telemetry_event, and delete actions.", ) .optional(), start_url: z @@ -497,6 +564,14 @@ export function registerBrowserCapabilities(server: McpServer) { "(get_telemetry) Read direction. asc (default) reads oldest first; desc reads newest first. Preserve it while paging.", ) .optional(), + seq: z + .number() + .int() + .min(0) + .describe( + "(get_telemetry_event) Sequence number of the event to fetch, taken from a get_telemetry result. Returns the event with its full payload, including fields get_telemetry omits (headers, body, post_data, oversized fields); screenshot png data stays omitted.", + ) + .optional(), telemetry_enabled: z .boolean() .describe( @@ -671,6 +746,21 @@ export function registerBrowserCapabilities(server: McpServer) { order: params.order, }); } + case "get_telemetry_event": { + if (!params.session_id) + return errorResponse( + "Error: session_id is required for get_telemetry_event action.", + ); + if (params.seq === undefined) + return errorResponse( + "Error: seq is required for get_telemetry_event action.", + ); + return await readBrowserTelemetryEvent( + client, + params.session_id, + params.seq, + ); + } case "delete": { if (!params.session_id) return errorResponse(