Skip to content

Commit 5fef193

Browse files
committed
Split screenshot into its own read-only tool
Screenshot was a terminal action inside computer_action, which meant the one capability every multimodal model can use was gated behind a tool flagged destructive, and clients with approval prompts confirmed on a read-only capture. Extract it as a standalone screenshot tool and point tool descriptions at execute_playwright_code as the default way to drive a browser, leaving computer_action for surfaces no selector can reach. Region screenshots also reported the full viewport as the coordinate space while returning a cropped image, so the offset a caller needed to add was never stated. The new tool reports the crop offset instead.
1 parent 4bc4a43 commit 5fef193

8 files changed

Lines changed: 100 additions & 68 deletions

File tree

‎README.md‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -278,9 +278,10 @@ Self-hosted deployments can hide sensitive tool families by setting `KERNEL_MCP_
278278

279279
### Standalone tools
280280

281-
- `computer_action` - Mouse, keyboard, clipboard, and screenshot controls for browser sessions (click, type, press_key, scroll, move, get_position, read_clipboard, write_clipboard, screenshot).
281+
- `execute_playwright_code` - Execute Playwright/TypeScript code against an existing browser session. The primary way to drive a browser. Does not create or delete browsers - use `manage_browsers` for session lifecycle.
282+
- `screenshot` - Capture a PNG of what a browser session currently displays, optionally cropped to a region. Read-only.
283+
- `computer_action` - Mouse, keyboard, and clipboard input at screen coordinates (click, type, press_key, scroll, move, drag, get_position, read_clipboard, write_clipboard). Fallback for surfaces Playwright selectors can't reach, such as canvas apps and embedded PDFs; it depends on the model being able to locate targets in a screenshot, so prefer `execute_playwright_code`.
282284
- `browser_curl` - Send HTTP requests through an existing browser session's Chrome network stack.
283-
- `execute_playwright_code` - Execute Playwright/TypeScript code against an existing browser session. Does not create or delete browsers - use `manage_browsers` for session lifecycle.
284285
- `exec_command` - Run shell commands inside a browser VM. Returns decoded stdout/stderr.
285286
- `search_docs` - Search Kernel platform documentation and guides.
286287

‎src/lib/mcp/prompts.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,7 @@ kernel browsers process --help
129129
kernel browsers playwright --help
130130
\`\`\`
131131
132-
**MCP Exceptions:** The \`computer_action\` MCP tool with action "screenshot" is useful since it returns images directly to the agent, and \`manage_browsers\` with action "get_telemetry" reads structured telemetry events (see below).
132+
**MCP Exceptions:** The \`screenshot\` MCP tool is useful since it returns images directly to the agent, and \`manage_browsers\` with action "get_telemetry" reads structured telemetry events (see below).
133133
134134
---
135135
@@ -152,7 +152,7 @@ ${TELEMETRY_EVENT_CATALOG}
152152
kernel browsers get ${session_id}
153153
\`\`\`
154154
155-
### Take a screenshot (or use MCP computer_action with action "screenshot")
155+
### Take a screenshot (or use the MCP screenshot tool)
156156
\`\`\`bash
157157
kernel browsers screenshot ${session_id}
158158
\`\`\`

‎src/lib/mcp/register.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { registerProfileCapabilities } from "@/lib/mcp/tools/profiles";
1616
import { registerProjectCapabilities } from "@/lib/mcp/tools/projects";
1717
import { registerProxyTools } from "@/lib/mcp/tools/proxies";
1818
import { registerReplayTools } from "@/lib/mcp/tools/replays";
19+
import { registerScreenshotTool } from "@/lib/mcp/tools/screenshot";
1920
import { registerShellTool } from "@/lib/mcp/tools/shell";
2021

2122
type RegisterMcpToolset = (server: McpServer) => void;
@@ -31,6 +32,7 @@ const mcpToolRegistrations = [
3132
["proxies", registerProxyTools],
3233
["extensions", registerExtensionTools],
3334
["apps", registerAppCapabilities],
35+
["screenshot", registerScreenshotTool],
3436
["computer", registerComputerActionTool],
3537
["shell", registerShellTool],
3638
["playwright", registerPlaywrightTool],

‎src/lib/mcp/tools/browser-pools.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -451,7 +451,7 @@ export function registerBrowserPoolCapabilities(server: McpServer) {
451451
return jsonResponse({
452452
browser: summarizeAcquiredBrowser(browser),
453453
next_actions: [
454-
`Use computer_action with session_id "${browser.session_id}" to control this browser.`,
454+
`Use execute_playwright_code with session_id "${browser.session_id}" to drive this browser, and screenshot to see what it currently shows.`,
455455
`When finished, use manage_browser_pools with action "release", id_or_name "${poolId}", and session_id "${browser.session_id}".`,
456456
`Use manage_browsers with action "get" and session_id "${browser.session_id}" for full browser details.`,
457457
],

‎src/lib/mcp/tools/browsers.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -266,7 +266,7 @@ async function readBrowserTelemetry(
266266

267267
function browserSessionNextActions(sessionId: string) {
268268
return [
269-
`Use computer_action with session_id "${sessionId}" to inspect or control the browser.`,
269+
`Use execute_playwright_code with session_id "${sessionId}" to drive the browser, and screenshot to see what it currently shows.`,
270270
`Use manage_browsers with action "get" and session_id "${sessionId}" for full browser details.`,
271271
`Use manage_browsers with action "delete" and session_id "${sessionId}" when the session is no longer needed.`,
272272
];

‎src/lib/mcp/tools/computer-action.ts‎

Lines changed: 4 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@ const computerActionSchema = z.object({
2626
"sleep",
2727
"write_clipboard",
2828
"read_clipboard",
29-
"screenshot",
3029
"get_mouse_position",
3130
])
3231
.describe("Action type."),
@@ -107,26 +106,11 @@ const computerActionSchema = z.object({
107106
})
108107
.describe("Params for write_clipboard action.")
109108
.optional(),
110-
screenshot: z
111-
.object({
112-
region: z
113-
.object({
114-
x: z.number(),
115-
y: z.number(),
116-
width: z.number().int().min(1),
117-
height: z.number().int().min(1),
118-
})
119-
.optional(),
120-
})
121-
.describe(
122-
"Params for screenshot action. Omit or pass {} for full-page screenshot.",
123-
)
124-
.optional(),
125109
});
126110

127111
type ComputerActionParams = z.infer<typeof computerActionSchema>;
128112
type TerminalAction = ComputerActionParams & {
129-
type: "screenshot" | "get_mouse_position" | "read_clipboard";
113+
type: "get_mouse_position" | "read_clipboard";
130114
};
131115
type WriteClipboardAction = ComputerActionParams & { type: "write_clipboard" };
132116
type PrefixExecutionResult =
@@ -137,9 +121,7 @@ function isTerminalAction(
137121
action: ComputerActionParams | undefined,
138122
): action is TerminalAction {
139123
return (
140-
action?.type === "screenshot" ||
141-
action?.type === "get_mouse_position" ||
142-
action?.type === "read_clipboard"
124+
action?.type === "get_mouse_position" || action?.type === "read_clipboard"
143125
);
144126
}
145127

@@ -250,7 +232,7 @@ export function registerComputerActionTool(server: McpServer) {
250232
// computer_action -- Execute one or more computer actions on a browser session
251233
server.tool(
252234
"computer_action",
253-
"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.",
235+
"Drive a browser session with raw mouse and keyboard input at screen coordinates. Prefer execute_playwright_code for anything a selector can reach -- it is faster, deterministic, and does not depend on the model's ability to locate targets in an image. Reach for this tool only when there is no selector to target: canvas apps, embedded PDFs, native dialogs, drag interactions. Coordinates come from a screenshot tool call, and screen coordinates are only as accurate as the model's pixel grounding, so verify with screenshot after acting. Pass a single action, or several to batch them into one request for lower latency (e.g. click, type, press_key). Use sleep actions between steps when the page needs time to react. Action types: click_mouse, move_mouse, type_text, press_key, scroll, drag_mouse, set_cursor, sleep, write_clipboard, read_clipboard, get_mouse_position. read_clipboard and get_mouse_position return data, so they must be the last action if included.",
254236
{
255237
session_id: z.string().describe("Browser session ID."),
256238
actions: z
@@ -261,7 +243,7 @@ export function registerComputerActionTool(server: McpServer) {
261243
),
262244
},
263245
{
264-
title: "Control browser (mouse, keyboard, screenshot)",
246+
title: "Control browser (mouse, keyboard)",
265247
readOnlyHint: false,
266248
destructiveHint: true,
267249
idempotentHint: false,
@@ -288,45 +270,6 @@ export function registerComputerActionTool(server: McpServer) {
288270

289271
const { executedActionCount } = prefixResult;
290272

291-
if (terminalAction?.type === "screenshot") {
292-
const screenshotParams = terminalAction.screenshot;
293-
const screenshotOpts = screenshotParams?.region
294-
? { region: screenshotParams.region }
295-
: undefined;
296-
const [screenshotResponse, browserInfo] = await Promise.all([
297-
client.browsers.computer.captureScreenshot(
298-
session_id,
299-
screenshotOpts,
300-
),
301-
client.browsers.retrieve(session_id),
302-
]);
303-
const blob = await screenshotResponse.blob();
304-
const buffer = Buffer.from(await blob.arrayBuffer());
305-
const viewport = browserInfo.viewport;
306-
const content: Array<
307-
| { type: "text"; text: string }
308-
| { type: "image"; data: string; mimeType: string }
309-
> = [];
310-
if (executedActionCount > 0) {
311-
content.push({
312-
type: "text",
313-
text: `Executed ${executedActionCount} action(s), then captured screenshot.`,
314-
});
315-
}
316-
content.push({
317-
type: "text",
318-
text: viewport
319-
? `Viewport: ${viewport.width}x${viewport.height}. Use these dimensions as the coordinate space for click, scroll, and move actions.`
320-
: "Could not determine viewport dimensions. Use manage_browsers with action 'get' to check the browser's viewport.",
321-
});
322-
content.push({
323-
type: "image",
324-
data: buffer.toString("base64"),
325-
mimeType: "image/png",
326-
});
327-
return { content };
328-
}
329-
330273
if (terminalAction?.type === "get_mouse_position") {
331274
const position =
332275
await client.browsers.computer.getMousePosition(session_id);

‎src/lib/mcp/tools/playwright.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ export function registerPlaywrightTool(server: McpServer) {
66
// execute_playwright_code -- Run Playwright/TypeScript code against a browser
77
server.tool(
88
"execute_playwright_code",
9-
"Execute Playwright/TypeScript automation code against an existing Kernel browser session. Does not create or delete browsers -- use manage_browsers to manage session lifecycle.",
9+
"Execute Playwright/TypeScript automation code against an existing Kernel browser session. This is the primary way to drive a browser: navigation, clicks, form fills, and extraction should all go through here rather than raw coordinate input, and `await page.locator('main').ariaSnapshot()` gives you the accessibility tree to locate elements without needing a screenshot. Does not create or delete browsers -- use manage_browsers to manage session lifecycle.",
1010
{
1111
code: z
1212
.string()

‎src/lib/mcp/tools/screenshot.ts‎

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2+
import { z } from "zod";
3+
import { createKernelClient } from "@/lib/mcp/kernel-client";
4+
import { toolErrorResponse } from "@/lib/mcp/responses";
5+
6+
const regionSchema = z.object({
7+
x: z.number(),
8+
y: z.number(),
9+
width: z.number().int().min(1),
10+
height: z.number().int().min(1),
11+
});
12+
13+
type Region = z.infer<typeof regionSchema>;
14+
15+
async function coordinateSpaceText(
16+
client: ReturnType<typeof createKernelClient>,
17+
sessionId: string,
18+
region: Region | undefined,
19+
) {
20+
if (region) {
21+
// The image is cropped, so its top-left is the region origin, not the screen
22+
// origin. Computer actions take screen coordinates, so spell out the offset
23+
// rather than leaving the model to guess which space it's looking at.
24+
return `Cropped region ${region.width}x${region.height} at screen offset (${region.x}, ${region.y}). Image coordinates start at the crop, so add the offset to get screen coordinates: screen_x = ${region.x} + image_x, screen_y = ${region.y} + image_y.`;
25+
}
26+
27+
const { viewport } = await client.browsers.retrieve(sessionId);
28+
if (!viewport) {
29+
return "Could not determine viewport dimensions. Use manage_browsers with action 'get' to check the browser's viewport.";
30+
}
31+
32+
return `Full screen ${viewport.width}x${viewport.height}. Image coordinates are screen coordinates.`;
33+
}
34+
35+
export function registerScreenshotTool(server: McpServer) {
36+
// screenshot -- Capture what a browser session currently shows
37+
server.tool(
38+
"screenshot",
39+
"Capture a PNG screenshot of what a browser session currently displays. Read-only: it observes the session without changing it. Use it to see page state, confirm what an automation did, or diagnose a stuck flow. To act on the page, prefer execute_playwright_code.",
40+
{
41+
session_id: z.string().describe("Browser session ID."),
42+
region: regionSchema
43+
.describe(
44+
"Crop to this screen region. Omit to capture the full screen.",
45+
)
46+
.optional(),
47+
},
48+
{
49+
title: "Screenshot browser session",
50+
readOnlyHint: true,
51+
destructiveHint: false,
52+
idempotentHint: true,
53+
openWorldHint: true,
54+
},
55+
async ({ session_id, region }, extra) => {
56+
if (!extra.authInfo) throw new Error("Authentication required");
57+
const client = createKernelClient(extra.authInfo.token);
58+
59+
try {
60+
const [screenshotResponse, spaceText] = await Promise.all([
61+
client.browsers.computer.captureScreenshot(
62+
session_id,
63+
region ? { region } : undefined,
64+
),
65+
coordinateSpaceText(client, session_id, region),
66+
]);
67+
68+
const blob = await screenshotResponse.blob();
69+
const buffer = Buffer.from(await blob.arrayBuffer());
70+
71+
return {
72+
content: [
73+
{ type: "text" as const, text: spaceText },
74+
{
75+
type: "image" as const,
76+
data: buffer.toString("base64"),
77+
mimeType: "image/png",
78+
},
79+
],
80+
};
81+
} catch (error) {
82+
return toolErrorResponse("screenshot", "capture", error);
83+
}
84+
},
85+
);
86+
}

0 commit comments

Comments
 (0)