Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
bd034e4
Add browser session parity options
IlyaasK May 29, 2026
a689dcc
Clean up browser tool action validation
IlyaasK May 29, 2026
bbadbaf
Derive browser field scopes from map
IlyaasK May 29, 2026
6498c24
Guard empty browser update response
IlyaasK Jun 1, 2026
9b8716e
Add browser utility MCP tool
IlyaasK Jun 1, 2026
2c91a8a
Split browser utility MCP tools
IlyaasK Jun 1, 2026
a2e930b
Simplify browser curl params
IlyaasK Jun 1, 2026
4e867c8
Add browser pool parity fields
IlyaasK Jun 1, 2026
8a9eaad
Share MCP browser config and response helpers
IlyaasK Jun 1, 2026
91c1f00
Tighten browser pool start URL schema
IlyaasK Jun 1, 2026
ea927af
Clean up MCP resource handlers
IlyaasK Jun 1, 2026
680ef3b
Allow partial browser pool updates
IlyaasK Jun 1, 2026
5d4a8e6
Align browser tools with MCP responses
IlyaasK Jun 2, 2026
43487a0
Use SDK 0.60 browser pool update types
IlyaasK Jun 3, 2026
dcf3732
Address browser pool Cursor findings
IlyaasK Jun 3, 2026
1331dab
Simplify browser MCP review fixes
IlyaasK Jun 3, 2026
db71a9b
Improve MCP agent ergonomics
IlyaasK Jun 3, 2026
ca5d455
Fix browser pool review findings
IlyaasK Jun 3, 2026
dd77f9f
Resolve main merge conflicts
IlyaasK Jun 3, 2026
8dd29c8
Fix PR 112 review follow-ups
IlyaasK Jun 3, 2026
f438766
Keep paginated MCP responses structured
IlyaasK Jun 3, 2026
b3b3c74
Address PR 112 review nits
IlyaasK Jun 22, 2026
bb8ad61
Make pool fill_rate_per_minute integer-only
IlyaasK Jun 22, 2026
33b5e58
Allow fractional pool fill_rate_per_minute
IlyaasK Jun 22, 2026
87a580c
Use stable pool id in acquire release hint
IlyaasK Jun 22, 2026
6f3e4a7
Omit empty pool proxy_id
IlyaasK Jun 22, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 12 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -255,9 +255,9 @@ Many other MCP-capable tools accept:

Configure these values wherever the tool expects MCP server settings.

## Tools (15 total)
## Tools (16 total)

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.
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.

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.

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

### Standalone tools

- `computer_action` - Mouse, keyboard, and screenshot controls for browser sessions (click, type, press_key, scroll, move, get_position, screenshot).
- `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.
- `exec_command` - Run shell commands inside a browser VM. Returns decoded stdout/stderr.
- `search_docs` - Search Kernel platform documentation and guides.

## Resources

- `browsers://` - Access browser sessions (list all or get specific session)
- `browser_pools://` - Access browser pools (list all or get specific pool)
- `profiles://` - Access browser profiles (list all or get specific profile)
- `apps://` - Access deployed apps (list all or get specific app)
- `browsers://` - List browser sessions
- `browser-pools://` - List browser pools
- `profiles://` - List browser profiles
- `apps://` - List deployed apps
- `browsers://{session_id}` - Access one browser session
- `browser-pools://{id_or_name}` - Access one browser pool
- `profiles://{profile_name}` - Access one browser profile
- `apps://{app_name}` - Access one deployed app

## Prompts

Expand Down
4 changes: 2 additions & 2 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
"@clerk/themes": "^2.4.19",
"@mcp-ui/server": "^5.10.0",
"@modelcontextprotocol/sdk": "1.26.0",
"@onkernel/sdk": "^0.58.0",
"@onkernel/sdk": "^0.60.0",
"@types/jsonwebtoken": "^9.0.10",
"@types/redis": "^4.0.11",
"builtin-modules": "^5.0.0",
Expand Down
227 changes: 227 additions & 0 deletions src/lib/mcp/browser-config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,227 @@
import type { KernelClient } from "@/lib/mcp/kernel-client";

type BrowserCreateParams = NonNullable<
Parameters<KernelClient["browsers"]["create"]>[0]
>;
type BrowserUpdateParams = Parameters<KernelClient["browsers"]["update"]>[1];
type BrowserPoolCreateParams = Parameters<
KernelClient["browserPools"]["create"]
>[0];
type BrowserPoolUpdateParams = Parameters<
KernelClient["browserPools"]["update"]
>[1];

export type BrowserProfileParams = {
profile_name?: string;
profile_id?: string;
save_profile_changes?: boolean;
};

export type BrowserExtensionParams = {
extension_id?: string;
extension_name?: string;
};

export type BrowserViewportParams = {
viewport_width?: number;
viewport_height?: number;
viewport_refresh_rate?: number;
};

export type BrowserViewportUpdateParams = BrowserViewportParams & {
viewport_force?: boolean;
};

export type BrowserCreateConfigParams = BrowserProfileParams &
BrowserExtensionParams &
BrowserViewportParams & {
start_url?: string;
};

export type BrowserUpdateConfigParams = BrowserProfileParams &
BrowserViewportUpdateParams;

type BrowserProfileConfig = NonNullable<
| BrowserCreateParams["profile"]
| BrowserUpdateParams["profile"]
| BrowserPoolCreateParams["profile"]
| BrowserPoolUpdateParams["profile"]
>;

type BrowserExtensionConfig = NonNullable<
| BrowserCreateParams["extensions"]
| BrowserPoolCreateParams["extensions"]
| BrowserPoolUpdateParams["extensions"]
>;

type BrowserViewportConfig = NonNullable<
| BrowserCreateParams["viewport"]
| BrowserPoolCreateParams["viewport"]
| BrowserPoolUpdateParams["viewport"]
>;

type BrowserViewportUpdateConfig = NonNullable<BrowserUpdateParams["viewport"]>;

export type BrowserCreateConfig = Pick<
BrowserCreateParams,
"profile" | "extensions" | "viewport" | "start_url"
>;

export type BrowserUpdateConfig = Pick<
BrowserUpdateParams,
"profile" | "viewport"
>;

export type BrowserConfigResult<T> =
| { ok: true; value: T }
| { ok: false; error: string };

function configValue<T>(value: T): BrowserConfigResult<T> {
return { ok: true, value };
}

function configError<T>(message: string): BrowserConfigResult<T> {
return { ok: false, error: `Error: ${message}` };
}

function buildBrowserStartUrl(
startUrl: string | undefined,
): BrowserConfigResult<string | undefined> {
if (startUrl === undefined) return configValue(undefined);

try {
new URL(startUrl);
} catch {
return configError("start_url must be a valid URL.");
}

return configValue(startUrl);
}

function buildBrowserProfile(
params: BrowserProfileParams,
): BrowserConfigResult<BrowserProfileConfig | undefined> {
if (params.profile_name && params.profile_id) {
return configError("Cannot specify both profile_name and profile_id.");
}
if (
params.save_profile_changes !== undefined &&
!params.profile_name &&
!params.profile_id
) {
return configError(
"profile_name or profile_id is required when save_profile_changes is set.",
);
}
if (!params.profile_name && !params.profile_id) return configValue(undefined);
return configValue({
...(params.profile_name && { name: params.profile_name }),
...(params.profile_id && { id: params.profile_id }),
...(params.save_profile_changes !== undefined && {
save_changes: params.save_profile_changes,
}),
});
}

function buildBrowserExtensions(
params: BrowserExtensionParams,
): BrowserConfigResult<BrowserExtensionConfig | undefined> {
if (params.extension_id && params.extension_name) {
return configError("Cannot specify both extension_id and extension_name.");
}
if (!params.extension_id && !params.extension_name)
return configValue(undefined);
return configValue([
{
...(params.extension_id && { id: params.extension_id }),
...(params.extension_name && { name: params.extension_name }),
},
]);
}

function buildBrowserViewport(
params: BrowserViewportParams,
): BrowserConfigResult<BrowserViewportConfig | undefined> {
const width = params.viewport_width;
const height = params.viewport_height;
const hasViewportOptions =
width !== undefined ||
height !== undefined ||
params.viewport_refresh_rate !== undefined;

if (!hasViewportOptions) return configValue(undefined);
if (width === undefined || height === undefined) {
return configError(
"viewport_width and viewport_height must be provided together.",
);
}

return configValue({
width,
height,
...(params.viewport_refresh_rate !== undefined && {
refresh_rate: params.viewport_refresh_rate,
}),
});
}

function buildBrowserViewportUpdate(
params: BrowserViewportUpdateParams,
): BrowserConfigResult<BrowserViewportUpdateConfig | undefined> {
const viewport = buildBrowserViewport(params);
if (!viewport.ok) return viewport;

if (!viewport.value) {
if (params.viewport_force !== undefined) {
return configError(
"viewport_width and viewport_height must be provided when viewport_force is set.",
);
}
return configValue(undefined);
}

return configValue({
...viewport.value,
...(params.viewport_force !== undefined && {
force: params.viewport_force,
}),
});
}

export function buildBrowserCreateConfig(
params: BrowserCreateConfigParams,
): BrowserConfigResult<BrowserCreateConfig> {
const profile = buildBrowserProfile(params);
if (!profile.ok) return profile;

const extensions = buildBrowserExtensions(params);
if (!extensions.ok) return extensions;

const viewport = buildBrowserViewport(params);
if (!viewport.ok) return viewport;

const startUrl = buildBrowserStartUrl(params.start_url);
if (!startUrl.ok) return startUrl;

return configValue({
...(profile.value && { profile: profile.value }),
...(extensions.value && { extensions: extensions.value }),
...(viewport.value && { viewport: viewport.value }),
...(startUrl.value !== undefined && { start_url: startUrl.value }),
});
}

export function buildBrowserUpdateConfig(
params: BrowserUpdateConfigParams,
): BrowserConfigResult<BrowserUpdateConfig> {
const profile = buildBrowserProfile(params);
if (!profile.ok) return profile;

const viewport = buildBrowserViewportUpdate(params);
if (!viewport.ok) return viewport;

return configValue({
...(profile.value && { profile: profile.value }),
...(viewport.value && { viewport: viewport.value }),
});
}
1 change: 1 addition & 0 deletions src/lib/mcp/register.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ const standaloneToolsetAliases: Partial<Record<string, McpToolset>> = {
search_docs: "docs",
execute_playwright_code: "playwright",
exec_command: "shell",
browser_utilities: "browser_curl",
};

function isMcpToolset(value: string): value is McpToolset {
Expand Down
61 changes: 61 additions & 0 deletions src/lib/mcp/resource-templates.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import {
ResourceTemplate,
type McpServer,
} from "@modelcontextprotocol/sdk/server/mcp.js";
import { createKernelClient, type KernelClient } from "@/lib/mcp/kernel-client";

type JsonResourceTemplateOptions = {
name: string;
uriTemplate: string;
variableName: string;
resourceLabel: string;
read: (
client: KernelClient,
identifier: string,
) => Promise<unknown | null | undefined>;
};

function templateVariableValue(
variables: Record<string, string | string[]>,
name: string,
) {
const value = variables[name];
return Array.isArray(value) ? value[0] : value;
}

export function registerJsonResourceTemplate(
server: McpServer,
options: JsonResourceTemplateOptions,
) {
server.resource(
options.name,
new ResourceTemplate(options.uriTemplate, { list: undefined }),
async (uri, variables, extra) => {
if (!extra.authInfo) {
throw new Error("Authentication required");
}

const identifier = templateVariableValue(variables, options.variableName);
if (!identifier) {
throw new Error(`Invalid ${options.resourceLabel} URI: ${uri}`);
}

const client = createKernelClient(extra.authInfo.token);
const resource = await options.read(client, identifier);

if (!resource) {
throw new Error(`${options.resourceLabel} "${identifier}" not found`);
}

return {
contents: [
{
uri: uri.toString(),
mimeType: "application/json",
text: JSON.stringify(resource, null, 2),
},
],
};
},
);
}
Loading
Loading