diff --git a/README.md b/README.md
index 1081363..700696b 100644
--- a/README.md
+++ b/README.md
@@ -332,6 +332,7 @@ Inputs that mirror the API's nested `proxy`, `network`, `browser`, and proxy `co
- `manage_config_registry` - Look up current browser and proxy recommendations, start and inspect analyses, request cancellation, and list project configurations or analysis history.
- `manage_proxies` - Create, list, get, check, and delete proxy configurations (datacenter, ISP, residential, mobile, custom). List filters by exact `name` or a `query` substring.
- `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_playwright_executors` - List and delete the Playwright executors of a browser session. `list` returns every executor (`default` first) with `busy`, `created_at`, `last_used_at`, and for named executors the `target_id` and `url` of the tab it owns. `delete` takes `name` and an optional `close_tab` (default true); deleting `default` restarts it instead of removing it. A browser can have at most 8 named executors.
- `web_search` - Search the web, retrieve retained results, and inspect provider capabilities. Tool visibility uses a per-credential, per-connection Search entitlement snapshot cached for up to 30 minutes; the Search API remains authoritative for execution access. Search creation is billable and is not automatically retried.
- `manage_extensions` - List and delete uploaded browser extensions.
- `manage_apps` - List/search apps, invoke actions, get/list/delete deployments, and get invocation results.
@@ -352,7 +353,7 @@ See [Vault payments](docs/vault-payments.md) for both provider flows, safety rul
- `get_connection_context` - Inspect the authenticated principal, organization, credential scope, and effective project scope.
- `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 isolated Playwright/TypeScript code and browser-wide WebMCP helpers against an existing browser session. Does not create or delete browsers - use `manage_browsers` for session lifecycle.
+- `execute_playwright_code` - Execute isolated Playwright/TypeScript code and browser-wide WebMCP helpers against an existing browser session. Does not create or delete browsers - use `manage_browsers` for session lifecycle. Every call runs in an executor: omit `executor` (or pass `default`) to bind `page` to the active tab, or give each independent task its own `executor` name to run them in parallel in separate background tabs of one browser. Calls on the same executor run one at a time. The result includes the `tab` (`target_id`, `created`) that `page` was bound to.
- `browser_repl` - Execute JavaScript in a persistent Node.js runtime inside an existing browser VM. Top-level bindings survive across calls and can use native browser helpers, WebMCP, Patchright, Playwright, raw CDP, Node built-ins, files, processes, and the network. The tool description includes native-helper, raw-CDP, and Patchright/Playwright examples; call `repl.help()` inside a cell for the runtime method reference. This is unrestricted VM code execution, not a sandbox. See the [complete Browser REPL reference](https://github.com/kernel/kernel-images/blob/main/server/docs/repl.md).
- `webmcp` - Discover native and custom tools across tabs and frames, invoke an exact opaque `tool_ref`, and manage custom definitions with `list_custom`, `add_custom`, and `remove_custom`. See [WebMCP actions](#webmcp-actions) below.
- `exec_command` - Run shell commands inside a browser VM. Returns decoded stdout/stderr.
diff --git a/bun.lock b/bun.lock
index 790bc0b..de1fbeb 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.3",
- "@onkernel/sdk": "0.117.0",
+ "@onkernel/sdk": "0.120.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.3", "", { "dependencies": { "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" } }, "sha512-5Ps7h7HknAxL4ZLnJCzsLhW8MLRr0SH1P8+eALBZXCmnx/K+g5ra2ar3iLoWFQ8vf/EvpuDF1LigIT6tDjRKdA=="],
- "@onkernel/sdk": ["@onkernel/sdk@0.117.0", "", {}, "sha512-bUYwF6cn965QWsqadCoBmqCGPz4DQ30kAFRLgECujKcWiHK4Yf9sXTrjb6CKQuzZFuS/0NwQ3vctmtimXZvs7A=="],
+ "@onkernel/sdk": ["@onkernel/sdk@0.120.0", "", {}, "sha512-EqDXRCehmuP8lYssYXettixDfBohJWCTOeiUYNptWky8JAtVo6u0Pklz867PO66pFJn+enZd3dxPBOL/0pW47Q=="],
"@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 2cee615..4cea025 100644
--- a/package.json
+++ b/package.json
@@ -41,7 +41,7 @@
"@modelcontextprotocol/core": "2.0.0",
"@modelcontextprotocol/server": "2.0.0",
"@onkernel/managed-auth-react": "0.5.3",
- "@onkernel/sdk": "0.117.0",
+ "@onkernel/sdk": "0.120.0",
"@posthog/mcp": "0.17.0",
"@types/jsonwebtoken": "^9.0.10",
"@types/redis": "^4.0.11",
diff --git a/src/lib/mcp/register.test.ts b/src/lib/mcp/register.test.ts
index 0dc6837..24d2b53 100644
--- a/src/lib/mcp/register.test.ts
+++ b/src/lib/mcp/register.test.ts
@@ -185,6 +185,7 @@ describe("MCP toolset allowlist", () => {
"get_connection_context",
"computer_action",
"execute_playwright_code",
+ "manage_playwright_executors",
"browser_repl",
]);
expect(registration.appTools).toEqual([]);
@@ -217,6 +218,7 @@ describe("project selection registration", () => {
"computer_action",
"exec_command",
"execute_playwright_code",
+ "manage_playwright_executors",
"browser_repl",
"manage_replays",
"manage_auth_connections",
diff --git a/src/lib/mcp/tool-names.ts b/src/lib/mcp/tool-names.ts
index e3427b7..acb3fc7 100644
--- a/src/lib/mcp/tool-names.ts
+++ b/src/lib/mcp/tool-names.ts
@@ -16,6 +16,7 @@ export const KERNEL_MCP_TOOL_NAMES = [
"manage_credential_providers",
"manage_credentials",
"manage_extensions",
+ "manage_playwright_executors",
"manage_profiles",
"manage_projects",
"manage_proxies",
diff --git a/src/lib/mcp/tools/playwright.test.ts b/src/lib/mcp/tools/playwright.test.ts
new file mode 100644
index 0000000..ab2159b
--- /dev/null
+++ b/src/lib/mcp/tools/playwright.test.ts
@@ -0,0 +1,365 @@
+///
+
+import { APIError } from "@onkernel/sdk";
+import { describe, expect, test } from "bun:test";
+
+import { connectTestMcp, toolResultJSON } from "@/lib/mcp/mcp-test-fixtures";
+import { registerPlaywrightTool } from "@/lib/mcp/tools/playwright";
+
+type ExecuteCall = { sessionId: string; body: Record };
+type DeleteCall = { name: string; params: Record };
+
+const executors = [
+ {
+ name: "default",
+ busy: false,
+ created_at: "2026-01-01T00:00:00Z",
+ last_used_at: "2026-01-01T00:01:00Z",
+ },
+ {
+ name: "checkout",
+ busy: true,
+ created_at: "2026-01-01T00:02:00Z",
+ last_used_at: "2026-01-01T00:03:00Z",
+ target_id: "A1B2",
+ url: "https://example.com/cart",
+ },
+];
+
+function playwrightClient({
+ executeCalls = [],
+ deleteCalls = [],
+ listCalls = [],
+ execute = async () => ({ success: true, result: "ok" }),
+}: {
+ executeCalls?: ExecuteCall[];
+ deleteCalls?: DeleteCall[];
+ listCalls?: string[];
+ execute?: () => Promise;
+} = {}) {
+ return {
+ browsers: {
+ playwright: {
+ execute: async (sessionId: string, body: Record) => {
+ executeCalls.push({ sessionId, body });
+ return execute();
+ },
+ executors: {
+ list: async (sessionId: string) => {
+ listCalls.push(sessionId);
+ return { executors };
+ },
+ delete: async (name: string, params: Record) => {
+ deleteCalls.push({ name, params });
+ },
+ },
+ },
+ },
+ };
+}
+
+function toolResultText(result: { content: unknown }) {
+ return (result.content as Array<{ text: string }>)[0].text;
+}
+
+describe("execute_playwright_code executors", () => {
+ test("omits executor from the request when the agent did not name one", async () => {
+ const executeCalls: ExecuteCall[] = [];
+ const { client, close } = await connectTestMcp(
+ registerPlaywrightTool,
+ playwrightClient({ executeCalls }),
+ );
+
+ try {
+ await client.callTool({
+ name: "execute_playwright_code",
+ arguments: { code: "return 1", session_id: "ses_1" },
+ });
+ } finally {
+ await close();
+ }
+
+ expect(executeCalls).toHaveLength(1);
+ expect("executor" in executeCalls[0].body).toBe(false);
+ });
+
+ test("passes a named executor through to the API", async () => {
+ const executeCalls: ExecuteCall[] = [];
+ const { client, close } = await connectTestMcp(
+ registerPlaywrightTool,
+ playwrightClient({ executeCalls }),
+ );
+
+ try {
+ await client.callTool({
+ name: "execute_playwright_code",
+ arguments: {
+ code: "return 1",
+ session_id: "ses_1",
+ executor: "checkout-1",
+ },
+ });
+ } finally {
+ await close();
+ }
+
+ expect(executeCalls[0].body).toMatchObject({
+ code: "return 1",
+ executor: "checkout-1",
+ });
+ });
+
+ test.each(["", "has space", "a".repeat(65), "tab/1"])(
+ "rejects the invalid executor name %j before calling the API",
+ async (executor) => {
+ const executeCalls: ExecuteCall[] = [];
+ const { client, close } = await connectTestMcp(
+ registerPlaywrightTool,
+ playwrightClient({ executeCalls }),
+ );
+
+ try {
+ const result = await client.callTool({
+ name: "execute_playwright_code",
+ arguments: { code: "return 1", session_id: "ses_1", executor },
+ });
+ expect(result.isError).toBe(true);
+ } finally {
+ await close();
+ }
+
+ expect(executeCalls).toHaveLength(0);
+ },
+ );
+
+ test("reports the tab page was bound to when the API returns one", async () => {
+ const { client, close } = await connectTestMcp(
+ registerPlaywrightTool,
+ playwrightClient({
+ execute: async () => ({
+ success: true,
+ result: "Example Domain",
+ tab: { target_id: "A1B2", created: true },
+ }),
+ }),
+ );
+
+ try {
+ const result = await client.callTool({
+ name: "execute_playwright_code",
+ arguments: {
+ code: "return await page.title()",
+ session_id: "ses_1",
+ executor: "search",
+ },
+ });
+
+ expect(toolResultJSON(result)).toEqual({
+ success: true,
+ result: "Example Domain",
+ tab: { target_id: "A1B2", created: true },
+ });
+ } finally {
+ await close();
+ }
+ });
+
+ test("leaves tab out of the result when the API did not bind one", async () => {
+ const { client, close } = await connectTestMcp(
+ registerPlaywrightTool,
+ playwrightClient({
+ execute: async () => ({ success: false, error: "boom" }),
+ }),
+ );
+
+ try {
+ const result = await client.callTool({
+ name: "execute_playwright_code",
+ arguments: { code: "throw new Error('boom')", session_id: "ses_1" },
+ });
+
+ expect("tab" in toolResultJSON(result)).toBe(false);
+ } finally {
+ await close();
+ }
+ });
+
+ test("surfaces the current executors when the browser is at its named executor limit", async () => {
+ const limit = {
+ message: "browser already has 8 named executors",
+ executors,
+ };
+ const { client, close } = await connectTestMcp(
+ registerPlaywrightTool,
+ playwrightClient({
+ execute: async () => {
+ throw new APIError(409, limit, undefined, new Headers());
+ },
+ }),
+ );
+
+ try {
+ const result = await client.callTool({
+ name: "execute_playwright_code",
+ arguments: { code: "return 1", session_id: "ses_1", executor: "ninth" },
+ });
+
+ expect(result.isError).toBe(true);
+ const text = toolResultText(result);
+ expect(text).toStartWith("error in execute_playwright_code (execute):");
+ expect(text).toContain(JSON.stringify(limit));
+ } finally {
+ await close();
+ }
+ });
+});
+
+describe("manage_playwright_executors", () => {
+ test("lists the executors of a session", async () => {
+ const listCalls: string[] = [];
+ const { client, close } = await connectTestMcp(
+ registerPlaywrightTool,
+ playwrightClient({ listCalls }),
+ );
+
+ try {
+ const result = await client.callTool({
+ name: "manage_playwright_executors",
+ arguments: { action: "list", session_id: "browser-name" },
+ });
+
+ expect(result.isError).toBeFalsy();
+ expect(toolResultJSON(result).items).toEqual(executors);
+ } finally {
+ await close();
+ }
+
+ expect(listCalls).toEqual(["browser-name"]);
+ });
+
+ test("deletes a named executor and closes its tab by default", async () => {
+ const deleteCalls: DeleteCall[] = [];
+ const { client, close } = await connectTestMcp(
+ registerPlaywrightTool,
+ playwrightClient({ deleteCalls }),
+ );
+
+ try {
+ const result = await client.callTool({
+ name: "manage_playwright_executors",
+ arguments: { action: "delete", session_id: "ses_1", name: "checkout" },
+ });
+
+ expect(result.isError).toBeFalsy();
+ expect(toolResultText(result)).toBe("executor checkout deleted");
+ } finally {
+ await close();
+ }
+
+ expect(deleteCalls).toEqual([
+ { name: "checkout", params: { id_or_name: "ses_1" } },
+ ]);
+ });
+
+ test("can keep the tab open when deleting", async () => {
+ const deleteCalls: DeleteCall[] = [];
+ const { client, close } = await connectTestMcp(
+ registerPlaywrightTool,
+ playwrightClient({ deleteCalls }),
+ );
+
+ try {
+ await client.callTool({
+ name: "manage_playwright_executors",
+ arguments: {
+ action: "delete",
+ session_id: "ses_1",
+ name: "checkout",
+ close_tab: false,
+ },
+ });
+ } finally {
+ await close();
+ }
+
+ expect(deleteCalls[0].params).toEqual({
+ id_or_name: "ses_1",
+ close_tab: false,
+ });
+ });
+
+ test("describes deleting default as a restart", async () => {
+ const { client, close } = await connectTestMcp(
+ registerPlaywrightTool,
+ playwrightClient(),
+ );
+
+ try {
+ const result = await client.callTool({
+ name: "manage_playwright_executors",
+ arguments: { action: "delete", session_id: "ses_1", name: "default" },
+ });
+
+ expect(toolResultText(result)).toBe("default executor restarted");
+ } finally {
+ await close();
+ }
+ });
+
+ test("requires a name to delete", async () => {
+ const deleteCalls: DeleteCall[] = [];
+ const { client, close } = await connectTestMcp(
+ registerPlaywrightTool,
+ playwrightClient({ deleteCalls }),
+ );
+
+ try {
+ const result = await client.callTool({
+ name: "manage_playwright_executors",
+ arguments: { action: "delete", session_id: "ses_1" },
+ });
+
+ expect(result.isError).toBe(true);
+ expect(toolResultText(result)).toBe(
+ "error: name is required for delete.",
+ );
+ } finally {
+ await close();
+ }
+
+ expect(deleteCalls).toHaveLength(0);
+ });
+
+ test("reports an unknown executor through the shared tool-error path", async () => {
+ const { client, close } = await connectTestMcp(registerPlaywrightTool, {
+ browsers: {
+ playwright: {
+ executors: {
+ delete: async () => {
+ throw new APIError(
+ 404,
+ { code: "not_found", message: "executor not found" },
+ undefined,
+ new Headers(),
+ );
+ },
+ },
+ },
+ },
+ });
+
+ try {
+ const result = await client.callTool({
+ name: "manage_playwright_executors",
+ arguments: { action: "delete", session_id: "ses_1", name: "gone" },
+ });
+
+ expect(result.isError).toBe(true);
+ expect(toolResultText(result)).toStartWith(
+ "error in manage_playwright_executors (delete):",
+ );
+ } finally {
+ await close();
+ }
+ });
+});
diff --git a/src/lib/mcp/tools/playwright.ts b/src/lib/mcp/tools/playwright.ts
index b004cce..cb0fb2c 100644
--- a/src/lib/mcp/tools/playwright.ts
+++ b/src/lib/mcp/tools/playwright.ts
@@ -1,4 +1,5 @@
import type { McpServer } from "@modelcontextprotocol/server";
+import { APIError } from "@onkernel/sdk";
import { z } from "zod";
import {
defaultMcpDependencies,
@@ -9,13 +10,26 @@ import {
projectSelectionInputSchema,
} from "@/lib/mcp/project-selection";
import { longOperationOptions } from "@/lib/mcp/request-options";
-import { throwToolError } from "@/lib/mcp/responses";
+import {
+ errorResponse,
+ itemsJsonResponse,
+ textResponse,
+ throwToolError,
+ throwToolErrorWithApiBody,
+} from "@/lib/mcp/responses";
// The script budget the browser VM enforces. It is sent explicitly so the deadline the VM
// cancels on and the deadline our request waits for come from one value, and our request
// always outlasts the VM's rather than giving up while the script is still running.
const SCRIPT_BUDGET_SEC = 60;
+const executorNameSchema = z
+ .string()
+ .regex(
+ /^[A-Za-z0-9_-]{1,64}$/,
+ "executor name must be 1-64 characters of letters, digits, '_' or '-'",
+ );
+
export function registerPlaywrightTool(
server: McpServer,
options: McpDependencies = {
@@ -27,7 +41,12 @@ export function registerPlaywrightTool(
"execute_playwright_code",
{
description:
- "execute playwright/typescript automation against an existing KERNEL browser session. for reusable site actions, check `webmcp.listTools()` first and prefer a suitable structured tool; use playwright when none is exposed. does not create or delete browsers -- use manage_browsers for session lifecycle.",
+ "Execute arbitrary Playwright code in a fresh execution context against the browser. The code runs in the same VM as the browser, minimizing latency and maximizing throughput. It has access to 'page', 'context', 'browser', and 'webmcp' variables. Use 'webmcp.listTools()' to discover browser-wide WebMCP tools and 'webmcp.invokeTool(toolRef, input?, { timeoutSec? })' to invoke an exact registration. It can `return` a value, and this value is returned as the tool result.\n\n" +
+ "Every call runs in an executor: a dedicated Node.js process with its own browser connection. Calls on different executors run concurrently; calls on the same executor run one at a time. A timeout, crash, or blocked event loop in one executor does not affect other executors. After a timeout the executor keeps its process and drops its browser connection, so code abandoned by the timeout cannot keep driving the browser. After a crash or a blocked event loop, the next call on that executor starts a fresh process.\n\n" +
+ "Calls without 'executor' run in the executor named 'default', which always exists and is the same as passing 'executor: \"default\"'. In the default executor, 'page' is bound to an active tab reported by Chrome. In single-window sessions, this is the foreground tab. When multiple browser windows are open, Chrome reports one active tab per window and the selected window is unspecified. 'context' is the BrowserContext that owns the selected page. Use 'browser.contexts()' to select a context or page explicitly.\n\n" +
+ "Pass any other name to run the call in a named executor. The first call with a new name creates it. Each named executor owns a tab: its first call opens a new background tab in the default browser context, and 'page' is bound to that tab on every later call while it stays open. Opening it does not change the active tab of an existing window. If the tab is closed, the next call opens a new one and reports 'tab.created: true'. Executor code can still reach other tabs through 'context' and 'browser'; ownership only decides what 'page' is bound to. Use named executors to drive several tabs of one browser in parallel.\n\n" +
+ "A browser can have at most 8 named executors; the default executor does not count. A call that would create another fails with an error listing the current executors; delete one with manage_playwright_executors (action 'delete'). Named executors are not removed automatically while the browser runs; when it shuts down, they are removed and their tabs closed.\n\n" +
+ "A named call to a browser whose image predates executors fails with an error instead of running on the active tab; calls without 'executor' keep working on every image.",
inputSchema: z.object({
...projectSelectionInputSchema(),
code: z
@@ -39,6 +58,11 @@ export function registerPlaywrightTool(
.string()
.min(1, "session_id is required")
.describe("browser session id or name to execute the code against."),
+ executor: executorNameSchema
+ .describe(
+ "name of the executor to run the code in. omit or pass `default` to bind `page` to the active tab. any other name runs in a named executor that owns its own background tab; the first call with a new name creates it (the result's `tab.created` is true), and later calls with the same name reuse that tab and run one at a time. use distinct names to drive several tabs of one browser in parallel.",
+ )
+ .optional(),
}),
annotations: {
title: "execute playwright code",
@@ -48,7 +72,7 @@ export function registerPlaywrightTool(
openWorldHint: true,
},
},
- async ({ code, session_id, project, project_id }, ctx) => {
+ async ({ code, session_id, executor, project, project_id }, ctx) => {
if (!ctx.http?.authInfo) throw new Error("authentication required");
const client = options.createKernelClient(
ctx.http.authInfo.token,
@@ -61,7 +85,11 @@ export function registerPlaywrightTool(
const response = await client.browsers.playwright.execute(
session_id,
- { code, timeout_sec: SCRIPT_BUDGET_SEC },
+ {
+ code,
+ timeout_sec: SCRIPT_BUDGET_SEC,
+ ...(executor !== undefined && { executor }),
+ },
longOperationOptions(SCRIPT_BUDGET_SEC),
);
@@ -76,6 +104,7 @@ export function registerPlaywrightTool(
error: response.error,
stdout: response.stdout,
stderr: response.stderr,
+ ...(response.tab && { tab: response.tab }),
},
null,
2,
@@ -84,6 +113,16 @@ export function registerPlaywrightTool(
],
};
} catch (error) {
+ // A 409 means the browser is at its named executor limit. The body lists the
+ // current executors so the agent can reuse one or pick one to delete.
+ if (error instanceof APIError && error.status === 409) {
+ throwToolErrorWithApiBody(
+ "execute_playwright_code",
+ "execute",
+ error,
+ "the browser is at its named executor limit; list and delete executors with manage_playwright_executors.",
+ );
+ }
// No normal API response came back -- the session was gone, unleased, the request
// was rejected, or it timed out. Distinct from code that ran and threw, which comes
// back as a 200 with success: false so the agent can read the failure and adjust.
@@ -91,4 +130,73 @@ export function registerPlaywrightTool(
}
},
);
+
+ // manage_playwright_executors -- List and delete the executors execute_playwright_code runs in
+ server.registerTool(
+ "manage_playwright_executors",
+ {
+ description:
+ 'manage the playwright executors of a KERNEL browser session. an executor is the dedicated process that execute_playwright_code runs a call in; `default` always exists and binds `page` to the active tab, and every other name is a named executor that owns its own background tab. use "list" to see every executor (default first) with whether it is busy, when it was created and last used, and for named executors the target_id and url of the tab it owns. use "delete" to stop a named executor you no longer need and, by default, close its tab; a browser can have at most 8 named executors, so delete unneeded ones before creating more. a call running on a deleted executor fails, and the name can be reused afterwards. deleting `default` restarts it instead of removing it.',
+ inputSchema: z.object({
+ ...projectSelectionInputSchema(),
+ action: z.enum(["list", "delete"]).describe("operation to perform."),
+ session_id: z
+ .string()
+ .min(1, "session_id is required")
+ .describe("browser session id or name."),
+ name: executorNameSchema
+ .describe("(delete) executor name to delete.")
+ .optional(),
+ close_tab: z
+ .boolean()
+ .describe(
+ "(delete) close the tab the named executor owns. defaults to true. has no effect on `default`, which owns no tab.",
+ )
+ .optional(),
+ }),
+ annotations: {
+ title: "manage playwright executors",
+ readOnlyHint: false,
+ destructiveHint: true,
+ idempotentHint: false,
+ openWorldHint: false,
+ },
+ },
+ async (params, ctx) => {
+ if (!ctx.http?.authInfo) throw new Error("authentication required");
+ const client = options.createKernelClient(
+ ctx.http.authInfo.token,
+ projectForOperation(ctx.http.authInfo, params),
+ );
+
+ try {
+ switch (params.action) {
+ case "list": {
+ const { executors } =
+ await client.browsers.playwright.executors.list(
+ params.session_id,
+ );
+ return itemsJsonResponse(executors);
+ }
+ case "delete": {
+ if (!params.name)
+ return errorResponse("error: name is required for delete.");
+ await client.browsers.playwright.executors.delete(params.name, {
+ id_or_name: params.session_id,
+ ...(params.close_tab !== undefined && {
+ close_tab: params.close_tab,
+ }),
+ });
+ return textResponse(
+ params.name === "default"
+ ? "default executor restarted"
+ : `executor ${params.name} deleted`,
+ );
+ }
+ }
+ } catch (error) {
+ throwToolError("manage_playwright_executors", params.action, error);
+ }
+ },
+ );
}