From dd9971e8df023ee2776563efb0c764db60ff116b Mon Sep 17 00:00:00 2001 From: dprevoznik <58714078+dprevoznik@users.noreply.github.com> Date: Thu, 16 Jul 2026 20:26:41 +0000 Subject: [PATCH 1/7] Remove per-call replay recording from execute_playwright_code Each execute_playwright_code call wrapped the run in replays.start / replays.stop and validated existing sessions with a browsers.retrieve, adding three round trips around the one useful call. In a tight agent loop that latency lands on the critical path every turn. Drop the replay start/stop and the pre-execute retrieve; execute surfaces a not-found error on its own. Existing-session calls now make a single round trip. Video replay is handled separately via managed replay controls. Co-Authored-By: Claude Opus 4.8 --- README.md | 6 ++-- src/lib/mcp/tools/playwright.ts | 55 +++++++-------------------------- 2 files changed, 14 insertions(+), 47 deletions(-) diff --git a/README.md b/README.md index 3102bff..03ca894 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ The Kernel MCP Server bridges AI assistants (like Claude, Cursor, or other MCP-c - 📊 Monitor deployments and track invocations - 🔍 Search Kernel documentation and inject context - 💻 Execute arbitrary Playwright code against live browsers -- 🎥 Automatically record video replays of browser automation +- 🎥 Record video replays of browser automation **Open-source & fully-managed** — the complete codebase is available here, and we run the production instance so you don't need to deploy anything. @@ -279,7 +279,7 @@ Self-hosted deployments can hide sensitive tool families by setting `KERNEL_MCP_ - `computer_action` - Mouse, keyboard, clipboard, and screenshot controls for browser sessions (click, type, press_key, scroll, move, get_position, read_clipboard, write_clipboard, screenshot). - `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. +- `execute_playwright_code` - Execute Playwright/TypeScript code against a browser, with automatic cleanup of browsers it creates. - `exec_command` - Run shell commands inside a browser VM. Returns decoded stdout/stderr. - `search_docs` - Search Kernel platform documentation and guides. @@ -322,7 +322,7 @@ Assistant: I'll execute your web-scraper action with reddit.com as the target. Human: Go to example.com and get me the page title Assistant: I'll execute Playwright code to navigate to the site and retrieve the title. [Uses execute_playwright_code tool with code: "await page.goto('https://example.com'); return await page.title();"] -Returns: { success: true, result: "Example Domain", replay_url: "https://..." } +Returns: { success: true, result: "Example Domain" } ``` ### Set up browser profiles for authentication diff --git a/src/lib/mcp/tools/playwright.ts b/src/lib/mcp/tools/playwright.ts index 2778765..06719b1 100644 --- a/src/lib/mcp/tools/playwright.ts +++ b/src/lib/mcp/tools/playwright.ts @@ -6,7 +6,7 @@ export function registerPlaywrightTool(server: McpServer) { // execute_playwright_code -- Run Playwright/TypeScript code against a browser server.tool( "execute_playwright_code", - 'Execute Playwright/TypeScript automation code against a Kernel browser session. If session_id is provided, uses that existing browser; otherwise creates a new one. Returns the result with a video replay URL. Auto-cleans up browsers it creates. Use computer_action with action "screenshot" instead of page.screenshot() in code.', + 'Execute Playwright/TypeScript automation code against a Kernel browser session. If session_id is provided, uses that existing browser; otherwise creates a new one. Auto-cleans up browsers it creates. Use computer_action with action "screenshot" instead of page.screenshot() in code.', { code: z .string() @@ -30,49 +30,27 @@ export function registerPlaywrightTool(server: McpServer) { async ({ code, session_id }, extra) => { if (!extra.authInfo) throw new Error("Authentication required"); const client = createKernelClient(extra.authInfo.token); - let kernelBrowser; - let replay; const shouldCleanup = !session_id; + let activeSessionId = session_id; try { if (!code || typeof code !== "string") throw new Error("code is required and must be a string"); - if (session_id) { - kernelBrowser = await client.browsers.retrieve(session_id); - if (!kernelBrowser) - throw new Error(`Browser session "${session_id}" not found`); - } else { - kernelBrowser = await client.browsers.create({ stealth: true }); - if (!kernelBrowser?.session_id) + if (!activeSessionId) { + const created = await client.browsers.create({ stealth: true }); + if (!created?.session_id) throw new Error("Failed to create browser session"); - } - - try { - replay = await client.browsers.replays.start( - kernelBrowser.session_id, - ); - } catch { - replay = null; + activeSessionId = created.session_id; } const response = await client.browsers.playwright.execute( - kernelBrowser.session_id, + activeSessionId, { code }, ); - let replayUrl = null; - if (replay && kernelBrowser?.session_id) { - try { - await client.browsers.replays.stop(replay.replay_id, { - id: kernelBrowser.session_id, - }); - replayUrl = replay.replay_view_url; - } catch {} - } - - if (shouldCleanup && kernelBrowser?.session_id) { - await client.browsers.deleteByID(kernelBrowser.session_id); + if (shouldCleanup) { + await client.browsers.deleteByID(activeSessionId); } return { @@ -86,7 +64,6 @@ export function registerPlaywrightTool(server: McpServer) { error: response.error, stdout: response.stdout, stderr: response.stderr, - replay_url: replayUrl, }, null, 2, @@ -95,18 +72,9 @@ export function registerPlaywrightTool(server: McpServer) { ], }; } catch (error) { - let replayUrl = null; - if (replay && kernelBrowser?.session_id) { - try { - await client.browsers.replays.stop(replay.replay_id, { - id: kernelBrowser.session_id, - }); - replayUrl = replay.replay_view_url; - } catch {} - } try { - if (shouldCleanup && kernelBrowser?.session_id) - await client.browsers.deleteByID(kernelBrowser.session_id); + if (shouldCleanup && activeSessionId) + await client.browsers.deleteByID(activeSessionId); } catch {} return { @@ -117,7 +85,6 @@ export function registerPlaywrightTool(server: McpServer) { { success: false, error: error instanceof Error ? error.message : String(error), - replay_url: replayUrl, }, null, 2, From e1f8ca3dca36646e34a93c95e5ea48ab275edb54 Mon Sep 17 00:00:00 2001 From: dprevoznik <58714078+dprevoznik@users.noreply.github.com> Date: Thu, 16 Jul 2026 20:49:05 +0000 Subject: [PATCH 2/7] Make execute_playwright_code a passthrough (require session_id) Fold browser lifecycle out of the execute tool: session_id is now required, and the tool no longer creates a stealth browser when it is omitted or deletes the browser afterward. It is now a thin passthrough to browsers.playwright.execute -- inputs in, outputs back. Creating and deleting a browser inside an "execute" call hid a lifecycle (and a baked-in stealth default) the caller never chose, and put create/ delete round trips on the path. Session lifecycle belongs to manage_browsers; recording belongs to managed replays. BREAKING CHANGE: callers that relied on omitting session_id to get a one-shot auto-created browser must now create a session with manage_browsers first and pass its session_id. Co-Authored-By: Claude Opus 4.8 --- README.md | 7 ++++--- src/lib/mcp/tools/playwright.ts | 32 +++++--------------------------- 2 files changed, 9 insertions(+), 30 deletions(-) diff --git a/README.md b/README.md index 03ca894..313b165 100644 --- a/README.md +++ b/README.md @@ -279,7 +279,7 @@ Self-hosted deployments can hide sensitive tool families by setting `KERNEL_MCP_ - `computer_action` - Mouse, keyboard, clipboard, and screenshot controls for browser sessions (click, type, press_key, scroll, move, get_position, read_clipboard, write_clipboard, screenshot). - `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 cleanup of browsers it creates. +- `execute_playwright_code` - Execute Playwright/TypeScript code against an existing browser session. Does not create or delete browsers - use `manage_browsers` for session lifecycle. - `exec_command` - Run shell commands inside a browser VM. Returns decoded stdout/stderr. - `search_docs` - Search Kernel platform documentation and guides. @@ -320,8 +320,9 @@ Assistant: I'll execute your web-scraper action with reddit.com as the target. ``` Human: Go to example.com and get me the page title -Assistant: I'll execute Playwright code to navigate to the site and retrieve the title. -[Uses execute_playwright_code tool with code: "await page.goto('https://example.com'); return await page.title();"] +Assistant: I'll create a browser session, then execute Playwright code against it to navigate to the site and retrieve the title. +[Uses manage_browsers tool with action: "create" to get a session_id] +[Uses execute_playwright_code tool with session_id and code: "await page.goto('https://example.com'); return await page.title();"] Returns: { success: true, result: "Example Domain" } ``` diff --git a/src/lib/mcp/tools/playwright.ts b/src/lib/mcp/tools/playwright.ts index 06719b1..e398a31 100644 --- a/src/lib/mcp/tools/playwright.ts +++ b/src/lib/mcp/tools/playwright.ts @@ -6,7 +6,7 @@ export function registerPlaywrightTool(server: McpServer) { // execute_playwright_code -- Run Playwright/TypeScript code against a browser server.tool( "execute_playwright_code", - 'Execute Playwright/TypeScript automation code against a Kernel browser session. If session_id is provided, uses that existing browser; otherwise creates a new one. Auto-cleans up browsers it creates. Use computer_action with action "screenshot" instead of page.screenshot() in code.', + 'Execute Playwright/TypeScript automation code against an existing Kernel browser session. Does not create or delete browsers -- use manage_browsers to manage session lifecycle. Use computer_action with action "screenshot" instead of page.screenshot() in code.', { code: z .string() @@ -15,10 +15,7 @@ export function registerPlaywrightTool(server: McpServer) { ), session_id: z .string() - .describe( - "Existing browser session ID. If omitted, a new browser is created and cleaned up after execution.", - ) - .optional(), + .describe("Browser session ID to execute the code against."), }, { title: "Execute Playwright code", @@ -30,28 +27,14 @@ export function registerPlaywrightTool(server: McpServer) { async ({ code, session_id }, extra) => { if (!extra.authInfo) throw new Error("Authentication required"); const client = createKernelClient(extra.authInfo.token); - const shouldCleanup = !session_id; - let activeSessionId = session_id; try { if (!code || typeof code !== "string") throw new Error("code is required and must be a string"); - if (!activeSessionId) { - const created = await client.browsers.create({ stealth: true }); - if (!created?.session_id) - throw new Error("Failed to create browser session"); - activeSessionId = created.session_id; - } - - const response = await client.browsers.playwright.execute( - activeSessionId, - { code }, - ); - - if (shouldCleanup) { - await client.browsers.deleteByID(activeSessionId); - } + const response = await client.browsers.playwright.execute(session_id, { + code, + }); return { content: [ @@ -72,11 +55,6 @@ export function registerPlaywrightTool(server: McpServer) { ], }; } catch (error) { - try { - if (shouldCleanup && activeSessionId) - await client.browsers.deleteByID(activeSessionId); - } catch {} - return { content: [ { From e7040416fa8e684d4d4fba5ac9c7b82eab305f11 Mon Sep 17 00:00:00 2001 From: dprevoznik <58714078+dprevoznik@users.noreply.github.com> Date: Thu, 16 Jul 2026 20:53:24 +0000 Subject: [PATCH 3/7] Add manage_replays tool for session-scoped video replays Introduce a dedicated replays toolset backing browsers.replays with start/stop/list actions, registered as the "replays" toolset (gated by KERNEL_MCP_DISABLED_TOOLSETS like the others). Recording is now session-scoped and opt-in: start once, run the automation, then stop -- replacing the per-call recording that used to be wrapped around every execute_playwright_code call. Co-Authored-By: Claude Opus 4.8 --- src/lib/mcp/register.ts | 2 + src/lib/mcp/tools/replays.ts | 97 ++++++++++++++++++++++++++++++++++++ 2 files changed, 99 insertions(+) create mode 100644 src/lib/mcp/tools/replays.ts diff --git a/src/lib/mcp/register.ts b/src/lib/mcp/register.ts index 555dd29..0b810a1 100644 --- a/src/lib/mcp/register.ts +++ b/src/lib/mcp/register.ts @@ -15,6 +15,7 @@ import { registerPlaywrightTool } from "@/lib/mcp/tools/playwright"; import { registerProfileCapabilities } from "@/lib/mcp/tools/profiles"; import { registerProjectCapabilities } from "@/lib/mcp/tools/projects"; import { registerProxyTools } from "@/lib/mcp/tools/proxies"; +import { registerReplayTools } from "@/lib/mcp/tools/replays"; import { registerShellTool } from "@/lib/mcp/tools/shell"; type RegisterMcpToolset = (server: McpServer) => void; @@ -33,6 +34,7 @@ const mcpToolRegistrations = [ ["computer", registerComputerActionTool], ["shell", registerShellTool], ["playwright", registerPlaywrightTool], + ["replays", registerReplayTools], ["auth_connections", registerAuthConnectionTools], ["credentials", registerCredentialTools], ["credential_providers", registerCredentialProviderTools], diff --git a/src/lib/mcp/tools/replays.ts b/src/lib/mcp/tools/replays.ts new file mode 100644 index 0000000..c27013f --- /dev/null +++ b/src/lib/mcp/tools/replays.ts @@ -0,0 +1,97 @@ +import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { z } from "zod"; +import { createKernelClient } from "@/lib/mcp/kernel-client"; +import { + errorResponse, + itemsJsonResponse, + jsonResponse, + textResponse, + toolErrorResponse, +} from "@/lib/mcp/responses"; + +export function registerReplayTools(server: McpServer) { + // manage_replays -- Start, stop, and list video replay recordings for a session + server.tool( + "manage_replays", + 'Manage video replay recordings for a browser session. Use "start" to begin recording a session (returns a replay_id and a viewable URL), "stop" to end a recording and persist the video, or "list" to see all replays for a session with their view URLs. Recording is session-scoped: start once, run your automation, then stop -- rather than recording each action separately.', + { + action: z + .enum(["start", "stop", "list"]) + .describe("Operation to perform."), + session_id: z.string().describe("Browser session ID."), + replay_id: z.string().describe("(stop) Replay ID to stop.").optional(), + framerate: z + .number() + .int() + .min(1) + .describe( + "(start) Recording framerate in fps. Values above 20 require GPU to be enabled on the session.", + ) + .optional(), + max_duration_in_seconds: z + .number() + .int() + .min(1) + .describe("(start) Maximum recording duration in seconds.") + .optional(), + record_audio: z + .boolean() + .describe( + "(start) Record audio in addition to video. Defaults to video-only.", + ) + .optional(), + }, + { + title: "Manage browser session replays", + readOnlyHint: false, + destructiveHint: false, + idempotentHint: false, + openWorldHint: false, + }, + async (params, extra) => { + if (!extra.authInfo) throw new Error("Authentication required"); + const client = createKernelClient(extra.authInfo.token); + + try { + switch (params.action) { + case "start": { + const body = { + ...(params.framerate !== undefined && { + framerate: params.framerate, + }), + ...(params.max_duration_in_seconds !== undefined && { + max_duration_in_seconds: params.max_duration_in_seconds, + }), + ...(params.record_audio !== undefined && { + record_audio: params.record_audio, + }), + }; + const replay = await client.browsers.replays.start( + params.session_id, + Object.keys(body).length > 0 ? body : undefined, + ); + return jsonResponse(replay); + } + case "stop": { + if (!params.replay_id) + return errorResponse("Error: replay_id is required for stop."); + await client.browsers.replays.stop(params.replay_id, { + id: params.session_id, + }); + return textResponse("Replay stopped successfully"); + } + case "list": { + const replays = await client.browsers.replays.list( + params.session_id, + ); + return itemsJsonResponse(replays, { + emptyText: "No replays found for this session", + }); + } + } + } catch (error) { + return toolErrorResponse("manage_replays", params.action, error); + } + }, + ); +} From 43a58613684b3d995000fb03a2cc033b8ad5e02a Mon Sep 17 00:00:00 2001 From: dprevoznik <58714078+dprevoznik@users.noreply.github.com> Date: Thu, 16 Jul 2026 20:53:24 +0000 Subject: [PATCH 4/7] Document manage_replays tool and replays toolset Co-Authored-By: Claude Opus 4.8 --- .env.example | 2 +- README.md | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.env.example b/.env.example index 0e4f8f3..acf98af 100644 --- a/.env.example +++ b/.env.example @@ -19,7 +19,7 @@ MINTLIFY_DOMAIN= # Optional MCP toolset gating. Comma-separated values. # Example: api_keys hides manage_api_keys so deployments can opt out of key management. -# Supported: apps, api_keys, browser_pools, browsers, computer, docs, extensions, playwright, profiles, projects, proxies, shell +# Supported: apps, api_keys, browser_pools, browsers, computer, docs, extensions, playwright, profiles, projects, proxies, replays, shell # KERNEL_MCP_DISABLED_TOOLSETS=api_keys # Redis Configuration diff --git a/README.md b/README.md index 313b165..de89348 100644 --- a/README.md +++ b/README.md @@ -269,6 +269,7 @@ Self-hosted deployments can hide sensitive tool families by setting `KERNEL_MCP_ - `manage_api_keys` - Create, list, get, update, and delete org-wide or project-scoped API keys. Create returns the plaintext key once. - `manage_browser_pools` - Create, list, get, delete, and flush pools of pre-warmed browsers. Acquire and release browsers from pools. - `manage_proxies` - Create, list, get, check, and delete proxy configurations (datacenter, ISP, residential, mobile, custom). +- `manage_replays` - Start, stop, and list video replay recordings for a browser session. Session-scoped: start once, run your automation, then stop. - `manage_extensions` - List and delete uploaded browser extensions. - `manage_apps` - List/search apps, invoke actions, get/list/delete deployments, and get invocation results. - `manage_auth_connections` - Create, list, get, delete managed auth connections; start login flows (returns a hosted URL and live view); submit MFA codes or SSO selections. From 455678344981a6992fb333d2086e8c29c1caba47 Mon Sep 17 00:00:00 2001 From: dprevoznik <58714078+dprevoznik@users.noreply.github.com> Date: Fri, 17 Jul 2026 02:44:22 +0000 Subject: [PATCH 5/7] Note paid-tier requirement in manage_replays description Co-Authored-By: Claude Opus 4.8 --- README.md | 2 +- src/lib/mcp/tools/replays.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index de89348..daecf81 100644 --- a/README.md +++ b/README.md @@ -269,7 +269,7 @@ Self-hosted deployments can hide sensitive tool families by setting `KERNEL_MCP_ - `manage_api_keys` - Create, list, get, update, and delete org-wide or project-scoped API keys. Create returns the plaintext key once. - `manage_browser_pools` - Create, list, get, delete, and flush pools of pre-warmed browsers. Acquire and release browsers from pools. - `manage_proxies` - Create, list, get, check, and delete proxy configurations (datacenter, ISP, residential, mobile, custom). -- `manage_replays` - Start, stop, and list video replay recordings for a browser session. Session-scoped: start once, run your automation, then stop. +- `manage_replays` - Start, stop, and list video replay recordings for a browser session. Session-scoped: start once, run your automation, then stop. Requires a paid Kernel plan. - `manage_extensions` - List and delete uploaded browser extensions. - `manage_apps` - List/search apps, invoke actions, get/list/delete deployments, and get invocation results. - `manage_auth_connections` - Create, list, get, delete managed auth connections; start login flows (returns a hosted URL and live view); submit MFA codes or SSO selections. diff --git a/src/lib/mcp/tools/replays.ts b/src/lib/mcp/tools/replays.ts index c27013f..d754a16 100644 --- a/src/lib/mcp/tools/replays.ts +++ b/src/lib/mcp/tools/replays.ts @@ -13,7 +13,7 @@ export function registerReplayTools(server: McpServer) { // manage_replays -- Start, stop, and list video replay recordings for a session server.tool( "manage_replays", - 'Manage video replay recordings for a browser session. Use "start" to begin recording a session (returns a replay_id and a viewable URL), "stop" to end a recording and persist the video, or "list" to see all replays for a session with their view URLs. Recording is session-scoped: start once, run your automation, then stop -- rather than recording each action separately.', + 'Manage video replay recordings for a browser session. Use "start" to begin recording a session (returns a replay_id and a viewable URL), "stop" to end a recording and persist the video, or "list" to see all replays for a session with their view URLs. Recording is session-scoped: start once, run your automation, then stop -- rather than recording each action separately. Requires a paid Kernel plan; not available on the free tier.', { action: z .enum(["start", "stop", "list"]) From f6879f59596c606c653b96b78caccc81df4aeb91 Mon Sep 17 00:00:00 2001 From: dprevoznik <58714078+dprevoznik@users.noreply.github.com> Date: Fri, 17 Jul 2026 02:56:50 +0000 Subject: [PATCH 6/7] Note replays are MP4 videos in README Co-Authored-By: Claude Opus 4.8 --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index daecf81..923ec9c 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ The Kernel MCP Server bridges AI assistants (like Claude, Cursor, or other MCP-c - 📊 Monitor deployments and track invocations - 🔍 Search Kernel documentation and inject context - 💻 Execute arbitrary Playwright code against live browsers -- 🎥 Record video replays of browser automation +- 🎥 Record MP4 video replays of browser automation **Open-source & fully-managed** — the complete codebase is available here, and we run the production instance so you don't need to deploy anything. @@ -269,7 +269,7 @@ Self-hosted deployments can hide sensitive tool families by setting `KERNEL_MCP_ - `manage_api_keys` - Create, list, get, update, and delete org-wide or project-scoped API keys. Create returns the plaintext key once. - `manage_browser_pools` - Create, list, get, delete, and flush pools of pre-warmed browsers. Acquire and release browsers from pools. - `manage_proxies` - Create, list, get, check, and delete proxy configurations (datacenter, ISP, residential, mobile, custom). -- `manage_replays` - Start, stop, and list video replay recordings for a browser session. Session-scoped: start once, run your automation, then stop. Requires a paid Kernel plan. +- `manage_replays` - Start, stop, and list MP4 video replay recordings for a browser session. Session-scoped: start once, run your automation, then stop. Requires a paid Kernel plan. - `manage_extensions` - List and delete uploaded browser extensions. - `manage_apps` - List/search apps, invoke actions, get/list/delete deployments, and get invocation results. - `manage_auth_connections` - Create, list, get, delete managed auth connections; start login flows (returns a hosted URL and live view); submit MFA codes or SSO selections. From 1265a2d8b31e5cf7f98de9a509f2075005c169e6 Mon Sep 17 00:00:00 2001 From: dprevoznik <58714078+dprevoznik@users.noreply.github.com> Date: Fri, 17 Jul 2026 02:59:07 +0000 Subject: [PATCH 7/7] Drop screenshot guidance from execute_playwright_code description Let the agent choose between page.screenshot() and computer_action rather than steering it toward one. Co-Authored-By: Claude Opus 4.8 --- src/lib/mcp/tools/playwright.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/mcp/tools/playwright.ts b/src/lib/mcp/tools/playwright.ts index e398a31..4f2281a 100644 --- a/src/lib/mcp/tools/playwright.ts +++ b/src/lib/mcp/tools/playwright.ts @@ -6,7 +6,7 @@ export function registerPlaywrightTool(server: McpServer) { // execute_playwright_code -- Run Playwright/TypeScript code against a browser server.tool( "execute_playwright_code", - 'Execute Playwright/TypeScript automation code against an existing Kernel browser session. Does not create or delete browsers -- use manage_browsers to manage session lifecycle. Use computer_action with action "screenshot" instead of page.screenshot() in code.', + "Execute Playwright/TypeScript automation code against an existing Kernel browser session. Does not create or delete browsers -- use manage_browsers to manage session lifecycle.", { code: z .string()