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 3102bff..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 -- 🎥 Automatically 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,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 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. @@ -279,7 +280,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 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,9 +321,10 @@ 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://..." } +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" } ``` ### Set up browser profiles for authentication 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/playwright.ts b/src/lib/mcp/tools/playwright.ts index 2778765..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 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 an existing Kernel browser session. Does not create or delete browsers -- use manage_browsers to manage session lifecycle.", { 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,50 +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); - let kernelBrowser; - let replay; - const shouldCleanup = !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) - throw new Error("Failed to create browser session"); - } - - try { - replay = await client.browsers.replays.start( - kernelBrowser.session_id, - ); - } catch { - replay = null; - } - - const response = await client.browsers.playwright.execute( - kernelBrowser.session_id, - { 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); - } + const response = await client.browsers.playwright.execute(session_id, { + code, + }); return { content: [ @@ -86,7 +47,6 @@ export function registerPlaywrightTool(server: McpServer) { error: response.error, stdout: response.stdout, stderr: response.stderr, - replay_url: replayUrl, }, null, 2, @@ -95,20 +55,6 @@ 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); - } catch {} - return { content: [ { @@ -117,7 +63,6 @@ export function registerPlaywrightTool(server: McpServer) { { success: false, error: error instanceof Error ? error.message : String(error), - replay_url: replayUrl, }, null, 2, diff --git a/src/lib/mcp/tools/replays.ts b/src/lib/mcp/tools/replays.ts new file mode 100644 index 0000000..d754a16 --- /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. Requires a paid Kernel plan; not available on the free tier.', + { + 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); + } + }, + ); +}