Skip to content

Commit f59b6e4

Browse files
authored
Merge pull request #112 from kernel/browser-pools-parity-mcp-tool
Add browser pool parity fields to MCP
2 parents b671700 + 3aa6fc7 commit f59b6e4

16 files changed

Lines changed: 1029 additions & 741 deletions

README.md

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -255,9 +255,9 @@ Many other MCP-capable tools accept:
255255

256256
Configure these values wherever the tool expects MCP server settings.
257257

258-
## Tools (15 total)
258+
## Tools (16 total)
259259

260-
Each Kernel feature has a single `manage_*` tool with an `action` parameter, keeping the tool set small and consistent. Four standalone tools handle high-frequency workflows.
260+
Each Kernel feature has a single `manage_*` tool with an `action` parameter, keeping the tool set small and consistent. Five standalone tools handle high-frequency workflows.
261261

262262
Self-hosted deployments can hide sensitive tool families by setting `KERNEL_MCP_DISABLED_TOOLSETS` to a comma-separated list. For example, `KERNEL_MCP_DISABLED_TOOLSETS=api_keys` prevents `manage_api_keys` from being registered.
263263

@@ -277,17 +277,22 @@ Self-hosted deployments can hide sensitive tool families by setting `KERNEL_MCP_
277277

278278
### Standalone tools
279279

280-
- `computer_action` - Mouse, keyboard, and screenshot controls for browser sessions (click, type, press_key, scroll, move, get_position, screenshot).
280+
- `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+
- `browser_curl` - Send HTTP requests through an existing browser session's Chrome network stack.
281282
- `execute_playwright_code` - Execute Playwright/TypeScript code against a browser with automatic video replay and cleanup.
282283
- `exec_command` - Run shell commands inside a browser VM. Returns decoded stdout/stderr.
283284
- `search_docs` - Search Kernel platform documentation and guides.
284285

285286
## Resources
286287

287-
- `browsers://` - Access browser sessions (list all or get specific session)
288-
- `browser_pools://` - Access browser pools (list all or get specific pool)
289-
- `profiles://` - Access browser profiles (list all or get specific profile)
290-
- `apps://` - Access deployed apps (list all or get specific app)
288+
- `browsers://` - List browser sessions
289+
- `browser-pools://` - List browser pools
290+
- `profiles://` - List browser profiles
291+
- `apps://` - List deployed apps
292+
- `browsers://{session_id}` - Access one browser session
293+
- `browser-pools://{id_or_name}` - Access one browser pool
294+
- `profiles://{profile_name}` - Access one browser profile
295+
- `apps://{app_name}` - Access one deployed app
291296

292297
## Prompts
293298

bun.lock

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434
"@clerk/themes": "^2.4.19",
3535
"@mcp-ui/server": "^5.10.0",
3636
"@modelcontextprotocol/sdk": "1.26.0",
37-
"@onkernel/sdk": "^0.58.0",
37+
"@onkernel/sdk": "^0.60.0",
3838
"@types/jsonwebtoken": "^9.0.10",
3939
"@types/redis": "^4.0.11",
4040
"builtin-modules": "^5.0.0",

src/lib/mcp/browser-config.ts

Lines changed: 227 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,227 @@
1+
import type { KernelClient } from "@/lib/mcp/kernel-client";
2+
3+
type BrowserCreateParams = NonNullable<
4+
Parameters<KernelClient["browsers"]["create"]>[0]
5+
>;
6+
type BrowserUpdateParams = Parameters<KernelClient["browsers"]["update"]>[1];
7+
type BrowserPoolCreateParams = Parameters<
8+
KernelClient["browserPools"]["create"]
9+
>[0];
10+
type BrowserPoolUpdateParams = Parameters<
11+
KernelClient["browserPools"]["update"]
12+
>[1];
13+
14+
export type BrowserProfileParams = {
15+
profile_name?: string;
16+
profile_id?: string;
17+
save_profile_changes?: boolean;
18+
};
19+
20+
export type BrowserExtensionParams = {
21+
extension_id?: string;
22+
extension_name?: string;
23+
};
24+
25+
export type BrowserViewportParams = {
26+
viewport_width?: number;
27+
viewport_height?: number;
28+
viewport_refresh_rate?: number;
29+
};
30+
31+
export type BrowserViewportUpdateParams = BrowserViewportParams & {
32+
viewport_force?: boolean;
33+
};
34+
35+
export type BrowserCreateConfigParams = BrowserProfileParams &
36+
BrowserExtensionParams &
37+
BrowserViewportParams & {
38+
start_url?: string;
39+
};
40+
41+
export type BrowserUpdateConfigParams = BrowserProfileParams &
42+
BrowserViewportUpdateParams;
43+
44+
type BrowserProfileConfig = NonNullable<
45+
| BrowserCreateParams["profile"]
46+
| BrowserUpdateParams["profile"]
47+
| BrowserPoolCreateParams["profile"]
48+
| BrowserPoolUpdateParams["profile"]
49+
>;
50+
51+
type BrowserExtensionConfig = NonNullable<
52+
| BrowserCreateParams["extensions"]
53+
| BrowserPoolCreateParams["extensions"]
54+
| BrowserPoolUpdateParams["extensions"]
55+
>;
56+
57+
type BrowserViewportConfig = NonNullable<
58+
| BrowserCreateParams["viewport"]
59+
| BrowserPoolCreateParams["viewport"]
60+
| BrowserPoolUpdateParams["viewport"]
61+
>;
62+
63+
type BrowserViewportUpdateConfig = NonNullable<BrowserUpdateParams["viewport"]>;
64+
65+
export type BrowserCreateConfig = Pick<
66+
BrowserCreateParams,
67+
"profile" | "extensions" | "viewport" | "start_url"
68+
>;
69+
70+
export type BrowserUpdateConfig = Pick<
71+
BrowserUpdateParams,
72+
"profile" | "viewport"
73+
>;
74+
75+
export type BrowserConfigResult<T> =
76+
| { ok: true; value: T }
77+
| { ok: false; error: string };
78+
79+
function configValue<T>(value: T): BrowserConfigResult<T> {
80+
return { ok: true, value };
81+
}
82+
83+
function configError<T>(message: string): BrowserConfigResult<T> {
84+
return { ok: false, error: `Error: ${message}` };
85+
}
86+
87+
function buildBrowserStartUrl(
88+
startUrl: string | undefined,
89+
): BrowserConfigResult<string | undefined> {
90+
if (startUrl === undefined) return configValue(undefined);
91+
92+
try {
93+
new URL(startUrl);
94+
} catch {
95+
return configError("start_url must be a valid URL.");
96+
}
97+
98+
return configValue(startUrl);
99+
}
100+
101+
function buildBrowserProfile(
102+
params: BrowserProfileParams,
103+
): BrowserConfigResult<BrowserProfileConfig | undefined> {
104+
if (params.profile_name && params.profile_id) {
105+
return configError("Cannot specify both profile_name and profile_id.");
106+
}
107+
if (
108+
params.save_profile_changes !== undefined &&
109+
!params.profile_name &&
110+
!params.profile_id
111+
) {
112+
return configError(
113+
"profile_name or profile_id is required when save_profile_changes is set.",
114+
);
115+
}
116+
if (!params.profile_name && !params.profile_id) return configValue(undefined);
117+
return configValue({
118+
...(params.profile_name && { name: params.profile_name }),
119+
...(params.profile_id && { id: params.profile_id }),
120+
...(params.save_profile_changes !== undefined && {
121+
save_changes: params.save_profile_changes,
122+
}),
123+
});
124+
}
125+
126+
function buildBrowserExtensions(
127+
params: BrowserExtensionParams,
128+
): BrowserConfigResult<BrowserExtensionConfig | undefined> {
129+
if (params.extension_id && params.extension_name) {
130+
return configError("Cannot specify both extension_id and extension_name.");
131+
}
132+
if (!params.extension_id && !params.extension_name)
133+
return configValue(undefined);
134+
return configValue([
135+
{
136+
...(params.extension_id && { id: params.extension_id }),
137+
...(params.extension_name && { name: params.extension_name }),
138+
},
139+
]);
140+
}
141+
142+
function buildBrowserViewport(
143+
params: BrowserViewportParams,
144+
): BrowserConfigResult<BrowserViewportConfig | undefined> {
145+
const width = params.viewport_width;
146+
const height = params.viewport_height;
147+
const hasViewportOptions =
148+
width !== undefined ||
149+
height !== undefined ||
150+
params.viewport_refresh_rate !== undefined;
151+
152+
if (!hasViewportOptions) return configValue(undefined);
153+
if (width === undefined || height === undefined) {
154+
return configError(
155+
"viewport_width and viewport_height must be provided together.",
156+
);
157+
}
158+
159+
return configValue({
160+
width,
161+
height,
162+
...(params.viewport_refresh_rate !== undefined && {
163+
refresh_rate: params.viewport_refresh_rate,
164+
}),
165+
});
166+
}
167+
168+
function buildBrowserViewportUpdate(
169+
params: BrowserViewportUpdateParams,
170+
): BrowserConfigResult<BrowserViewportUpdateConfig | undefined> {
171+
const viewport = buildBrowserViewport(params);
172+
if (!viewport.ok) return viewport;
173+
174+
if (!viewport.value) {
175+
if (params.viewport_force !== undefined) {
176+
return configError(
177+
"viewport_width and viewport_height must be provided when viewport_force is set.",
178+
);
179+
}
180+
return configValue(undefined);
181+
}
182+
183+
return configValue({
184+
...viewport.value,
185+
...(params.viewport_force !== undefined && {
186+
force: params.viewport_force,
187+
}),
188+
});
189+
}
190+
191+
export function buildBrowserCreateConfig(
192+
params: BrowserCreateConfigParams,
193+
): BrowserConfigResult<BrowserCreateConfig> {
194+
const profile = buildBrowserProfile(params);
195+
if (!profile.ok) return profile;
196+
197+
const extensions = buildBrowserExtensions(params);
198+
if (!extensions.ok) return extensions;
199+
200+
const viewport = buildBrowserViewport(params);
201+
if (!viewport.ok) return viewport;
202+
203+
const startUrl = buildBrowserStartUrl(params.start_url);
204+
if (!startUrl.ok) return startUrl;
205+
206+
return configValue({
207+
...(profile.value && { profile: profile.value }),
208+
...(extensions.value && { extensions: extensions.value }),
209+
...(viewport.value && { viewport: viewport.value }),
210+
...(startUrl.value !== undefined && { start_url: startUrl.value }),
211+
});
212+
}
213+
214+
export function buildBrowserUpdateConfig(
215+
params: BrowserUpdateConfigParams,
216+
): BrowserConfigResult<BrowserUpdateConfig> {
217+
const profile = buildBrowserProfile(params);
218+
if (!profile.ok) return profile;
219+
220+
const viewport = buildBrowserViewportUpdate(params);
221+
if (!viewport.ok) return viewport;
222+
223+
return configValue({
224+
...(profile.value && { profile: profile.value }),
225+
...(viewport.value && { viewport: viewport.value }),
226+
});
227+
}

src/lib/mcp/register.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ const standaloneToolsetAliases: Partial<Record<string, McpToolset>> = {
4848
search_docs: "docs",
4949
execute_playwright_code: "playwright",
5050
exec_command: "shell",
51+
browser_utilities: "browser_curl",
5152
};
5253

5354
function isMcpToolset(value: string): value is McpToolset {

src/lib/mcp/resource-templates.ts

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import {
2+
ResourceTemplate,
3+
type McpServer,
4+
} from "@modelcontextprotocol/sdk/server/mcp.js";
5+
import { createKernelClient, type KernelClient } from "@/lib/mcp/kernel-client";
6+
7+
type JsonResourceTemplateOptions = {
8+
name: string;
9+
uriTemplate: string;
10+
variableName: string;
11+
resourceLabel: string;
12+
read: (
13+
client: KernelClient,
14+
identifier: string,
15+
) => Promise<unknown | null | undefined>;
16+
};
17+
18+
function templateVariableValue(
19+
variables: Record<string, string | string[]>,
20+
name: string,
21+
) {
22+
const value = variables[name];
23+
return Array.isArray(value) ? value[0] : value;
24+
}
25+
26+
export function registerJsonResourceTemplate(
27+
server: McpServer,
28+
options: JsonResourceTemplateOptions,
29+
) {
30+
server.resource(
31+
options.name,
32+
new ResourceTemplate(options.uriTemplate, { list: undefined }),
33+
async (uri, variables, extra) => {
34+
if (!extra.authInfo) {
35+
throw new Error("Authentication required");
36+
}
37+
38+
const identifier = templateVariableValue(variables, options.variableName);
39+
if (!identifier) {
40+
throw new Error(`Invalid ${options.resourceLabel} URI: ${uri}`);
41+
}
42+
43+
const client = createKernelClient(extra.authInfo.token);
44+
const resource = await options.read(client, identifier);
45+
46+
if (!resource) {
47+
throw new Error(`${options.resourceLabel} "${identifier}" not found`);
48+
}
49+
50+
return {
51+
contents: [
52+
{
53+
uri: uri.toString(),
54+
mimeType: "application/json",
55+
text: JSON.stringify(resource, null, 2),
56+
},
57+
],
58+
};
59+
},
60+
);
61+
}

0 commit comments

Comments
 (0)