From ef43753ebf4f8ac378ac7c4abee344be8a7b7d7a Mon Sep 17 00:00:00 2001 From: masnwilliams <43387599+masnwilliams@users.noreply.github.com> Date: Thu, 12 Nov 2026 19:04:15 +0000 Subject: [PATCH 1/3] Add create_live_view action to manage_browsers --- src/lib/mcp/tools/browsers.test.ts | 119 +++++++++++++++++++++++++++++ src/lib/mcp/tools/browsers.ts | 50 +++++++++++- 2 files changed, 167 insertions(+), 2 deletions(-) diff --git a/src/lib/mcp/tools/browsers.test.ts b/src/lib/mcp/tools/browsers.test.ts index 83662be..5e5b293 100644 --- a/src/lib/mcp/tools/browsers.test.ts +++ b/src/lib/mcp/tools/browsers.test.ts @@ -1385,6 +1385,125 @@ describe("manage_browsers next actions", () => { }); }); +describe("manage_browsers create_live_view", () => { + function liveViewClient(calls: Array<{ id: string; body: unknown }>) { + return { + browsers: { + liveView: { + create: async (id: string, body: unknown) => { + calls.push({ id, body }); + return { + url: "https://live.example/view", + scope: "control", + expires_at: "2026-01-01T00:10:00Z", + }; + }, + }, + }, + }; + } + + test("passes scope and ttl and returns the url", async () => { + const calls: Array<{ id: string; body: unknown }> = []; + const { client, close } = await connectTestMcp( + registerBrowserCapabilities, + liveViewClient(calls), + ); + + try { + const result = await client.callTool({ + name: "manage_browsers", + arguments: { + action: "create_live_view", + session_id: "my-browser", + live_view_scope: "control", + live_view_ttl_seconds: 600, + }, + }); + + expect(result.isError).toBeFalsy(); + expect(calls).toEqual([ + { id: "my-browser", body: { scope: "control", ttl_seconds: 600 } }, + ]); + expect(toolResultJSON(result)).toMatchObject({ + url: "https://live.example/view", + scope: "control", + expires_at: "2026-01-01T00:10:00Z", + }); + } finally { + await close(); + } + }); + + test("leaves scope and ttl to the api defaults when omitted", async () => { + const calls: Array<{ id: string; body: unknown }> = []; + const { client, close } = await connectTestMcp( + registerBrowserCapabilities, + liveViewClient(calls), + ); + + try { + await client.callTool({ + name: "manage_browsers", + arguments: { action: "create_live_view", session_id: "brr_123" }, + }); + + expect(calls).toEqual([{ id: "brr_123", body: {} }]); + } finally { + await close(); + } + }); + + test("requires session_id", async () => { + const calls: Array<{ id: string; body: unknown }> = []; + const { client, close } = await connectTestMcp( + registerBrowserCapabilities, + liveViewClient(calls), + ); + + try { + const result = await client.callTool({ + name: "manage_browsers", + arguments: { action: "create_live_view" }, + }); + + expect(result.isError).toBeTrue(); + expect(toolResultText(result)).toContain( + "session_id is required for create_live_view", + ); + expect(calls).toEqual([]); + } finally { + await close(); + } + }); + + test("rejects live view params on other actions", async () => { + const calls: Array<{ id: string; body: unknown }> = []; + const { client, close } = await connectTestMcp( + registerBrowserCapabilities, + liveViewClient(calls), + ); + + try { + const result = await client.callTool({ + name: "manage_browsers", + arguments: { + action: "get", + session_id: "brr_123", + live_view_scope: "read", + }, + }); + + expect(result.isError).toBeTrue(); + expect(toolResultText(result)).toContain( + "apply only to the create_live_view action", + ); + } finally { + await close(); + } + }); +}); + describe("browser resources", () => { test("uses injected dependencies", async () => { const kernelClient = { diff --git a/src/lib/mcp/tools/browsers.ts b/src/lib/mcp/tools/browsers.ts index bff61fc..bcefd8a 100644 --- a/src/lib/mcp/tools/browsers.ts +++ b/src/lib/mcp/tools/browsers.ts @@ -498,7 +498,7 @@ export function registerBrowserCapabilities( "manage_browsers", { description: - 'manage the browser sessions that every web task runs in, and their archived telemetry. once a session exists, drive it with webmcp, browser_repl, execute_playwright_code, or computer_action, preferring the first that can carry the current step. 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. live sessions can be addressed by id or by the name given at creation or set on update; deleted sessions only by id. 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.', + 'manage the browser sessions that every web task runs in, and their archived telemetry. once a session exists, drive it with webmcp, browser_repl, execute_playwright_code, or computer_action, preferring the first that can carry the current step. 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, "create_live_view" to get a shareable live view url limited to read-only or control access, and "delete" when finished. live sessions can be addressed by id or by the name given at creation or set on update; deleted sessions only by id. 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.', inputSchema: z.strictObject( { ...projectSelectionInputSchema(), @@ -509,6 +509,7 @@ export function registerBrowserCapabilities( "list", "get", "get_telemetry", + "create_live_view", "delete", ]) .describe("operation to perform."), @@ -519,7 +520,7 @@ export function registerBrowserCapabilities( session_id: z .string() .describe( - "browser session id or name. required for update, get, get_telemetry, and delete actions. a name resolves only a live session; for a deleted session (get_telemetry) pass its id.", + "browser session id or name. required for update, get, get_telemetry, create_live_view, and delete actions. a name resolves only a live session; for a deleted session (get_telemetry) pass its id.", ) .optional(), name: z @@ -844,6 +845,21 @@ export function registerBrowserCapabilities( "(create, update) enable or disable user interaction telemetry (clicks, keys, scrolls). off by default; enable for debugging.", ) .optional(), + live_view_scope: z + .enum(["read", "control"]) + .describe( + "(create_live_view) read shows the browser without accepting input; control also lets the viewer drive it. defaults to read.", + ) + .optional(), + live_view_ttl_seconds: z + .number() + .int() + .min(1) + .max(31_536_000) + .describe( + "(create_live_view) seconds until the url expires. omit to keep it valid until the browser is deleted.", + ) + .optional(), }, { error: unknownBrowserKeyMessage }, ), @@ -887,6 +903,15 @@ export function registerBrowserCapabilities( "proxy routes and private hosts are creation-only; only network.allowed_hosts can change on an existing browser.", ); } + if ( + params.action !== "create_live_view" && + (params.live_view_scope !== undefined || + params.live_view_ttl_seconds !== undefined) + ) { + return errorResponse( + "error: live_view_scope and live_view_ttl_seconds apply only to the create_live_view action.", + ); + } if (params.proxy !== undefined) { const error = proxyConfigError("proxy", params.proxy) ?? @@ -1136,6 +1161,27 @@ export function registerBrowserCapabilities( compact: params.compact, }); } + case "create_live_view": { + if (!params.session_id) + return errorResponse( + "error: session_id is required for create_live_view action.", + ); + const liveView = await client.browsers.liveView.create( + params.session_id, + { + ...(params.live_view_scope !== undefined && { + scope: params.live_view_scope, + }), + ...(params.live_view_ttl_seconds !== undefined && { + ttl_seconds: params.live_view_ttl_seconds, + }), + }, + ); + return jsonResponse({ + ...liveView, + note: "the url opens only the live view (not cdp or other browser endpoints) and cannot be revoked before it expires. share it only with the intended viewer.", + }); + } case "delete": { if (!params.session_id) return errorResponse( From e7a9838369124b5afddcfbdb70f180bdb8cda1b1 Mon Sep 17 00:00:00 2001 From: masnwilliams <43387599+masnwilliams@users.noreply.github.com> Date: Thu, 12 Nov 2026 20:05:28 +0000 Subject: [PATCH 2/3] Bump @onkernel/sdk to 0.148.0 --- bun.lock | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/bun.lock b/bun.lock index 22f0584..d6fcb57 100644 --- a/bun.lock +++ b/bun.lock @@ -11,7 +11,7 @@ "@modelcontextprotocol/core": "2.0.0", "@modelcontextprotocol/server": "2.0.0", "@onkernel/managed-auth-react": "0.5.5", - "@onkernel/sdk": "0.141.0", + "@onkernel/sdk": "0.148.0", "@posthog/mcp": "0.17.0", "@types/jsonwebtoken": "^9.0.10", "@types/redis": "^4.0.11", @@ -160,7 +160,7 @@ "@onkernel/managed-auth-react": ["@onkernel/managed-auth-react@0.5.5", "", { "dependencies": { "clsx": "^2.1.1", "tldts": "7.4.15" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" } }, "sha512-uoeHAJ0RnLoEJe7TdpT0pA3RgPWEpJLf9F532aLEcahFNrMIqyAdMp94LNIwVuMlxkkfx4+3S/fqzHubbnzjJg=="], - "@onkernel/sdk": ["@onkernel/sdk@0.141.0", "", {}, "sha512-NAsynTkRK3N7udBgitPBd5OBbo5j+NzAAsX8eIbHWR4Ly9CNAVuhJQ2uljnuZaXrCQwZhQ/29L6f2X2Vd4gf1w=="], + "@onkernel/sdk": ["@onkernel/sdk@0.148.0", "", {}, "sha512-e58SYGkmQoKMTKYv8SNpzbO4uE9Bhz/7dv6iyW8V1dq/m9V7CHNa8L3o9peP2arbaQdyqWCt5kljxKrK5XnlOw=="], "@oven/bun-darwin-aarch64": ["@oven/bun-darwin-aarch64@1.3.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-eJopQrUk0WR7jViYDC29+Rp50xGvs4GtWOXBeqCoFMzutkkO3CZvHehA4JqnjfWMTSS8toqvRhCSOpOz62Wf9w=="], diff --git a/package.json b/package.json index 08df7ad..5bdb220 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,7 @@ "@modelcontextprotocol/core": "2.0.0", "@modelcontextprotocol/server": "2.0.0", "@onkernel/managed-auth-react": "0.5.5", - "@onkernel/sdk": "0.141.0", + "@onkernel/sdk": "0.148.0", "@posthog/mcp": "0.17.0", "@types/jsonwebtoken": "^9.0.10", "@types/redis": "^4.0.11", From d897db3d8d37b9a2df1235745782c019b38df559 Mon Sep 17 00:00:00 2001 From: masnwilliams <43387599+masnwilliams@users.noreply.github.com> Date: Thu, 12 Nov 2026 20:13:28 +0000 Subject: [PATCH 3/3] Don't retry live view URL creation --- src/lib/mcp/tools/browsers.test.ts | 26 +++++++++++++++++--------- src/lib/mcp/tools/browsers.ts | 3 +++ 2 files changed, 20 insertions(+), 9 deletions(-) diff --git a/src/lib/mcp/tools/browsers.test.ts b/src/lib/mcp/tools/browsers.test.ts index 5e5b293..0fa178b 100644 --- a/src/lib/mcp/tools/browsers.test.ts +++ b/src/lib/mcp/tools/browsers.test.ts @@ -1386,12 +1386,14 @@ describe("manage_browsers next actions", () => { }); describe("manage_browsers create_live_view", () => { - function liveViewClient(calls: Array<{ id: string; body: unknown }>) { + type LiveViewCall = { id: string; body: unknown; options: unknown }; + + function liveViewClient(calls: LiveViewCall[]) { return { browsers: { liveView: { - create: async (id: string, body: unknown) => { - calls.push({ id, body }); + create: async (id: string, body: unknown, options: unknown) => { + calls.push({ id, body, options }); return { url: "https://live.example/view", scope: "control", @@ -1404,7 +1406,7 @@ describe("manage_browsers create_live_view", () => { } test("passes scope and ttl and returns the url", async () => { - const calls: Array<{ id: string; body: unknown }> = []; + const calls: LiveViewCall[] = []; const { client, close } = await connectTestMcp( registerBrowserCapabilities, liveViewClient(calls), @@ -1423,7 +1425,11 @@ describe("manage_browsers create_live_view", () => { expect(result.isError).toBeFalsy(); expect(calls).toEqual([ - { id: "my-browser", body: { scope: "control", ttl_seconds: 600 } }, + { + id: "my-browser", + body: { scope: "control", ttl_seconds: 600 }, + options: { maxRetries: 0 }, + }, ]); expect(toolResultJSON(result)).toMatchObject({ url: "https://live.example/view", @@ -1436,7 +1442,7 @@ describe("manage_browsers create_live_view", () => { }); test("leaves scope and ttl to the api defaults when omitted", async () => { - const calls: Array<{ id: string; body: unknown }> = []; + const calls: LiveViewCall[] = []; const { client, close } = await connectTestMcp( registerBrowserCapabilities, liveViewClient(calls), @@ -1448,14 +1454,16 @@ describe("manage_browsers create_live_view", () => { arguments: { action: "create_live_view", session_id: "brr_123" }, }); - expect(calls).toEqual([{ id: "brr_123", body: {} }]); + expect(calls).toEqual([ + { id: "brr_123", body: {}, options: { maxRetries: 0 } }, + ]); } finally { await close(); } }); test("requires session_id", async () => { - const calls: Array<{ id: string; body: unknown }> = []; + const calls: LiveViewCall[] = []; const { client, close } = await connectTestMcp( registerBrowserCapabilities, liveViewClient(calls), @@ -1478,7 +1486,7 @@ describe("manage_browsers create_live_view", () => { }); test("rejects live view params on other actions", async () => { - const calls: Array<{ id: string; body: unknown }> = []; + const calls: LiveViewCall[] = []; const { client, close } = await connectTestMcp( registerBrowserCapabilities, liveViewClient(calls), diff --git a/src/lib/mcp/tools/browsers.ts b/src/lib/mcp/tools/browsers.ts index bcefd8a..42f1d8a 100644 --- a/src/lib/mcp/tools/browsers.ts +++ b/src/lib/mcp/tools/browsers.ts @@ -1176,6 +1176,9 @@ export function registerBrowserCapabilities( ttl_seconds: params.live_view_ttl_seconds, }), }, + // each call mints a url that cannot be revoked, so a retry after a + // lost response would leave an extra live url behind. + { maxRetries: 0 }, ); return jsonResponse({ ...liveView,