Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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