From 7f98f1a501fb6d150731925f709ef07c6f1cf855 Mon Sep 17 00:00:00 2001 From: yummybomb <19238148+yummybomb@users.noreply.github.com> Date: Tue, 14 Jul 2026 20:59:54 +0000 Subject: [PATCH 01/12] Add browser telemetry reader --- src/lib/mcp/prompts.ts | 31 ++++-- src/lib/mcp/telemetry.ts | 14 +++ src/lib/mcp/tools/browsers.ts | 181 +++++++++++++++++++++++++++++++++- 3 files changed, 216 insertions(+), 10 deletions(-) create mode 100644 src/lib/mcp/telemetry.ts diff --git a/src/lib/mcp/prompts.ts b/src/lib/mcp/prompts.ts index ac347e4..242b3bd 100644 --- a/src/lib/mcp/prompts.ts +++ b/src/lib/mcp/prompts.ts @@ -1,5 +1,6 @@ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { z } from "zod"; +import { TELEMETRY_EVENT_CATALOG } from "@/lib/mcp/telemetry"; export function registerKernelPrompts(server: McpServer) { // MCP Prompt explaining Kernel concepts @@ -128,7 +129,23 @@ kernel browsers process --help kernel browsers playwright --help \`\`\` -**MCP Exception:** The \`computer_action\` MCP tool with action "screenshot" is useful since it returns images directly to the agent. +**MCP Exceptions:** The \`computer_action\` MCP tool with action "screenshot" is useful since it returns images directly to the agent, and \`get_browser_telemetry\` reads structured telemetry events (see below). + +--- + +## Telemetry Events (structured signal — works even after the session is deleted) + +**Check telemetry first when it's available** — it's the fastest way to pinpoint failures. + +**Gotcha: telemetry is opt-in and must have been enabled when the relevant activity occurred.** Always try \`get_browser_telemetry\` first because archived events survive telemetry being disabled and the session being deleted. \`manage_browsers\` action "get" shows only the current telemetry config, so a null \`telemetry\` field means capture is off now, not that the archive is necessarily empty. The default bundle (control/connection/system/captcha) also omits the debug-critical categories. For 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 original session has ended. + +**Flow:** +1. \`get_browser_telemetry\` with session_id "${session_id}" — filter with categories ["console", "network", "page"] to cut noise, or order "desc" to inspect the end of the session +2. Scan for \`console_error\`, \`network_loading_failed\`, \`network_response\` with non-2xx status, and \`captcha_*\` outcomes +3. Correlate event timestamps with the failing automation step +4. Page with \`next_offset\` while \`has_more\` is true + +${TELEMETRY_EVENT_CATALOG} --- @@ -224,6 +241,7 @@ These are **normal** and don't indicate problems: ## Debugging Checklist - [ ] Session exists and is active +- [ ] Telemetry events reviewed (if any were captured) - [ ] Screenshot shows expected content (or reveals error) - [ ] Current URL is as expected - [ ] Supervisor logs show all services running @@ -237,11 +255,12 @@ These are **normal** and don't indicate problems: Based on your issue "${issue_description}", start with: -1. **Get browser info** to confirm session is active -2. **Take screenshot** to see current state -3. **Check page URL** to see if on error page -4. **Test network** if seeing connection errors -5. **Review logs** for specific error patterns`; +1. **Get browser info** to confirm session is active and check whether telemetry was enabled +2. **Read telemetry events**; if needed, enable telemetry on an active session and reproduce +3. **Take screenshot** to see current state +4. **Check page URL** to see if on error page +5. **Test network** if seeing connection errors +6. **Review logs** for specific error patterns`; return { messages: [ diff --git a/src/lib/mcp/telemetry.ts b/src/lib/mcp/telemetry.ts new file mode 100644 index 0000000..761ada7 --- /dev/null +++ b/src/lib/mcp/telemetry.ts @@ -0,0 +1,14 @@ +export const telemetryEventCategories = [ + "console", + "network", + "page", + "interaction", + "control", + "connection", + "system", + "screenshot", + "captcha", + "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).`; diff --git a/src/lib/mcp/tools/browsers.ts b/src/lib/mcp/tools/browsers.ts index 42040f1..74ad563 100644 --- a/src/lib/mcp/tools/browsers.ts +++ b/src/lib/mcp/tools/browsers.ts @@ -15,11 +15,18 @@ import { toolErrorResponse, } from "@/lib/mcp/responses"; import { paginationParams } from "@/lib/mcp/schemas"; +import { + TELEMETRY_EVENT_CATALOG, + telemetryEventCategories, +} from "@/lib/mcp/telemetry"; type BrowserCreateParams = NonNullable< Parameters[0] >; type BrowserUpdateParams = Parameters[1]; +type TelemetryEventsQuery = NonNullable< + Parameters[1] +>; type TelemetryParams = { telemetry_enabled?: boolean; @@ -79,6 +86,42 @@ function buildTelemetry( }; } +type TelemetryEnvelope = Awaited< + ReturnType +>["items"][number]; + +// Payload fields that can carry kilobytes per event (response bodies, header +// maps). Dropped so a full page of events fits in an agent context window; +// omitted_fields tells the agent what to fetch via the API/CLI if needed. +const bulkyTelemetryDataFields = ["body", "headers", "post_data"] as const; + +function compactTelemetryEvent({ seq, event }: TelemetryEnvelope) { + const { ts, category, type, truncated } = event; + const data = "data" in event ? event.data : undefined; + + let compactData: Record | undefined; + let omittedFields: string[] | undefined; + if (data) { + compactData = { ...(data as Record) }; + for (const field of bulkyTelemetryDataFields) { + if (compactData[field] !== undefined) { + delete compactData[field]; + (omittedFields ??= []).push(field); + } + } + } + + return { + seq, + time: new Date(ts / 1000).toISOString(), + category, + type, + ...(compactData && { data: compactData }), + ...(truncated && { truncated }), + ...(omittedFields && { omitted_fields: omittedFields }), + }; +} + function browserSessionNextActions(sessionId: string) { return [ `Use computer_action with session_id "${sessionId}" to inspect or control the browser.`, @@ -299,21 +342,25 @@ export function registerBrowserCapabilities(server: McpServer) { telemetry_enabled: z .boolean() .describe( - "(create, update) Enable telemetry with VM defaults, or disable telemetry when false.", + "(create, update) Enable telemetry, or disable telemetry when false. Telemetry is off unless requested. The default category set is the lightweight operational bundle (control, connection, system, captcha) and does NOT include console, network, or page — enable those explicitly when you intend to debug page behavior.", ) .optional(), telemetry_console: z .boolean() - .describe("(create, update) Enable or disable console telemetry.") + .describe( + "(create, update) Enable or disable console telemetry (console output and uncaught exceptions). Off by default; enable for debugging.", + ) .optional(), telemetry_network: z .boolean() - .describe("(create, update) Enable or disable network telemetry.") + .describe( + "(create, update) Enable or disable network telemetry (request/response metadata). Off by default; enable for debugging.", + ) .optional(), telemetry_page: z .boolean() .describe( - "(create, update) Enable or disable page lifecycle telemetry.", + "(create, update) Enable or disable page lifecycle telemetry (navigation, load, layout shifts, LCP). Off by default; enable for debugging.", ) .optional(), telemetry_interaction: z @@ -460,4 +507,130 @@ export function registerBrowserCapabilities(server: McpServer) { } }, ); + + // get_browser_telemetry -- Read archived telemetry events for a session + server.tool( + "get_browser_telemetry", + `Read archived telemetry events for a browser session. Works while the session is active and after it is deleted, including events captured before telemetry was disabled. If the response reports status "telemetry_currently_disabled", widen or remove filters before enabling telemetry and reproducing: update an active browser, or recreate one that has ended. Page through long sessions with offset/next_offset instead of raising limit. ${TELEMETRY_EVENT_CATALOG}`, + { + session_id: z.string().describe("Browser session ID."), + categories: z + .array(z.enum(telemetryEventCategories)) + .min(1) + .describe( + "Restrict results to these event categories. The filter applies within each page, so a filtered page can be empty while has_more is true.", + ) + .optional(), + limit: z + .number() + .int() + .min(1) + .max(100) + .describe("Max events per page (1-100). Default 100.") + .optional(), + offset: z + .number() + .int() + .min(0) + .describe( + "Pagination cursor: pass next_offset from the previous response to fetch the next page. Opaque — do not derive it from event seq values.", + ) + .optional(), + since: z + .string() + .describe( + "Start of the window: an RFC-3339 timestamp or a duration like '30m' meaning that long ago. Defaults to the session's creation time. Ignored when offset is set; cannot be combined with order=desc.", + ) + .optional(), + until: z + .string() + .describe( + "End of the window (exclusive): an RFC-3339 timestamp or a duration like '5m'.", + ) + .optional(), + order: z + .enum(["asc", "desc"]) + .describe( + "Read direction. asc (default) reads oldest first starting from since; desc reads newest first — useful for inspecting the end of a session.", + ) + .optional(), + }, + { + title: "Read browser telemetry events", + readOnlyHint: true, + destructiveHint: false, + idempotentHint: true, + openWorldHint: false, + }, + async (params, extra) => { + if (!extra.authInfo) throw new Error("Authentication required"); + const client = createKernelClient(extra.authInfo.token); + + // Best-effort lookup for the session's telemetry config and creation + // time; when it fails we still read events but skip disambiguation. + const fetchBrowser = () => + client.browsers.retrieve(params.session_id).catch(() => null); + + try { + const query: TelemetryEventsQuery = { limit: params.limit ?? 100 }; + if (params.categories) query.category = params.categories; + if (params.offset !== undefined) query.offset = params.offset; + if (params.since !== undefined) query.since = params.since; + if (params.until !== undefined) query.until = params.until; + if (params.order !== undefined) query.order = params.order; + + let browser: Awaited> = null; + if ( + query.offset === undefined && + query.since === undefined && + query.order !== "desc" + ) { + // The API's since default is only 5m; cover the whole session. + browser = await fetchBrowser(); + query.since = browser?.created_at ?? "1970-01-01T00:00:00Z"; + } + + const page = await client.browsers.telemetry.events( + params.session_id, + query, + ); + const items = page.getPaginatedItems().map(compactTelemetryEvent); + + let status: "ok" | "telemetry_currently_disabled" | "no_events" = "ok"; + let note: string | undefined; + if (items.length === 0) { + if (page.has_more) { + note = + "This page had no matching events, but more are archived — continue paging with next_offset."; + } else { + browser ??= await fetchBrowser(); + if (browser && !browser.telemetry) { + status = "telemetry_currently_disabled"; + note = + "No archived events matched this window and filter, and telemetry is currently disabled. Widen since/until or drop the categories filter before reproducing: update this active browser with telemetry_enabled=true plus telemetry_console, telemetry_network, and telemetry_page."; + } else { + status = "no_events"; + note = browser + ? "No archived events matched this window and filter. Widen since/until or drop the categories filter." + : "No archived events matched, and the session could not be fetched. Widen since/until or drop the categories filter; if the session has ended and telemetry was not enabled, recreate it with telemetry enabled (including console, network, and page) and reproduce the issue."; + } + } + } + + // Compact serialization: a full page of events would waste a large + // share of its size on pretty-printing indentation. + return textResponse( + JSON.stringify({ + status, + items, + has_more: page.has_more, + next_offset: page.next_offset, + ...(note && { note }), + }), + ); + } catch (error) { + return toolErrorResponse("get_browser_telemetry", "events", error); + } + }, + ); } From bf283198a0ff7523404ab1ce319b639733b9976b Mon Sep 17 00:00:00 2001 From: yummybomb <19238148+yummybomb@users.noreply.github.com> Date: Wed, 15 Jul 2026 14:43:27 +0000 Subject: [PATCH 02/12] Keep source and raw ts in events, validate since+desc, fix empty notes --- src/lib/mcp/tools/browsers.ts | 29 ++++++++++++++++++++++++----- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/src/lib/mcp/tools/browsers.ts b/src/lib/mcp/tools/browsers.ts index 74ad563..ff3c383 100644 --- a/src/lib/mcp/tools/browsers.ts +++ b/src/lib/mcp/tools/browsers.ts @@ -96,7 +96,7 @@ type TelemetryEnvelope = Awaited< const bulkyTelemetryDataFields = ["body", "headers", "post_data"] as const; function compactTelemetryEvent({ seq, event }: TelemetryEnvelope) { - const { ts, category, type, truncated } = event; + const { ts, category, type, source, truncated } = event; const data = "data" in event ? event.data : undefined; let compactData: Record | undefined; @@ -113,9 +113,13 @@ function compactTelemetryEvent({ seq, event }: TelemetryEnvelope) { 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 }), @@ -564,6 +568,11 @@ export function registerBrowserCapabilities(server: McpServer) { }, async (params, extra) => { if (!extra.authInfo) throw new Error("Authentication required"); + if (params.since !== undefined && params.order === "desc") { + return errorResponse( + "Error in get_browser_telemetry (events): since cannot be combined with order=desc. Use until to bound a newest-first read, or order=asc with since.", + ); + } const client = createKernelClient(extra.authInfo.token); // Best-effort lookup for the session's telemetry config and creation @@ -589,6 +598,14 @@ export function registerBrowserCapabilities(server: McpServer) { browser = await fetchBrowser(); query.since = browser?.created_at ?? "1970-01-01T00:00:00Z"; } + // When the read covers the whole session with no filters (asc from + // creation, or desc from the newest event), an empty result means the + // archive is empty — there is nothing to widen. + const fullSessionRead = + params.offset === undefined && + params.since === undefined && + params.until === undefined && + params.categories === undefined; const page = await client.browsers.telemetry.events( params.session_id, @@ -603,16 +620,18 @@ export function registerBrowserCapabilities(server: McpServer) { note = "This page had no matching events, but more are archived — continue paging with next_offset."; } else { + const emptyReason = fullSessionRead + ? "No telemetry events are archived for this session" + : "No archived events matched this window and filter — widen since/until or drop the categories filter"; browser ??= await fetchBrowser(); if (browser && !browser.telemetry) { status = "telemetry_currently_disabled"; - note = - "No archived events matched this window and filter, and telemetry is currently disabled. Widen since/until or drop the categories filter before reproducing: update this active browser with telemetry_enabled=true plus telemetry_console, telemetry_network, and telemetry_page."; + note = `${emptyReason}. Telemetry is currently disabled: update this active browser with telemetry_enabled=true plus telemetry_console, telemetry_network, and telemetry_page, then reproduce the issue.`; } else { status = "no_events"; note = browser - ? "No archived events matched this window and filter. Widen since/until or drop the categories filter." - : "No archived events matched, and the session could not be fetched. Widen since/until or drop the categories filter; if the session has ended and telemetry was not enabled, recreate it with telemetry enabled (including console, network, and page) and reproduce the issue."; + ? `${emptyReason}.` + : `${emptyReason}, and the session could not be fetched. If the session has ended and telemetry was not enabled, recreate it with telemetry enabled (including console, network, and page) and reproduce the issue.`; } } } From bf300c93ac7b028ea9f7e3e876b3a773b3409e0e Mon Sep 17 00:00:00 2001 From: yummybomb <19238148+yummybomb@users.noreply.github.com> Date: Wed, 15 Jul 2026 14:48:38 +0000 Subject: [PATCH 03/12] Make telemetry debug guidance heuristic instead of prescriptive --- src/lib/mcp/prompts.ts | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/src/lib/mcp/prompts.ts b/src/lib/mcp/prompts.ts index 242b3bd..7a33b4d 100644 --- a/src/lib/mcp/prompts.ts +++ b/src/lib/mcp/prompts.ts @@ -135,15 +135,11 @@ kernel browsers playwright --help ## Telemetry Events (structured signal — works even after the session is deleted) -**Check telemetry first when it's available** — it's the fastest way to pinpoint failures. +When telemetry was captured, it's usually the fastest way to pinpoint a failure — read it before reaching for screenshots or logs. -**Gotcha: telemetry is opt-in and must have been enabled when the relevant activity occurred.** Always try \`get_browser_telemetry\` first because archived events survive telemetry being disabled and the session being deleted. \`manage_browsers\` action "get" shows only the current telemetry config, so a null \`telemetry\` field means capture is off now, not that the archive is necessarily empty. The default bundle (control/connection/system/captcha) also omits the debug-critical categories. For 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 original session has ended. +Start broad: call \`get_browser_telemetry\` with session_id "${session_id}" and no filters. That reads the whole session and definitively answers whether anything was archived. Narrow only when the output is too large to scan: \`categories\` to isolate a signal you've already spotted, order "desc" to inspect the end of the session, \`since\`/\`until\` to bracket the failing step. Correlate event timestamps with the failing automation step, and page with \`next_offset\` while \`has_more\` is true. -**Flow:** -1. \`get_browser_telemetry\` with session_id "${session_id}" — filter with categories ["console", "network", "page"] to cut noise, or order "desc" to inspect the end of the session -2. Scan for \`console_error\`, \`network_loading_failed\`, \`network_response\` with non-2xx status, and \`captcha_*\` outcomes -3. Correlate event timestamps with the failing automation step -4. Page with \`next_offset\` while \`has_more\` is true +**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 a null \`telemetry\` field 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. ${TELEMETRY_EVENT_CATALOG} From b2c9f94ec1669b4dcde9c008fb20ca695c285dc2 Mon Sep 17 00:00:00 2001 From: yummybomb <19238148+yummybomb@users.noreply.github.com> Date: Wed, 15 Jul 2026 18:24:55 +0000 Subject: [PATCH 04/12] Resolve telemetry ordering contradiction in debug prompt --- src/lib/mcp/prompts.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/lib/mcp/prompts.ts b/src/lib/mcp/prompts.ts index 7a33b4d..3dcba54 100644 --- a/src/lib/mcp/prompts.ts +++ b/src/lib/mcp/prompts.ts @@ -135,9 +135,9 @@ kernel browsers playwright --help ## Telemetry Events (structured signal — works even after the session is deleted) -When telemetry was captured, it's usually the fastest way to pinpoint a failure — read it before reaching for screenshots or logs. +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 \`get_browser_telemetry\` with session_id "${session_id}" and no filters. That reads the whole session and definitively answers whether anything was archived. Narrow only when the output is too large to scan: \`categories\` to isolate a signal you've already spotted, order "desc" to inspect the end of the session, \`since\`/\`until\` to bracket the failing step. Correlate event timestamps with the failing automation step, and page with \`next_offset\` while \`has_more\` is true. +Start broad: call \`get_browser_telemetry\` with session_id "${session_id}" and no filters. That reads the whole session and definitively answers whether anything was archived. Narrow only when the output is too large to scan: \`categories\` to isolate a signal you've already spotted, \`order\` "desc" to inspect the end of the session, \`since\`/\`until\` to bracket the failing step. Correlate event timestamps with the failing automation step, and page with \`next_offset\` while \`has_more\` is true. **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 a null \`telemetry\` field 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. @@ -251,8 +251,8 @@ These are **normal** and don't indicate problems: Based on your issue "${issue_description}", start with: -1. **Get browser info** to confirm session is active and check whether telemetry was enabled -2. **Read telemetry events**; if needed, enable telemetry on an active session and reproduce +1. **Read telemetry events** — works whether or not the session still exists; if the archive is empty and the session is active, enable the debug categories and reproduce +2. **Get browser info** to confirm the session is active before using the CLI commands 3. **Take screenshot** to see current state 4. **Check page URL** to see if on error page 5. **Test network** if seeing connection errors From ad15907c8445952b93f6bf8dae800bba7dc35297 Mon Sep 17 00:00:00 2001 From: yummybomb <19238148+yummybomb@users.noreply.github.com> Date: Wed, 15 Jul 2026 19:14:10 +0000 Subject: [PATCH 05/12] Clarify paging and narrowing guidance in telemetry debug prompt --- src/lib/mcp/prompts.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/mcp/prompts.ts b/src/lib/mcp/prompts.ts index 3dcba54..baeaa42 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 \`get_browser_telemetry\` with session_id "${session_id}" and no filters. That reads the whole session and definitively answers whether anything was archived. Narrow only when the output is too large to scan: \`categories\` to isolate a signal you've already spotted, \`order\` "desc" to inspect the end of the session, \`since\`/\`until\` to bracket the failing step. Correlate event timestamps with the failing automation step, and page with \`next_offset\` while \`has_more\` is true. +Start broad: call \`get_browser_telemetry\` with 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\` while \`has_more\` is true. 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. **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 a null \`telemetry\` field 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. From d3e2b941eaa258b8455bd16aa86ab016db9baa21 Mon Sep 17 00:00:00 2001 From: yummybomb <19238148+yummybomb@users.noreply.github.com> Date: Wed, 15 Jul 2026 19:18:19 +0000 Subject: [PATCH 06/12] Let runtime notes carry empty-result guidance in tool description --- src/lib/mcp/tools/browsers.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/mcp/tools/browsers.ts b/src/lib/mcp/tools/browsers.ts index ff3c383..143d6a0 100644 --- a/src/lib/mcp/tools/browsers.ts +++ b/src/lib/mcp/tools/browsers.ts @@ -515,7 +515,7 @@ export function registerBrowserCapabilities(server: McpServer) { // get_browser_telemetry -- Read archived telemetry events for a session server.tool( "get_browser_telemetry", - `Read archived telemetry events for a browser session. Works while the session is active and after it is deleted, including events captured before telemetry was disabled. If the response reports status "telemetry_currently_disabled", widen or remove filters before enabling telemetry and reproducing: update an active browser, or recreate one that has ended. Page through long sessions with offset/next_offset instead of raising limit. ${TELEMETRY_EVENT_CATALOG}`, + `Read archived telemetry events for a browser session. Works while the session is active and after it is deleted, including events captured before telemetry was disabled. Empty results include a status and note explaining what to do next. Page through long sessions with offset/next_offset instead of raising limit. ${TELEMETRY_EVENT_CATALOG}`, { session_id: z.string().describe("Browser session ID."), categories: z From 4dd0ee273e79791c718f80f896918ba861438f8b Mon Sep 17 00:00:00 2001 From: yummybomb <19238148+yummybomb@users.noreply.github.com> Date: Wed, 15 Jul 2026 19:28:02 +0000 Subject: [PATCH 07/12] Tighten get_browser_telemetry description --- src/lib/mcp/tools/browsers.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/mcp/tools/browsers.ts b/src/lib/mcp/tools/browsers.ts index 143d6a0..4d64312 100644 --- a/src/lib/mcp/tools/browsers.ts +++ b/src/lib/mcp/tools/browsers.ts @@ -515,7 +515,7 @@ export function registerBrowserCapabilities(server: McpServer) { // get_browser_telemetry -- Read archived telemetry events for a session server.tool( "get_browser_telemetry", - `Read archived telemetry events for a browser session. Works while the session is active and after it is deleted, including events captured before telemetry was disabled. Empty results include a status and note explaining what to do next. Page through long sessions with offset/next_offset instead of raising limit. ${TELEMETRY_EVENT_CATALOG}`, + `Read archived telemetry events for a browser session to diagnose failures. The archive is durable: it works while the session is active and after it is deleted, and includes events captured before telemetry was disabled. ${TELEMETRY_EVENT_CATALOG}`, { session_id: z.string().describe("Browser session ID."), categories: z From e5c6575fe49105118aa5db0aa5c546d167346e6a Mon Sep 17 00:00:00 2001 From: yummybomb <19238148+yummybomb@users.noreply.github.com> Date: Wed, 15 Jul 2026 19:40:36 +0000 Subject: [PATCH 08/12] Reserve telemetry_currently_disabled status for full-session reads --- src/lib/mcp/tools/browsers.ts | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/lib/mcp/tools/browsers.ts b/src/lib/mcp/tools/browsers.ts index 4d64312..c89b7d5 100644 --- a/src/lib/mcp/tools/browsers.ts +++ b/src/lib/mcp/tools/browsers.ts @@ -624,14 +624,21 @@ export function registerBrowserCapabilities(server: McpServer) { ? "No telemetry events are archived for this session" : "No archived events matched this window and filter — widen since/until or drop the categories filter"; browser ??= await fetchBrowser(); - if (browser && !browser.telemetry) { + const telemetryDisabled = browser !== null && !browser.telemetry; + // Only a full-session read proves the archive is empty; a filter + // miss stays no_events so the status alone can't be misread. + if (telemetryDisabled && fullSessionRead) { status = "telemetry_currently_disabled"; note = `${emptyReason}. Telemetry is currently disabled: update this active browser with telemetry_enabled=true plus telemetry_console, telemetry_network, and telemetry_page, then reproduce the issue.`; } else { status = "no_events"; - note = browser - ? `${emptyReason}.` - : `${emptyReason}, and the session could not be fetched. If the session has ended and telemetry was not enabled, recreate it with telemetry enabled (including console, network, and page) and reproduce the issue.`; + if (!browser) { + note = `${emptyReason}, and the session could not be fetched. If the session has ended and telemetry was not enabled, recreate it with telemetry enabled (including console, network, and page) and reproduce the issue.`; + } else if (telemetryDisabled) { + note = `${emptyReason}. Telemetry is also currently disabled — if an unfiltered read is empty too, update this active browser with telemetry_enabled=true plus telemetry_console, telemetry_network, and telemetry_page, then reproduce the issue.`; + } else { + note = `${emptyReason}.`; + } } } } From 01515e4cfd688e615de18d97eecd0df17f4684f8 Mon Sep 17 00:00:00 2001 From: yummybomb <19238148+yummybomb@users.noreply.github.com> Date: Wed, 15 Jul 2026 19:46:19 +0000 Subject: [PATCH 09/12] Make empty-archive note category guidance hypothesis-driven --- src/lib/mcp/tools/browsers.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/lib/mcp/tools/browsers.ts b/src/lib/mcp/tools/browsers.ts index c89b7d5..9a7fd64 100644 --- a/src/lib/mcp/tools/browsers.ts +++ b/src/lib/mcp/tools/browsers.ts @@ -625,17 +625,19 @@ export function registerBrowserCapabilities(server: McpServer) { : "No archived events matched this window and filter — widen since/until or drop the categories filter"; browser ??= await fetchBrowser(); const telemetryDisabled = browser !== null && !browser.telemetry; + const enableHint = + "update this active browser with telemetry_enabled=true plus the categories your investigation needs (telemetry_enabled alone captures only the default bundle, not console/network/page), then reproduce the issue"; // Only a full-session read proves the archive is empty; a filter // miss stays no_events so the status alone can't be misread. if (telemetryDisabled && fullSessionRead) { status = "telemetry_currently_disabled"; - note = `${emptyReason}. Telemetry is currently disabled: update this active browser with telemetry_enabled=true plus telemetry_console, telemetry_network, and telemetry_page, then reproduce the issue.`; + note = `No telemetry events are archived for this session and telemetry is currently disabled. To capture evidence, ${enableHint}.`; } else { status = "no_events"; if (!browser) { - note = `${emptyReason}, and the session could not be fetched. If the session has ended and telemetry was not enabled, recreate it with telemetry enabled (including console, network, and page) and reproduce the issue.`; + note = `${emptyReason}, and the session could not be fetched. If the session has ended and telemetry was not enabled, recreate it with the telemetry categories your investigation needs and reproduce the issue.`; } else if (telemetryDisabled) { - note = `${emptyReason}. Telemetry is also currently disabled — if an unfiltered read is empty too, update this active browser with telemetry_enabled=true plus telemetry_console, telemetry_network, and telemetry_page, then reproduce the issue.`; + note = `${emptyReason}. Telemetry is also currently disabled — if an unfiltered read is empty too, ${enableHint}.`; } else { note = `${emptyReason}.`; } From d5b960fc9b3241450d168ddd2a04f7edf06dcb40 Mon Sep 17 00:00:00 2001 From: yummybomb <19238148+yummybomb@users.noreply.github.com> Date: Wed, 15 Jul 2026 19:56:42 +0000 Subject: [PATCH 10/12] Align interaction telemetry description with sibling categories --- src/lib/mcp/tools/browsers.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/mcp/tools/browsers.ts b/src/lib/mcp/tools/browsers.ts index 9a7fd64..48ca22e 100644 --- a/src/lib/mcp/tools/browsers.ts +++ b/src/lib/mcp/tools/browsers.ts @@ -370,7 +370,7 @@ export function registerBrowserCapabilities(server: McpServer) { telemetry_interaction: z .boolean() .describe( - "(create, update) Enable or disable user interaction telemetry.", + "(create, update) Enable or disable user interaction telemetry (clicks, keys, scrolls). Off by default; enable for debugging.", ) .optional(), }, From cf07dd3d1d265900b63a3638f5546213747fbeff Mon Sep 17 00:00:00 2001 From: yummybomb <19238148+yummybomb@users.noreply.github.com> Date: Wed, 15 Jul 2026 20:10:43 +0000 Subject: [PATCH 11/12] Detect disabled telemetry from empty category config --- src/lib/mcp/tools/browsers.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/lib/mcp/tools/browsers.ts b/src/lib/mcp/tools/browsers.ts index 48ca22e..cf81464 100644 --- a/src/lib/mcp/tools/browsers.ts +++ b/src/lib/mcp/tools/browsers.ts @@ -624,7 +624,13 @@ export function registerBrowserCapabilities(server: McpServer) { ? "No telemetry events are archived for this session" : "No archived events matched this window and filter — widen since/until or drop the categories filter"; browser ??= await fetchBrowser(); - const telemetryDisabled = browser !== null && !browser.telemetry; + // A cleared config serializes as {} (not null), so "disabled" means + // no category is currently enabled rather than a nullish field. + const telemetryDisabled = + browser !== null && + !Object.values(browser.telemetry?.browser ?? {}).some( + (category) => category?.enabled, + ); const enableHint = "update this active browser with telemetry_enabled=true plus the categories your investigation needs (telemetry_enabled alone captures only the default bundle, not console/network/page), then reproduce the issue"; // Only a full-session read proves the archive is empty; a filter From 98eb0a97023bb2d981198b64951363a153c0e7ac Mon Sep 17 00:00:00 2001 From: yummybomb <19238148+yummybomb@users.noreply.github.com> Date: Wed, 15 Jul 2026 20:29:24 +0000 Subject: [PATCH 12/12] Skip since default on until-only reads; document get_browser_telemetry in README --- README.md | 5 +++-- src/lib/mcp/tools/browsers.ts | 5 ++++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 3102bff..cfda98c 100644 --- a/README.md +++ b/README.md @@ -255,9 +255,9 @@ Many other MCP-capable tools accept: Configure these values wherever the tool expects MCP server settings. -## Tools (16 total) +## Tools (17 total) -Each Kernel feature has a single `manage_*` tool with an `action` parameter, keeping the tool set small and consistent. Five standalone tools handle high-frequency workflows. +Each Kernel feature has a single `manage_*` tool with an `action` parameter, keeping the tool set small and consistent. Six standalone tools handle high-frequency workflows. Self-hosted deployments can hide sensitive tool families by setting `KERNEL_MCP_DISABLED_TOOLSETS` to a comma-separated list. For example, `KERNEL_MCP_DISABLED_TOOLSETS=api_keys` prevents `manage_api_keys` from being registered. @@ -281,6 +281,7 @@ Self-hosted deployments can hide sensitive tool families by setting `KERNEL_MCP_ - `browser_curl` - Send HTTP requests through an existing browser session's Chrome network stack. - `execute_playwright_code` - Execute Playwright/TypeScript code against a browser with automatic video replay and cleanup. - `exec_command` - Run shell commands inside a browser VM. Returns decoded stdout/stderr. +- `get_browser_telemetry` - Read archived telemetry events for a browser session (console, network, page lifecycle, captcha outcomes, VM health). Works for active and deleted sessions, with category filters, time windows, and pagination. - `search_docs` - Search Kernel platform documentation and guides. ## Resources diff --git a/src/lib/mcp/tools/browsers.ts b/src/lib/mcp/tools/browsers.ts index cf81464..4055bf0 100644 --- a/src/lib/mcp/tools/browsers.ts +++ b/src/lib/mcp/tools/browsers.ts @@ -592,9 +592,12 @@ export function registerBrowserCapabilities(server: McpServer) { if ( query.offset === undefined && query.since === undefined && + query.until === undefined && query.order !== "desc" ) { - // The API's since default is only 5m; cover the whole session. + // The API's since default is only 5m; cover the whole session. Not + // needed for until-only reads (the API starts those at the stream + // head), and injecting since there can invert the window. browser = await fetchBrowser(); query.since = browser?.created_at ?? "1970-01-01T00:00:00Z"; }