diff --git a/README.md b/README.md index 090e342..b9e7fa0 100644 --- a/README.md +++ b/README.md @@ -304,7 +304,7 @@ Call `get_connection_context` before deciding whether to create or select a proj ### manage\_\* tools -- `manage_browsers` - Create, update, list, get, and delete browser sessions, and read archived telemetry for active or deleted sessions. Supports headless/stealth modes, profiles, proxies, viewports, extensions, and SSH tunneling. +- `manage_browsers` - Create, update, list, get, and delete browser sessions, and read archived telemetry for active or deleted sessions. Supports headless/stealth modes, profiles, proxies, viewports, extensions, names and tags, and SSH tunneling. The browser tools (`manage_browsers`, `computer_action`, `execute_playwright_code`, `execute_shell_command`, `browser_curl`, `manage_replays`) accept a live session's name in place of its `session_id`; deleted sessions, and `manage_browser_pools` release, take the ID only. - `manage_profiles` - Setup (with guided live browser session), search/list with pagination, get, and delete browser profiles for persisting cookies and logins. - `manage_projects` - Create, list, get, update, and delete organization projects. Inspect and update per-project resource limits. - `manage_api_keys` - Create, list, get, update, and delete org-wide or project-scoped API keys. Create returns the plaintext key once. @@ -332,7 +332,7 @@ Call `get_connection_context` before deciding whether to create or select a proj Project resources use the prefix `kernel://orgs/{organization_id}/projects/{project_id}`. -- `/browsers` and `/browsers/{session_id}` - List or access browser sessions +- `/browsers` and `/browsers/{session_id}` - List or access browser sessions (`{session_id}` may be a live session's ID or name) - `/browser-pools` and `/browser-pools/{id_or_name}` - List or access browser pools - `/profiles` and `/profiles/{profile_name}` - List or access browser profiles - `/apps` and `/apps/{app_name}` - List or access deployed apps diff --git a/bun.lock b/bun.lock index 57c052b..6e978dc 100644 --- a/bun.lock +++ b/bun.lock @@ -10,7 +10,7 @@ "@clerk/themes": "^2.4.19", "@modelcontextprotocol/sdk": "1.26.0", "@onkernel/managed-auth-react": "0.5.1", - "@onkernel/sdk": "^0.97.0", + "@onkernel/sdk": "^0.98.0", "@posthog/mcp": "0.10.1", "@types/jsonwebtoken": "^9.0.10", "@types/redis": "^4.0.11", @@ -21,7 +21,7 @@ "jszip": "^3.10.1", "lucide-react": "^0.534.0", "mcp-handler": "^1.1.0", - "next": "16.2.11", + "next": "^16.2.11", "next-themes": "^0.4.4", "playwright": "^1.49.1", "posthog-node": "^5.46.1", @@ -149,7 +149,7 @@ "@onkernel/managed-auth-react": ["@onkernel/managed-auth-react@0.5.1", "", { "dependencies": { "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" } }, "sha512-tRnx91QTqlop2otlXyOxmI+jHodAMCW0dytWyk4hvu26cjACqH0387S0nP4IfgBVkZW6rwdNoBYKOheQfIxMgA=="], - "@onkernel/sdk": ["@onkernel/sdk@0.97.0", "", {}, "sha512-Mi9//b9l81tl+eBPV3TpYwU9noriW7c0lFdJ8z5dj1SOQwdPB3nwdQDIfpewxzMeOBNng/qA2jkLkhopXWLbDw=="], + "@onkernel/sdk": ["@onkernel/sdk@0.98.0", "", {}, "sha512-FTvTDEPj3rS6INa+JTqso1XpnGkBKP3axvcAumX8dfd5nAI157mIlaZDWQ2k61MxwKpHDEvD5iNwD+sUk4OEcw=="], "@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 a649ff7..9a78182 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,7 @@ "@clerk/themes": "^2.4.19", "@modelcontextprotocol/sdk": "1.26.0", "@onkernel/managed-auth-react": "0.5.1", - "@onkernel/sdk": "^0.97.0", + "@onkernel/sdk": "^0.98.0", "@posthog/mcp": "0.10.1", "@types/jsonwebtoken": "^9.0.10", "@types/redis": "^4.0.11", diff --git a/src/lib/mcp/prompts.ts b/src/lib/mcp/prompts.ts index 513e9fe..3881490 100644 --- a/src/lib/mcp/prompts.ts +++ b/src/lib/mcp/prompts.ts @@ -98,7 +98,7 @@ Production-ready platform for deploying and hosting browser automation code. Han session_id: z .string() .describe( - "The browser session ID to debug (e.g., 'abc123example456xyz')", + "The browser session ID or name to debug (e.g., 'abc123example456xyz' or 'checkout-flow'). A name resolves only a live session; if the session was deleted, pass its ID so telemetry can still be read.", ), issue_description: z .string() @@ -135,7 +135,7 @@ kernel browsers playwright --help ## Telemetry Events (structured signal — works even after the session is deleted) -When telemetry was captured, it's usually the fastest way to pinpoint a failure — read it before reaching for screenshots or logs. If the session has been deleted, it's the only signal still available: every CLI command in this guide needs a live session. +When telemetry was captured, it's usually the fastest way to pinpoint a failure — read it before reaching for screenshots or logs. If the session has been deleted, it's the only signal still available: every CLI command in this guide needs a live session. A deleted session must be addressed by its ID; its name no longer resolves. Start broad: call \`manage_browsers\` with action "get_telemetry", session_id "${session_id}", and no filters. That starts at session creation and returns the first page (up to 100 events); page with \`next_offset\` as \`offset\` while \`has_more\` is true, preserving \`categories\`, \`until\`, and \`order\`. An empty unfiltered read is definitive: nothing was archived. Narrow when the output is too large to scan or you already know where to look: \`categories\` to isolate a signal you've spotted, \`order\` "desc" when the end of the session matters most, \`since\`/\`until\` to bracket a known failing step. Correlate event timestamps with the failing automation step. diff --git a/src/lib/mcp/tools/browser-curl.ts b/src/lib/mcp/tools/browser-curl.ts index 61b2b3e..6ceba04 100644 --- a/src/lib/mcp/tools/browser-curl.ts +++ b/src/lib/mcp/tools/browser-curl.ts @@ -33,7 +33,7 @@ export function registerBrowserCurlTool(server: McpServer) { "Send an HTTP request through an existing Kernel browser session's Chrome network stack. Use when the request needs that browser session's cookies, proxy, network context, or origin behavior; do not use for general documentation lookup or web search.", { ...projectSelectionInputSchema(), - session_id: z.string().describe("Browser session ID."), + session_id: z.string().describe("Browser session ID or name."), url: z.string().url().describe("Target http or https URL."), method: z .enum(["GET", "HEAD", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"]) diff --git a/src/lib/mcp/tools/browser-pools.ts b/src/lib/mcp/tools/browser-pools.ts index c06cb82..be732ec 100644 --- a/src/lib/mcp/tools/browser-pools.ts +++ b/src/lib/mcp/tools/browser-pools.ts @@ -388,7 +388,9 @@ export function registerBrowserPoolCapabilities(server: McpServer) { .optional(), session_id: z .string() - .describe("(release) Session ID of browser to release.") + .describe( + "(release) Session ID of the browser to release. Must be the ID, not the session name.", + ) .optional(), reuse: z .boolean() diff --git a/src/lib/mcp/tools/browsers.test.ts b/src/lib/mcp/tools/browsers.test.ts index 5200813..400642e 100644 --- a/src/lib/mcp/tools/browsers.test.ts +++ b/src/lib/mcp/tools/browsers.test.ts @@ -393,6 +393,136 @@ describe("manage_browsers region", () => { }); }); +describe("manage_browsers id or name", () => { + test("passes name and tags through create and update", async () => { + const createCalls: unknown[] = []; + const updateCalls: unknown[] = []; + const kernelClient = { + browsers: { + create: async (params: unknown) => { + createCalls.push(params); + return { session_id: "brr_123", name: "checkout-flow" }; + }, + update: async (idOrName: string, params: unknown) => { + updateCalls.push([idOrName, params]); + return { session_id: "brr_123" }; + }, + }, + }; + const { client, close } = await connectTestMcp( + registerBrowserCapabilities, + kernelClient, + ); + + try { + await client.callTool({ + name: "manage_browsers", + arguments: { + action: "create", + name: "checkout-flow", + tags: { team: "payments" }, + }, + }); + expect(createCalls).toEqual([ + { name: "checkout-flow", tags: { team: "payments" } }, + ]); + + await client.callTool({ + name: "manage_browsers", + arguments: { + action: "update", + session_id: "checkout-flow", + name: "checkout-flow-2", + tags: {}, + }, + }); + expect(updateCalls).toEqual([ + ["checkout-flow", { name: "checkout-flow-2", tags: {} }], + ]); + + await client.callTool({ + name: "manage_browsers", + arguments: { action: "update", session_id: "brr_123", name: "" }, + }); + expect(updateCalls[1]).toEqual(["brr_123", { name: "" }]); + } finally { + await close(); + } + }); + + test("passes query and tags filters to list", async () => { + const listCalls: unknown[] = []; + const kernelClient = { + browsers: { + list: async (params: unknown) => { + listCalls.push(params); + return { + getPaginatedItems: () => [], + has_more: false, + next_offset: null, + }; + }, + }, + }; + const { client, close } = await connectTestMcp( + registerBrowserCapabilities, + kernelClient, + ); + + try { + await client.callTool({ + name: "manage_browsers", + arguments: { + action: "list", + query: "checkout", + tags: { team: "payments" }, + }, + }); + expect(listCalls).toEqual([ + { query: "checkout", tags: { team: "payments" } }, + ]); + } finally { + await close(); + } + }); + + test("forwards a session name unchanged to get and delete", async () => { + const seen: string[] = []; + const kernelClient = { + browsers: { + retrieve: async (idOrName: string) => { + seen.push(`get:${idOrName}`); + return { session_id: "brr_123", name: idOrName }; + }, + deleteByID: async (idOrName: string) => { + seen.push(`delete:${idOrName}`); + }, + }, + }; + const { client, close } = await connectTestMcp( + registerBrowserCapabilities, + kernelClient, + ); + + try { + const got = toolResultJSON( + await client.callTool({ + name: "manage_browsers", + arguments: { action: "get", session_id: "checkout-flow" }, + }), + ); + expect(got.session_id).toBe("brr_123"); + await client.callTool({ + name: "manage_browsers", + arguments: { action: "delete", session_id: "checkout-flow" }, + }); + expect(seen).toEqual(["get:checkout-flow", "delete:checkout-flow"]); + } 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 8236fe1..51ddf4b 100644 --- a/src/lib/mcp/tools/browsers.ts +++ b/src/lib/mcp/tools/browsers.ts @@ -433,7 +433,7 @@ export function registerBrowserCapabilities( // manage_browsers -- Manage browser sessions and read archived telemetry server.tool( "manage_browsers", - 'Manage browser sessions and their archived telemetry. 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. 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 browser sessions and their archived telemetry. 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.', { ...projectSelectionInputSchema(), action: z @@ -442,7 +442,25 @@ export function registerBrowserCapabilities( session_id: z .string() .describe( - "Browser session ID. Required for update, get, get_telemetry, and delete actions.", + "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.", + ) + .optional(), + name: z + .string() + .describe( + "(create, update) Human-readable session name, unique among active sessions in the project. 1-255 chars of letters, digits, '.', '_' or '-', and not a cuid-like ID. While the session is live it can be passed as session_id to the browser tools (manage_browsers, computer_action, execute_playwright_code, execute_shell_command, browser_curl, manage_replays). On update, an empty string clears the name.", + ) + .optional(), + tags: z + .record(z.string()) + .describe( + "(create, update) Key-value tags for grouping sessions. Up to 50 pairs. On update, an empty object clears all tags. (list) Return only sessions carrying all of these tags.", + ) + .optional(), + query: z + .string() + .describe( + "(list) Text filter matched against session name, session ID, profile name or ID, proxy ID, or pool name.", ) .optional(), start_url: z @@ -679,6 +697,8 @@ export function registerBrowserCapabilities( createParams.chrome_policy = params.chrome_policy; } if (params.proxy_id) createParams.proxy_id = params.proxy_id; + if (params.name !== undefined) createParams.name = params.name; + if (params.tags !== undefined) createParams.tags = params.tags; const browserConfig = buildBrowserCreateConfig(params); if (!browserConfig.ok) return errorResponse(browserConfig.error); Object.assign(createParams, browserConfig.value); @@ -723,6 +743,8 @@ export function registerBrowserCapabilities( } else if (params.proxy_id !== undefined) { updateParams.proxy_id = params.proxy_id; } + if (params.name !== undefined) updateParams.name = params.name; + if (params.tags !== undefined) updateParams.tags = params.tags; const browserConfig = buildBrowserUpdateConfig(params); if (!browserConfig.ok) return errorResponse(browserConfig.error); Object.assign(updateParams, browserConfig.value); @@ -752,6 +774,8 @@ export function registerBrowserCapabilities( const page = await client.browsers.list({ ...(params.status && { status: params.status }), ...(params.region && { region: params.region }), + ...(params.query && { query: params.query }), + ...(params.tags !== undefined && { tags: params.tags }), ...(params.limit !== undefined && { limit: params.limit }), ...(params.offset !== undefined && { offset: params.offset }), }); diff --git a/src/lib/mcp/tools/computer-action.ts b/src/lib/mcp/tools/computer-action.ts index ff1f55f..2536200 100644 --- a/src/lib/mcp/tools/computer-action.ts +++ b/src/lib/mcp/tools/computer-action.ts @@ -257,7 +257,7 @@ export function registerComputerActionTool(server: McpServer) { "Execute computer actions on a browser session. Pass a single action for simple operations (e.g. one click or one screenshot), or pass multiple actions to batch them into a single request for lower latency (e.g. click, type, press_key in one call). Use sleep actions between steps when the page needs time to react (e.g. after a click that triggers navigation or animation). IMPORTANT: Always include a screenshot as the last action so you can see the result of your actions. Action types: click_mouse, move_mouse, type_text, press_key, scroll, drag_mouse, set_cursor, sleep, write_clipboard, read_clipboard, screenshot, get_mouse_position. screenshot, read_clipboard, and get_mouse_position return data, so they must be the last action if included.", { ...projectSelectionInputSchema(), - session_id: z.string().describe("Browser session ID."), + session_id: z.string().describe("Browser session ID or name."), actions: z .array(computerActionSchema) .min(1) diff --git a/src/lib/mcp/tools/playwright.ts b/src/lib/mcp/tools/playwright.ts index 3fd0120..1bf88ba 100644 --- a/src/lib/mcp/tools/playwright.ts +++ b/src/lib/mcp/tools/playwright.ts @@ -36,7 +36,7 @@ export function registerPlaywrightTool( session_id: z .string() .min(1, "session_id is required") - .describe("Browser session ID to execute the code against."), + .describe("Browser session ID or name to execute the code against."), }, { title: "Execute Playwright code", diff --git a/src/lib/mcp/tools/replays.ts b/src/lib/mcp/tools/replays.ts index 3baeda6..e7405fe 100644 --- a/src/lib/mcp/tools/replays.ts +++ b/src/lib/mcp/tools/replays.ts @@ -23,7 +23,7 @@ export function registerReplayTools(server: McpServer) { action: z .enum(["start", "stop", "list"]) .describe("Operation to perform."), - session_id: z.string().describe("Browser session ID."), + session_id: z.string().describe("Browser session ID or name."), replay_id: z.string().describe("(stop) Replay ID to stop.").optional(), framerate: z .number() @@ -84,7 +84,7 @@ export function registerReplayTools(server: McpServer) { 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, + id_or_name: params.session_id, }); return textResponse("Replay stopped successfully"); } diff --git a/src/lib/mcp/tools/shell.ts b/src/lib/mcp/tools/shell.ts index 71e8e05..3e8256a 100644 --- a/src/lib/mcp/tools/shell.ts +++ b/src/lib/mcp/tools/shell.ts @@ -30,7 +30,7 @@ export function registerShellTool( 'Execute a command synchronously inside a browser VM. Returns stdout, stderr, and exit code. The command field is the executable; use args for its arguments. Common uses: read files (command: "cat", args: ["/var/log/supervisord.log"]), list dirs (command: "ls", args: ["/var/log"]), check DNS (command: "cat", args: ["/etc/resolv.conf"]), test connectivity (command: "curl", args: ["-I", "https://example.com"]).', { ...projectSelectionInputSchema(), - session_id: z.string().describe("Browser session ID."), + session_id: z.string().describe("Browser session ID or name."), command: z .string() .describe("Executable to run (e.g., 'cat', 'ls', 'curl')."),