From e9a2a1f584d2ae70ca26b1c1f31e35bbb8a0588f Mon Sep 17 00:00:00 2001
From: rgarcia <72655+rgarcia@users.noreply.github.com>
Date: Wed, 12 Aug 2026 14:44:11 +0000
Subject: [PATCH 1/5] Allow raw telemetry page reads
---
src/lib/mcp/tools/browsers.test.ts | 125 +++++++++++++++++++++++++++++
src/lib/mcp/tools/browsers.ts | 27 +++++--
2 files changed, 147 insertions(+), 5 deletions(-)
create mode 100644 src/lib/mcp/tools/browsers.test.ts
diff --git a/src/lib/mcp/tools/browsers.test.ts b/src/lib/mcp/tools/browsers.test.ts
new file mode 100644
index 0000000..6adad5f
--- /dev/null
+++ b/src/lib/mcp/tools/browsers.test.ts
@@ -0,0 +1,125 @@
+///
+
+import { describe, expect, test } from "bun:test";
+import { connectTestMcp, toolResultJSON } from "@/lib/mcp/mcp-test-fixtures";
+import { registerBrowserCapabilities } from "@/lib/mcp/tools/browsers";
+
+describe("manage_browsers telemetry", () => {
+ test("re-fetches the same telemetry page without compaction", async () => {
+ const event = {
+ seq: 42,
+ event: {
+ ts: 1_700_000_000_000_000,
+ category: "network",
+ type: "network_response",
+ source: { service: "cdp" },
+ data: {
+ headers: { "set-cookie": "secret" },
+ post_data: "query=secret",
+ body: '{"result":"complete"}',
+ status: 200,
+ },
+ },
+ };
+ const queries: unknown[] = [];
+ const kernelClient = {
+ browsers: {
+ telemetry: {
+ events: async (_sessionId: string, query: unknown) => {
+ queries.push(query);
+ return {
+ getPaginatedItems: () => [event],
+ has_more: true,
+ next_offset: 43,
+ };
+ },
+ },
+ },
+ };
+ const { client, close } = await connectTestMcp(
+ registerBrowserCapabilities,
+ kernelClient,
+ );
+ const page = {
+ action: "get_telemetry",
+ session_id: "brr_123",
+ categories: ["network"],
+ offset: 41,
+ limit: 1,
+ order: "asc",
+ };
+
+ try {
+ const compact = toolResultJSON(
+ await client.callTool({
+ name: "manage_browsers",
+ arguments: page,
+ }),
+ );
+ const raw = toolResultJSON(
+ await client.callTool({
+ name: "manage_browsers",
+ arguments: { ...page, compact: false },
+ }),
+ );
+
+ expect(compact.items).toEqual([
+ {
+ seq: 42,
+ ts: 1_700_000_000_000_000,
+ time: "2023-11-14T22:13:20.000Z",
+ category: "network",
+ type: "network_response",
+ source: { service: "cdp" },
+ data: { status: 200 },
+ omitted_fields: ["headers", "post_data", "body"],
+ },
+ ]);
+ expect(raw).toEqual({
+ items: [event],
+ has_more: true,
+ next_offset: 43,
+ });
+ expect(queries).toEqual([
+ {
+ limit: 1,
+ category: ["network"],
+ offset: 41,
+ order: "asc",
+ },
+ {
+ limit: 1,
+ category: ["network"],
+ offset: 41,
+ order: "asc",
+ },
+ ]);
+ } finally {
+ await close();
+ }
+ });
+
+ test("documents compact telemetry recovery on the tool schema", async () => {
+ const kernelClient = { browsers: {} };
+ const { client, close } = await connectTestMcp(
+ registerBrowserCapabilities,
+ kernelClient,
+ );
+
+ try {
+ const tools = await client.listTools();
+ const tool = tools.tools.find(({ name }) => name === "manage_browsers");
+ const compact = tool?.inputSchema.properties?.compact as
+ | { description?: string }
+ | undefined;
+
+ expect(compact?.description).toContain(
+ "body, headers, post_data, or png",
+ );
+ expect(compact?.description).toContain("same categories, offset");
+ expect(compact?.description).toContain("not next_offset");
+ } finally {
+ await close();
+ }
+ });
+});
diff --git a/src/lib/mcp/tools/browsers.ts b/src/lib/mcp/tools/browsers.ts
index d683445..7aa0bda 100644
--- a/src/lib/mcp/tools/browsers.ts
+++ b/src/lib/mcp/tools/browsers.ts
@@ -6,7 +6,11 @@ import {
buildBrowserUpdateConfig,
type BrowserConfigResult,
} from "@/lib/mcp/browser-config";
-import { createKernelClient, type KernelClient } from "@/lib/mcp/kernel-client";
+import {
+ defaultMcpDependencies,
+ type McpDependencies,
+} from "@/lib/mcp/dependencies";
+import type { KernelClient } from "@/lib/mcp/kernel-client";
import {
registerJsonResourceCollection,
registerJsonResourceTemplate,
@@ -201,6 +205,7 @@ type BrowserTelemetryReadParams = {
since?: string;
until?: string;
order?: "asc" | "desc";
+ compact?: boolean;
};
async function readBrowserTelemetry(
@@ -233,7 +238,9 @@ async function readBrowserTelemetry(
const fullSessionRead = unfilteredExceptSince && params.since === undefined;
const page = await client.browsers.telemetry.events(params.session_id, query);
- const items = page.getPaginatedItems().map(compactTelemetryEvent);
+ const pageItems = page.getPaginatedItems();
+ const items =
+ params.compact === false ? pageItems : pageItems.map(compactTelemetryEvent);
// Counter-steer the pagination reflex: an unfiltered ascending read starts
// at session creation, so an agent chasing a recent failure should flip to
@@ -318,7 +325,10 @@ function buildSshPortForwardingInfo(
};
}
-export function registerBrowserCapabilities(server: McpServer) {
+export function registerBrowserCapabilities(
+ server: McpServer,
+ dependencies: McpDependencies = defaultMcpDependencies,
+) {
registerJsonResourceCollection(server, {
name: "browsers",
uriTemplate: "kernel://orgs/{organizationId}/projects/{projectId}/browsers",
@@ -344,7 +354,7 @@ 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, and "delete" when finished. get_telemetry compacts events by default; set compact=false on a narrowly filtered page when raw headers, request data, response bodies, or other omitted fields are needed.',
{
...projectSelectionInputSchema(),
action: z
@@ -511,6 +521,12 @@ export function registerBrowserCapabilities(server: McpServer) {
"(get_telemetry) Read direction. asc (default) reads oldest first from session start; desc reads newest first. Prefer desc when diagnosing a recent failure in a long session — it reaches the end without paging. Preserve it while paging.",
)
.optional(),
+ compact: z
+ .boolean()
+ .describe(
+ "(get_telemetry) Defaults to true. Compact items flatten the event envelope, add an ISO time, and omit data fields named body, headers, post_data, or png plus any data field over 8 KiB; omitted_fields lists removals. To recover them, repeat the call with compact=false and the same categories, offset, since, until, order, and limit. Use the original page offset, not next_offset, which points to the following page. Raw events may be large, so narrow the categories, time window, and limit first.",
+ )
+ .optional(),
telemetry_enabled: z
.boolean()
.describe(
@@ -551,7 +567,7 @@ export function registerBrowserCapabilities(server: McpServer) {
},
async (params, extra) => {
if (!extra.authInfo) throw new Error("Authentication required");
- const client = createKernelClient(
+ const client = dependencies.createKernelClient(
extra.authInfo.token,
projectIDForOperation(extra.authInfo, params.project_id),
);
@@ -686,6 +702,7 @@ export function registerBrowserCapabilities(server: McpServer) {
since: params.since,
until: params.until,
order: params.order,
+ compact: params.compact,
});
}
case "delete": {
From 61eb4e653ab01acdf83772a3f03860f23c99211f Mon Sep 17 00:00:00 2001
From: rgarcia <72655+rgarcia@users.noreply.github.com>
Date: Wed, 12 Aug 2026 14:56:59 +0000
Subject: [PATCH 2/5] Wire browser resource dependencies
---
src/lib/mcp/tools/browsers.test.ts | 18 +++++++++++
src/lib/mcp/tools/browsers.ts | 49 ++++++++++++++++++------------
2 files changed, 47 insertions(+), 20 deletions(-)
diff --git a/src/lib/mcp/tools/browsers.test.ts b/src/lib/mcp/tools/browsers.test.ts
index 6adad5f..31b3efd 100644
--- a/src/lib/mcp/tools/browsers.test.ts
+++ b/src/lib/mcp/tools/browsers.test.ts
@@ -24,6 +24,10 @@ describe("manage_browsers telemetry", () => {
const queries: unknown[] = [];
const kernelClient = {
browsers: {
+ list: async function* () {
+ yield { session_id: "brr_123" };
+ },
+ retrieve: async (sessionId: string) => ({ session_id: sessionId }),
telemetry: {
events: async (_sessionId: string, query: unknown) => {
queries.push(query);
@@ -94,6 +98,20 @@ describe("manage_browsers telemetry", () => {
order: "asc",
},
]);
+
+ const collection = await client.readResource({
+ uri: "kernel://orgs/org_test/projects/proj_test/browsers",
+ });
+ const item = await client.readResource({
+ uri: "kernel://orgs/org_test/projects/proj_test/browsers/brr_123",
+ });
+ const collectionContent = collection.contents[0];
+ const itemContent = item.contents[0];
+ const collectionText =
+ "text" in collectionContent ? collectionContent.text : "";
+ const itemText = "text" in itemContent ? itemContent.text : "";
+ expect(JSON.parse(collectionText)).toEqual([{ session_id: "brr_123" }]);
+ expect(JSON.parse(itemText)).toEqual({ session_id: "brr_123" });
} finally {
await close();
}
diff --git a/src/lib/mcp/tools/browsers.ts b/src/lib/mcp/tools/browsers.ts
index 7aa0bda..3628695 100644
--- a/src/lib/mcp/tools/browsers.ts
+++ b/src/lib/mcp/tools/browsers.ts
@@ -329,27 +329,36 @@ export function registerBrowserCapabilities(
server: McpServer,
dependencies: McpDependencies = defaultMcpDependencies,
) {
- registerJsonResourceCollection(server, {
- name: "browsers",
- uriTemplate: "kernel://orgs/{organizationId}/projects/{projectId}/browsers",
- emptyText: "No browsers found",
- read: async (client) => {
- const browsers = [];
- for await (const browser of client.browsers.list()) {
- browsers.push(browser);
- }
- return browsers;
+ registerJsonResourceCollection(
+ server,
+ {
+ name: "browsers",
+ uriTemplate:
+ "kernel://orgs/{organizationId}/projects/{projectId}/browsers",
+ emptyText: "No browsers found",
+ read: async (client) => {
+ const browsers = [];
+ for await (const browser of client.browsers.list()) {
+ browsers.push(browser);
+ }
+ return browsers;
+ },
+ },
+ dependencies,
+ );
+
+ registerJsonResourceTemplate(
+ server,
+ {
+ name: "browser",
+ uriTemplate:
+ "kernel://orgs/{organizationId}/projects/{projectId}/browsers/{sessionId}",
+ variableName: "sessionId",
+ resourceLabel: "Browser session",
+ read: (client, sessionId) => client.browsers.retrieve(sessionId),
},
- });
-
- registerJsonResourceTemplate(server, {
- name: "browser",
- uriTemplate:
- "kernel://orgs/{organizationId}/projects/{projectId}/browsers/{sessionId}",
- variableName: "sessionId",
- resourceLabel: "Browser session",
- read: (client, sessionId) => client.browsers.retrieve(sessionId),
- });
+ dependencies,
+ );
// manage_browsers -- Manage browser sessions and read archived telemetry
server.tool(
From f45b2f80127cba8e356528525db96c93bbb7881c Mon Sep 17 00:00:00 2001
From: rgarcia <72655+rgarcia@users.noreply.github.com>
Date: Wed, 12 Aug 2026 17:42:17 +0000
Subject: [PATCH 3/5] Stabilize raw telemetry replay
---
src/lib/mcp/tools/browsers.test.ts | 212 +++++++++++++++++++++--------
src/lib/mcp/tools/browsers.ts | 87 ++++++++++--
2 files changed, 229 insertions(+), 70 deletions(-)
diff --git a/src/lib/mcp/tools/browsers.test.ts b/src/lib/mcp/tools/browsers.test.ts
index 31b3efd..9be8635 100644
--- a/src/lib/mcp/tools/browsers.test.ts
+++ b/src/lib/mcp/tools/browsers.test.ts
@@ -4,45 +4,50 @@ import { describe, expect, test } from "bun:test";
import { connectTestMcp, toolResultJSON } from "@/lib/mcp/mcp-test-fixtures";
import { registerBrowserCapabilities } from "@/lib/mcp/tools/browsers";
-describe("manage_browsers telemetry", () => {
- test("re-fetches the same telemetry page without compaction", async () => {
- const event = {
- seq: 42,
- event: {
- ts: 1_700_000_000_000_000,
- category: "network",
- type: "network_response",
- source: { service: "cdp" },
- data: {
- headers: { "set-cookie": "secret" },
- post_data: "query=secret",
- body: '{"result":"complete"}',
- status: 200,
+const event = {
+ seq: 42,
+ event: {
+ ts: 1_700_000_000_000_000,
+ category: "network",
+ type: "network_response",
+ source: { service: "cdp" },
+ truncated: true,
+ data: {
+ headers: { "set-cookie": "secret" },
+ post_data: "query=secret",
+ body: '{"result":"complete"}',
+ status: 200,
+ },
+ },
+};
+
+function telemetryClient(queries: unknown[]) {
+ return {
+ browsers: {
+ telemetry: {
+ events: async (_sessionId: string, query: unknown) => {
+ queries.push(query);
+ return {
+ getPaginatedItems: () => [event],
+ has_more: true,
+ next_offset: 43,
+ };
},
},
- };
+ },
+ };
+}
+
+type TextResourceResult = {
+ contents: Array<{ text: string }>;
+};
+
+describe("manage_browsers telemetry", () => {
+ test("re-fetches the same cursor page without compaction", async () => {
const queries: unknown[] = [];
- const kernelClient = {
- browsers: {
- list: async function* () {
- yield { session_id: "brr_123" };
- },
- retrieve: async (sessionId: string) => ({ session_id: sessionId }),
- telemetry: {
- events: async (_sessionId: string, query: unknown) => {
- queries.push(query);
- return {
- getPaginatedItems: () => [event],
- has_more: true,
- next_offset: 43,
- };
- },
- },
- },
- };
const { client, close } = await connectTestMcp(
registerBrowserCapabilities,
- kernelClient,
+ telemetryClient(queries),
);
const page = {
action: "get_telemetry",
@@ -63,7 +68,7 @@ describe("manage_browsers telemetry", () => {
const raw = toolResultJSON(
await client.callTool({
name: "manage_browsers",
- arguments: { ...page, compact: false },
+ arguments: compact.raw_replay as Record,
}),
);
@@ -76,42 +81,129 @@ describe("manage_browsers telemetry", () => {
type: "network_response",
source: { service: "cdp" },
data: { status: 200 },
+ truncated: true,
omitted_fields: ["headers", "post_data", "body"],
},
]);
+ expect(compact.raw_replay).toMatchObject({
+ ...page,
+ compact: false,
+ });
+ expect(Date.parse(compact.raw_replay.until as string)).toBeNumber();
expect(raw).toEqual({
items: [event],
has_more: true,
next_offset: 43,
});
- expect(queries).toEqual([
- {
- limit: 1,
- category: ["network"],
- offset: 41,
- order: "asc",
- },
- {
- limit: 1,
- category: ["network"],
- offset: 41,
- order: "asc",
+ expect(queries).toHaveLength(2);
+ expect(queries[1]).toEqual(queries[0]);
+ } finally {
+ await close();
+ }
+ });
+
+ test("pins an offset-less newest-first page for raw replay", async () => {
+ const queries: unknown[] = [];
+ const { client, close } = await connectTestMcp(
+ registerBrowserCapabilities,
+ telemetryClient(queries),
+ );
+
+ try {
+ const compact = toolResultJSON(
+ await client.callTool({
+ name: "manage_browsers",
+ arguments: {
+ action: "get_telemetry",
+ session_id: "brr_123",
+ categories: ["network"],
+ limit: 1,
+ order: "desc",
+ },
+ }),
+ );
+ await client.callTool({
+ name: "manage_browsers",
+ arguments: compact.raw_replay as Record,
+ });
+
+ expect(compact.raw_replay).toMatchObject({
+ action: "get_telemetry",
+ session_id: "brr_123",
+ categories: ["network"],
+ limit: 1,
+ order: "desc",
+ compact: false,
+ });
+ expect(Date.parse(compact.raw_replay.until as string)).toBeNumber();
+ expect(queries[1]).toEqual(queries[0]);
+ } finally {
+ await close();
+ }
+ });
+
+ test("resolves relative windows once for raw replay", async () => {
+ const queries: unknown[] = [];
+ const { client, close } = await connectTestMcp(
+ registerBrowserCapabilities,
+ telemetryClient(queries),
+ );
+
+ try {
+ const compact = toolResultJSON(
+ await client.callTool({
+ name: "manage_browsers",
+ arguments: {
+ action: "get_telemetry",
+ session_id: "brr_123",
+ since: "30m",
+ until: "5m",
+ limit: 1,
+ },
+ }),
+ );
+ await client.callTool({
+ name: "manage_browsers",
+ arguments: compact.raw_replay as Record,
+ });
+
+ const since = Date.parse(compact.raw_replay.since as string);
+ const until = Date.parse(compact.raw_replay.until as string);
+ expect(until - since).toBe(25 * 60 * 1_000);
+ expect(queries[1]).toEqual(queries[0]);
+ } finally {
+ await close();
+ }
+ });
+
+ test("serves browser resources through injected dependencies", async () => {
+ const kernelClient = {
+ browsers: {
+ list: async function* () {
+ yield { session_id: "brr_123" };
},
- ]);
+ retrieve: async (sessionId: string) => ({ session_id: sessionId }),
+ },
+ };
+ const { client, close } = await connectTestMcp(
+ registerBrowserCapabilities,
+ kernelClient,
+ );
- const collection = await client.readResource({
+ try {
+ const collection = (await client.readResource({
uri: "kernel://orgs/org_test/projects/proj_test/browsers",
- });
- const item = await client.readResource({
+ })) as TextResourceResult;
+ const item = (await client.readResource({
uri: "kernel://orgs/org_test/projects/proj_test/browsers/brr_123",
+ })) as TextResourceResult;
+
+ expect(JSON.parse(collection.contents[0].text)).toEqual([
+ { session_id: "brr_123" },
+ ]);
+ expect(JSON.parse(item.contents[0].text)).toEqual({
+ session_id: "brr_123",
});
- const collectionContent = collection.contents[0];
- const itemContent = item.contents[0];
- const collectionText =
- "text" in collectionContent ? collectionContent.text : "";
- const itemText = "text" in itemContent ? itemContent.text : "";
- expect(JSON.parse(collectionText)).toEqual([{ session_id: "brr_123" }]);
- expect(JSON.parse(itemText)).toEqual({ session_id: "brr_123" });
} finally {
await close();
}
@@ -134,8 +226,8 @@ describe("manage_browsers telemetry", () => {
expect(compact?.description).toContain(
"body, headers, post_data, or png",
);
- expect(compact?.description).toContain("same categories, offset");
- expect(compact?.description).toContain("not next_offset");
+ expect(compact?.description).toContain("exact page");
+ expect(compact?.description).toContain("raw_replay");
} finally {
await close();
}
diff --git a/src/lib/mcp/tools/browsers.ts b/src/lib/mcp/tools/browsers.ts
index 3628695..f5c53b3 100644
--- a/src/lib/mcp/tools/browsers.ts
+++ b/src/lib/mcp/tools/browsers.ts
@@ -199,6 +199,7 @@ function compactTelemetryEvent({ seq, event }: TelemetryEnvelope) {
type BrowserTelemetryReadParams = {
session_id: string;
+ project_id?: string;
categories?: TelemetryEventsQuery["category"];
limit?: number;
offset?: number;
@@ -208,25 +209,73 @@ type BrowserTelemetryReadParams = {
compact?: boolean;
};
+const telemetryDurationUnitsInMilliseconds = {
+ ns: 1 / 1_000_000,
+ us: 1 / 1_000,
+ µs: 1 / 1_000,
+ μs: 1 / 1_000,
+ ms: 1,
+ s: 1_000,
+ m: 60_000,
+ h: 3_600_000,
+} as const;
+
+function resolveTelemetryTimeParam(value: string, now: Date) {
+ if (/^[+-]?0$/.test(value)) return now.toISOString();
+
+ const sign = value.startsWith("-") ? -1 : 1;
+ const duration = value.match(/^[+-]?(.*)$/)?.[1] ?? value;
+ const parts = duration.matchAll(
+ /((?:\d+(?:\.\d*)?|\.\d+))(ns|us|µs|μs|ms|s|m|h)/g,
+ );
+ let milliseconds = 0;
+ let parsedLength = 0;
+ for (const part of parts) {
+ if (part.index !== parsedLength) return value;
+ milliseconds +=
+ Number(part[1]) *
+ telemetryDurationUnitsInMilliseconds[
+ part[2] as keyof typeof telemetryDurationUnitsInMilliseconds
+ ];
+ parsedLength += part[0].length;
+ }
+ if (
+ parsedLength !== duration.length ||
+ parsedLength === 0 ||
+ !Number.isFinite(milliseconds)
+ ) {
+ return value;
+ }
+ const resolved = new Date(now.getTime() - sign * milliseconds);
+ return Number.isNaN(resolved.getTime()) ? value : resolved.toISOString();
+}
+
async function readBrowserTelemetry(
client: KernelClient,
params: BrowserTelemetryReadParams,
) {
- const query: TelemetryEventsQuery = { limit: params.limit ?? 100 };
+ // Pin the page to one absolute window so raw_replay cannot move as new
+ // telemetry arrives or relative time bounds age.
+ const now = new Date();
+ const query: TelemetryEventsQuery = {
+ limit: params.limit ?? 100,
+ until: params.until
+ ? resolveTelemetryTimeParam(params.until, now)
+ : now.toISOString(),
+ order: params.order ?? "asc",
+ };
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;
+ if (params.offset !== undefined) {
+ query.offset = params.offset;
+ } else if (params.since !== undefined) {
+ query.since = resolveTelemetryTimeParam(params.since, now);
+ }
// Avoid the API's five-minute default. The archive can't predate the
- // session, so the epoch reads the full session without a browser lookup;
- // until-only reads already start at the stream head and desc reads anchor
- // at the stream tail.
+ // session, so the epoch reads the full session without a browser lookup.
if (
query.offset === undefined &&
query.since === undefined &&
- query.until === undefined &&
query.order !== "desc"
) {
query.since = "1970-01-01T00:00:00Z";
@@ -266,6 +315,22 @@ async function readBrowserTelemetry(
})
: ascPagingNote;
+ const rawReplay =
+ params.compact === false
+ ? undefined
+ : {
+ action: "get_telemetry",
+ session_id: params.session_id,
+ ...(params.project_id && { project_id: params.project_id }),
+ ...(query.category && { categories: query.category }),
+ limit: query.limit,
+ ...(query.offset !== undefined && { offset: query.offset }),
+ ...(query.since && { since: query.since }),
+ ...(query.until && { until: query.until }),
+ order: query.order,
+ compact: false,
+ };
+
// Single-line JSON rather than the pretty-printed house helpers: a page
// carries up to 100 events and indentation would inflate the token cost.
return textResponse(
@@ -273,6 +338,7 @@ async function readBrowserTelemetry(
items,
has_more: page.has_more,
next_offset: page.next_offset,
+ ...(rawReplay && { raw_replay: rawReplay }),
...(note && { note }),
}),
);
@@ -533,7 +599,7 @@ export function registerBrowserCapabilities(
compact: z
.boolean()
.describe(
- "(get_telemetry) Defaults to true. Compact items flatten the event envelope, add an ISO time, and omit data fields named body, headers, post_data, or png plus any data field over 8 KiB; omitted_fields lists removals. To recover them, repeat the call with compact=false and the same categories, offset, since, until, order, and limit. Use the original page offset, not next_offset, which points to the following page. Raw events may be large, so narrow the categories, time window, and limit first.",
+ "(get_telemetry) Defaults to true. Compact items flatten the event envelope, add an ISO time, and omit data fields named body, headers, post_data, or png plus any data field over 8 KiB; omitted_fields lists removals. To recover them from the exact page, call manage_browsers with the returned raw_replay arguments. Raw events may be large, so narrow the categories, time window, and limit first.",
)
.optional(),
telemetry_enabled: z
@@ -705,6 +771,7 @@ export function registerBrowserCapabilities(
}
return await readBrowserTelemetry(client, {
session_id: params.session_id,
+ project_id: params.project_id,
categories: params.categories,
limit: params.limit,
offset: params.offset,
From b7e0866a93ba659d4211c1ce91eb12a9430b8df6 Mon Sep 17 00:00:00 2001
From: rgarcia <72655+rgarcia@users.noreply.github.com>
Date: Wed, 12 Aug 2026 17:56:33 +0000
Subject: [PATCH 4/5] Simplify telemetry duration parsing
---
src/lib/mcp/tools/browsers.test.ts | 52 ++++++++++++++++--------------
src/lib/mcp/tools/browsers.ts | 27 ++++++----------
2 files changed, 36 insertions(+), 43 deletions(-)
diff --git a/src/lib/mcp/tools/browsers.test.ts b/src/lib/mcp/tools/browsers.test.ts
index 9be8635..531dbec 100644
--- a/src/lib/mcp/tools/browsers.test.ts
+++ b/src/lib/mcp/tools/browsers.test.ts
@@ -176,7 +176,33 @@ describe("manage_browsers telemetry", () => {
}
});
- test("serves browser resources through injected dependencies", async () => {
+ test("documents compact telemetry recovery on the tool schema", async () => {
+ const kernelClient = { browsers: {} };
+ const { client, close } = await connectTestMcp(
+ registerBrowserCapabilities,
+ kernelClient,
+ );
+
+ try {
+ const tools = await client.listTools();
+ const tool = tools.tools.find(({ name }) => name === "manage_browsers");
+ const compact = tool?.inputSchema.properties?.compact as
+ | { description?: string }
+ | undefined;
+
+ expect(compact?.description).toContain(
+ "body, headers, post_data, or png",
+ );
+ expect(compact?.description).toContain("exact page");
+ expect(compact?.description).toContain("raw_replay");
+ } finally {
+ await close();
+ }
+ });
+});
+
+describe("browser resources", () => {
+ test("uses injected dependencies", async () => {
const kernelClient = {
browsers: {
list: async function* () {
@@ -208,28 +234,4 @@ describe("manage_browsers telemetry", () => {
await close();
}
});
-
- test("documents compact telemetry recovery on the tool schema", async () => {
- const kernelClient = { browsers: {} };
- const { client, close } = await connectTestMcp(
- registerBrowserCapabilities,
- kernelClient,
- );
-
- try {
- const tools = await client.listTools();
- const tool = tools.tools.find(({ name }) => name === "manage_browsers");
- const compact = tool?.inputSchema.properties?.compact as
- | { description?: string }
- | undefined;
-
- expect(compact?.description).toContain(
- "body, headers, post_data, or png",
- );
- expect(compact?.description).toContain("exact page");
- expect(compact?.description).toContain("raw_replay");
- } finally {
- await close();
- }
- });
});
diff --git a/src/lib/mcp/tools/browsers.ts b/src/lib/mcp/tools/browsers.ts
index f5c53b3..bb99dba 100644
--- a/src/lib/mcp/tools/browsers.ts
+++ b/src/lib/mcp/tools/browsers.ts
@@ -220,31 +220,22 @@ const telemetryDurationUnitsInMilliseconds = {
h: 3_600_000,
} as const;
+const telemetryDurationPart =
+ /((?:\d+(?:\.\d*)?|\.\d+))(ns|us|µs|μs|ms|s|m|h)/g;
+const telemetryDuration =
+ /^[+-]?(?:0|(?:(?:\d+(?:\.\d*)?|\.\d+)(?:ns|us|µs|μs|ms|s|m|h))+)$/;
+
function resolveTelemetryTimeParam(value: string, now: Date) {
- if (/^[+-]?0$/.test(value)) return now.toISOString();
+ if (!telemetryDuration.test(value)) return value;
const sign = value.startsWith("-") ? -1 : 1;
- const duration = value.match(/^[+-]?(.*)$/)?.[1] ?? value;
- const parts = duration.matchAll(
- /((?:\d+(?:\.\d*)?|\.\d+))(ns|us|µs|μs|ms|s|m|h)/g,
- );
let milliseconds = 0;
- let parsedLength = 0;
- for (const part of parts) {
- if (part.index !== parsedLength) return value;
+ for (const [, amount, unit] of value.matchAll(telemetryDurationPart)) {
milliseconds +=
- Number(part[1]) *
+ Number(amount) *
telemetryDurationUnitsInMilliseconds[
- part[2] as keyof typeof telemetryDurationUnitsInMilliseconds
+ unit as keyof typeof telemetryDurationUnitsInMilliseconds
];
- parsedLength += part[0].length;
- }
- if (
- parsedLength !== duration.length ||
- parsedLength === 0 ||
- !Number.isFinite(milliseconds)
- ) {
- return value;
}
const resolved = new Date(now.getTime() - sign * milliseconds);
return Number.isNaN(resolved.getTime()) ? value : resolved.toISOString();
From 53ca6b7d3e2a69a95c45d55b4ce5347cec19bc9a Mon Sep 17 00:00:00 2001
From: rgarcia <72655+rgarcia@users.noreply.github.com>
Date: Wed, 12 Aug 2026 20:56:38 +0000
Subject: [PATCH 5/5] Bound raw telemetry responses
---
src/lib/mcp/tools/browsers.test.ts | 225 +++++++++++++++++++++++------
src/lib/mcp/tools/browsers.ts | 131 +++++++++--------
2 files changed, 254 insertions(+), 102 deletions(-)
diff --git a/src/lib/mcp/tools/browsers.test.ts b/src/lib/mcp/tools/browsers.test.ts
index 531dbec..2e07300 100644
--- a/src/lib/mcp/tools/browsers.test.ts
+++ b/src/lib/mcp/tools/browsers.test.ts
@@ -1,5 +1,6 @@
///
+import type { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { describe, expect, test } from "bun:test";
import { connectTestMcp, toolResultJSON } from "@/lib/mcp/mcp-test-fixtures";
import { registerBrowserCapabilities } from "@/lib/mcp/tools/browsers";
@@ -21,14 +22,14 @@ const event = {
},
};
-function telemetryClient(queries: unknown[]) {
+function telemetryClient(queries: unknown[], items: unknown[] = [event]) {
return {
browsers: {
telemetry: {
events: async (_sessionId: string, query: unknown) => {
queries.push(query);
return {
- getPaginatedItems: () => [event],
+ getPaginatedItems: () => items,
has_more: true,
next_offset: 43,
};
@@ -38,12 +39,17 @@ function telemetryClient(queries: unknown[]) {
};
}
+function toolResultText(result: Awaited>) {
+ const content = result.content as Array<{ type: string; text: string }>;
+ return content[0].text;
+}
+
type TextResourceResult = {
contents: Array<{ text: string }>;
};
describe("manage_browsers telemetry", () => {
- test("re-fetches the same cursor page without compaction", async () => {
+ test("re-fetches a cursor page without compaction", async () => {
const queries: unknown[] = [];
const { client, close } = await connectTestMcp(
registerBrowserCapabilities,
@@ -68,7 +74,7 @@ describe("manage_browsers telemetry", () => {
const raw = toolResultJSON(
await client.callTool({
name: "manage_browsers",
- arguments: compact.raw_replay as Record,
+ arguments: compact.raw_replay_best_effort,
}),
);
@@ -85,24 +91,35 @@ describe("manage_browsers telemetry", () => {
omitted_fields: ["headers", "post_data", "body"],
},
]);
- expect(compact.raw_replay).toMatchObject({
+ expect(compact.raw_replay_best_effort).toEqual({
...page,
compact: false,
});
- expect(Date.parse(compact.raw_replay.until as string)).toBeNumber();
expect(raw).toEqual({
items: [event],
has_more: true,
next_offset: 43,
});
- expect(queries).toHaveLength(2);
- expect(queries[1]).toEqual(queries[0]);
+ expect(queries).toEqual([
+ {
+ limit: 1,
+ category: ["network"],
+ offset: 41,
+ order: "asc",
+ },
+ {
+ limit: 1,
+ category: ["network"],
+ offset: 41,
+ order: "asc",
+ },
+ ]);
} finally {
await close();
}
});
- test("pins an offset-less newest-first page for raw replay", async () => {
+ test("preserves API time and tail semantics", async () => {
const queries: unknown[] = [];
const { client, close } = await connectTestMcp(
registerBrowserCapabilities,
@@ -110,7 +127,7 @@ describe("manage_browsers telemetry", () => {
);
try {
- const compact = toolResultJSON(
+ const descending = toolResultJSON(
await client.callTool({
name: "manage_browsers",
arguments: {
@@ -122,27 +139,119 @@ describe("manage_browsers telemetry", () => {
},
}),
);
- await client.callTool({
+ const relative = toolResultJSON(
+ await client.callTool({
+ name: "manage_browsers",
+ arguments: {
+ action: "get_telemetry",
+ session_id: "brr_123",
+ categories: ["network"],
+ since: "30m",
+ until: "5m",
+ limit: 1,
+ },
+ }),
+ );
+
+ expect(queries).toEqual([
+ { limit: 1, category: ["network"], order: "desc" },
+ {
+ limit: 1,
+ category: ["network"],
+ since: "30m",
+ until: "5m",
+ },
+ ]);
+ expect(descending.raw_replay_best_effort).not.toHaveProperty("until");
+ expect(relative.raw_replay_best_effort).toMatchObject({
+ since: "30m",
+ until: "5m",
+ compact: false,
+ });
+ } finally {
+ await close();
+ }
+ });
+
+ test("requires an explicit small raw limit", async () => {
+ const queries: unknown[] = [];
+ const { client, close } = await connectTestMcp(
+ registerBrowserCapabilities,
+ telemetryClient(queries),
+ );
+
+ try {
+ const missing = await client.callTool({
+ name: "manage_browsers",
+ arguments: {
+ action: "get_telemetry",
+ session_id: "brr_123",
+ categories: ["network"],
+ compact: false,
+ },
+ });
+ const tooLarge = await client.callTool({
name: "manage_browsers",
- arguments: compact.raw_replay as Record,
+ arguments: {
+ action: "get_telemetry",
+ session_id: "brr_123",
+ categories: ["network"],
+ limit: 6,
+ compact: false,
+ },
});
- expect(compact.raw_replay).toMatchObject({
- action: "get_telemetry",
- session_id: "brr_123",
- categories: ["network"],
- limit: 1,
- order: "desc",
- compact: false,
+ expect(missing.isError).toBeTrue();
+ expect(toolResultText(missing)).toContain(
+ "explicit limit between 1 and 5",
+ );
+ expect(tooLarge.isError).toBeTrue();
+ expect(toolResultText(tooLarge)).toContain(
+ "explicit limit between 1 and 5",
+ );
+ expect(queries).toEqual([]);
+ } finally {
+ await close();
+ }
+ });
+
+ test("caps the serialized raw response", async () => {
+ const queries: unknown[] = [];
+ const oversizedEvents = [1, 2, 3].map((seq) => ({
+ ...event,
+ seq,
+ event: {
+ ...event.event,
+ data: { body: "x".repeat(400_000) },
+ },
+ }));
+ const { client, close } = await connectTestMcp(
+ registerBrowserCapabilities,
+ telemetryClient(queries, oversizedEvents),
+ );
+
+ try {
+ const result = await client.callTool({
+ name: "manage_browsers",
+ arguments: {
+ action: "get_telemetry",
+ session_id: "brr_123",
+ categories: ["network"],
+ limit: 3,
+ compact: false,
+ },
});
- expect(Date.parse(compact.raw_replay.until as string)).toBeNumber();
- expect(queries[1]).toEqual(queries[0]);
+
+ expect(result.isError).toBeTrue();
+ expect(toolResultText(result)).toContain(
+ "raw telemetry response exceeds 1048576 bytes",
+ );
} finally {
await close();
}
});
- test("resolves relative windows once for raw replay", async () => {
+ test("keeps screenshot PNGs out of raw JSON", async () => {
const queries: unknown[] = [];
const { client, close } = await connectTestMcp(
registerBrowserCapabilities,
@@ -150,33 +259,61 @@ describe("manage_browsers telemetry", () => {
);
try {
- const compact = toolResultJSON(
- await client.callTool({
- name: "manage_browsers",
- arguments: {
- action: "get_telemetry",
- session_id: "brr_123",
- since: "30m",
- until: "5m",
- limit: 1,
- },
- }),
+ const result = await client.callTool({
+ name: "manage_browsers",
+ arguments: {
+ action: "get_telemetry",
+ session_id: "brr_123",
+ categories: ["screenshot"],
+ limit: 1,
+ compact: false,
+ },
+ });
+
+ expect(result.isError).toBeTrue();
+ expect(toolResultText(result)).toContain(
+ "does not support the screenshot category",
);
- await client.callTool({
+ expect(queries).toEqual([]);
+ } finally {
+ await close();
+ }
+ });
+
+ test("rejects PNG fields returned under another category", async () => {
+ const queries: unknown[] = [];
+ const pngEvent = {
+ ...event,
+ event: { ...event.event, data: { png: "base64" } },
+ };
+ const { client, close } = await connectTestMcp(
+ registerBrowserCapabilities,
+ telemetryClient(queries, [pngEvent]),
+ );
+
+ try {
+ const result = await client.callTool({
name: "manage_browsers",
- arguments: compact.raw_replay as Record,
+ arguments: {
+ action: "get_telemetry",
+ session_id: "brr_123",
+ categories: ["network"],
+ limit: 1,
+ compact: false,
+ },
});
- const since = Date.parse(compact.raw_replay.since as string);
- const until = Date.parse(compact.raw_replay.until as string);
- expect(until - since).toBe(25 * 60 * 1_000);
- expect(queries[1]).toEqual(queries[0]);
+ expect(result.isError).toBeTrue();
+ expect(toolResultText(result)).toContain(
+ "Raw screenshot PNGs are not available",
+ );
+ expect(queries).toHaveLength(1);
} finally {
await close();
}
});
- test("documents compact telemetry recovery on the tool schema", async () => {
+ test("documents bounded best-effort raw replay", async () => {
const kernelClient = { browsers: {} };
const { client, close } = await connectTestMcp(
registerBrowserCapabilities,
@@ -193,8 +330,12 @@ describe("manage_browsers telemetry", () => {
expect(compact?.description).toContain(
"body, headers, post_data, or png",
);
- expect(compact?.description).toContain("exact page");
- expect(compact?.description).toContain("raw_replay");
+ expect(compact?.description).toContain("raw_replay_best_effort");
+ expect(compact?.description).toContain(
+ "late events or retention may change results",
+ );
+ expect(compact?.description).toContain("limit<=5");
+ expect(compact?.description).toContain("1 MiB");
} finally {
await close();
}
diff --git a/src/lib/mcp/tools/browsers.ts b/src/lib/mcp/tools/browsers.ts
index bb99dba..fde5dac 100644
--- a/src/lib/mcp/tools/browsers.ts
+++ b/src/lib/mcp/tools/browsers.ts
@@ -209,64 +209,58 @@ type BrowserTelemetryReadParams = {
compact?: boolean;
};
-const telemetryDurationUnitsInMilliseconds = {
- ns: 1 / 1_000_000,
- us: 1 / 1_000,
- µs: 1 / 1_000,
- μs: 1 / 1_000,
- ms: 1,
- s: 1_000,
- m: 60_000,
- h: 3_600_000,
-} as const;
-
-const telemetryDurationPart =
- /((?:\d+(?:\.\d*)?|\.\d+))(ns|us|µs|μs|ms|s|m|h)/g;
-const telemetryDuration =
- /^[+-]?(?:0|(?:(?:\d+(?:\.\d*)?|\.\d+)(?:ns|us|µs|μs|ms|s|m|h))+)$/;
-
-function resolveTelemetryTimeParam(value: string, now: Date) {
- if (!telemetryDuration.test(value)) return value;
-
- const sign = value.startsWith("-") ? -1 : 1;
- let milliseconds = 0;
- for (const [, amount, unit] of value.matchAll(telemetryDurationPart)) {
- milliseconds +=
- Number(amount) *
- telemetryDurationUnitsInMilliseconds[
- unit as keyof typeof telemetryDurationUnitsInMilliseconds
- ];
+const maxRawTelemetryEvents = 5;
+// Producers cap each archived record at 1,000,000 bytes. This leaves room for
+// the page envelope while ensuring one response cannot carry multiple max-size records.
+const maxRawTelemetryResponseBytes = 1024 * 1024;
+
+function rawTelemetryPageError(items: TelemetryEnvelope[]) {
+ for (const { event } of items) {
+ const data = "data" in event ? event.data : undefined;
+ if (data && typeof data === "object" && "png" in data) {
+ return "Raw screenshot PNGs are not available in JSON telemetry responses.";
+ }
}
- const resolved = new Date(now.getTime() - sign * milliseconds);
- return Number.isNaN(resolved.getTime()) ? value : resolved.toISOString();
+ return undefined;
}
async function readBrowserTelemetry(
client: KernelClient,
params: BrowserTelemetryReadParams,
) {
- // Pin the page to one absolute window so raw_replay cannot move as new
- // telemetry arrives or relative time bounds age.
- const now = new Date();
- const query: TelemetryEventsQuery = {
- limit: params.limit ?? 100,
- until: params.until
- ? resolveTelemetryTimeParam(params.until, now)
- : now.toISOString(),
- order: params.order ?? "asc",
- };
- if (params.categories) query.category = params.categories;
- if (params.offset !== undefined) {
- query.offset = params.offset;
- } else if (params.since !== undefined) {
- query.since = resolveTelemetryTimeParam(params.since, now);
+ if (params.compact === false) {
+ if (params.limit === undefined || params.limit > maxRawTelemetryEvents) {
+ return errorResponse(
+ `Error: compact=false requires an explicit limit between 1 and ${maxRawTelemetryEvents}.`,
+ );
+ }
+ if (!params.categories) {
+ return errorResponse(
+ "Error: compact=false requires at least one explicit category.",
+ );
+ }
+ if (params.categories.includes("screenshot")) {
+ return errorResponse(
+ "Error: compact=false does not support the screenshot category because PNGs are not returned in JSON.",
+ );
+ }
}
+ 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;
+
// Avoid the API's five-minute default. The archive can't predate the
- // session, so the epoch reads the full session without a browser lookup.
+ // session, so the epoch reads the full session without a browser lookup;
+ // until-only reads already start at the stream head and desc reads anchor
+ // at the stream tail.
if (
query.offset === undefined &&
query.since === undefined &&
+ query.until === undefined &&
query.order !== "desc"
) {
query.since = "1970-01-01T00:00:00Z";
@@ -306,33 +300,50 @@ async function readBrowserTelemetry(
})
: ascPagingNote;
- const rawReplay =
- params.compact === false
+ const rawReplayBestEffort =
+ params.compact === false ||
+ !query.category ||
+ query.category.includes("screenshot")
? undefined
: {
action: "get_telemetry",
session_id: params.session_id,
...(params.project_id && { project_id: params.project_id }),
...(query.category && { categories: query.category }),
- limit: query.limit,
+ limit: Math.min(query.limit ?? 100, maxRawTelemetryEvents),
...(query.offset !== undefined && { offset: query.offset }),
...(query.since && { since: query.since }),
...(query.until && { until: query.until }),
- order: query.order,
+ ...(query.order && { order: query.order }),
compact: false,
};
+ const response = {
+ items,
+ has_more: page.has_more,
+ next_offset: page.next_offset,
+ ...(rawReplayBestEffort && {
+ raw_replay_best_effort: rawReplayBestEffort,
+ }),
+ ...(note && { note }),
+ };
+ if (params.compact === false) {
+ const rawError = rawTelemetryPageError(pageItems);
+ if (rawError) return errorResponse(`Error: ${rawError}`);
+ }
+
// Single-line JSON rather than the pretty-printed house helpers: a page
// carries up to 100 events and indentation would inflate the token cost.
- return textResponse(
- JSON.stringify({
- items,
- has_more: page.has_more,
- next_offset: page.next_offset,
- ...(rawReplay && { raw_replay: rawReplay }),
- ...(note && { note }),
- }),
- );
+ const serializedResponse = JSON.stringify(response);
+ if (
+ params.compact === false &&
+ Buffer.byteLength(serializedResponse, "utf8") > maxRawTelemetryResponseBytes
+ ) {
+ return errorResponse(
+ `Error: raw telemetry response exceeds ${maxRawTelemetryResponseBytes} bytes. Reduce limit or narrow the category and time window.`,
+ );
+ }
+ return textResponse(serializedResponse);
}
function browserSessionNextActions(sessionId: string) {
@@ -420,7 +431,7 @@ export function registerBrowserCapabilities(
// 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. get_telemetry compacts events by default; set compact=false on a narrowly filtered page when raw headers, request data, response bodies, or other omitted fields are needed.',
+ '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. get_telemetry compacts events by default; set compact=false with explicit categories and a limit of at most 5 when raw headers, request data, response bodies, or other omitted fields are needed.',
{
...projectSelectionInputSchema(),
action: z
@@ -590,7 +601,7 @@ export function registerBrowserCapabilities(
compact: z
.boolean()
.describe(
- "(get_telemetry) Defaults to true. Compact items flatten the event envelope, add an ISO time, and omit data fields named body, headers, post_data, or png plus any data field over 8 KiB; omitted_fields lists removals. To recover them from the exact page, call manage_browsers with the returned raw_replay arguments. Raw events may be large, so narrow the categories, time window, and limit first.",
+ "(get_telemetry) Defaults to true. Compact items flatten the event envelope, add an ISO time, and omit data fields named body, headers, post_data, or png plus any data field over 8 KiB; omitted_fields lists removals. An eligible category-filtered compact response includes raw_replay_best_effort arguments targeting the same page start, but late events or retention may change results between calls. Raw mode requires explicit categories and limit<=5, rejects screenshot PNGs, and caps the serialized response at 1 MiB.",
)
.optional(),
telemetry_enabled: z