Skip to content
Merged
2 changes: 1 addition & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ MINTLIFY_DOMAIN=<x>

# 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
Expand Down
12 changes: 7 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down Expand Up @@ -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.
Expand All @@ -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.

Expand Down Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions src/lib/mcp/register.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -33,6 +34,7 @@ const mcpToolRegistrations = [
["computer", registerComputerActionTool],
["shell", registerShellTool],
["playwright", registerPlaywrightTool],
["replays", registerReplayTools],
["auth_connections", registerAuthConnectionTools],
["credentials", registerCredentialTools],
["credential_providers", registerCredentialProviderTools],
Expand Down
66 changes: 6 additions & 60 deletions src/lib/mcp/tools/playwright.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -15,10 +15,8 @@ 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(),
.min(1, "session_id is required")
.describe("Browser session ID to execute the code against."),
},
{
title: "Execute Playwright code",
Expand All @@ -30,50 +28,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: [
Expand All @@ -86,7 +48,6 @@ export function registerPlaywrightTool(server: McpServer) {
error: response.error,
stdout: response.stdout,
stderr: response.stderr,
replay_url: replayUrl,
},
null,
2,
Expand All @@ -95,20 +56,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: [
{
Expand All @@ -117,7 +64,6 @@ export function registerPlaywrightTool(server: McpServer) {
{
success: false,
error: error instanceof Error ? error.message : String(error),
replay_url: replayUrl,
},
null,
2,
Expand Down
97 changes: 97 additions & 0 deletions src/lib/mcp/tools/replays.ts
Original file line number Diff line number Diff line change
@@ -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",
});
Comment thread
dprevoznik marked this conversation as resolved.
}
}
} catch (error) {
return toolErrorResponse("manage_replays", params.action, error);
}
},
);
}
Loading