Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
51087f5
Add browser session parity options
IlyaasK May 29, 2026
9f2a3a5
Clean up browser tool action validation
IlyaasK May 29, 2026
de97ef4
Derive browser field scopes from map
IlyaasK May 29, 2026
a25eecf
Guard empty browser update response
IlyaasK Jun 1, 2026
1764932
Add browser utility MCP tool
IlyaasK Jun 1, 2026
a5909ff
Split browser utility MCP tools
IlyaasK Jun 1, 2026
ea9503f
Simplify browser curl params
IlyaasK Jun 1, 2026
692e156
Add browser pool parity fields
IlyaasK Jun 1, 2026
55d4fca
Share MCP browser config and response helpers
IlyaasK Jun 1, 2026
5b1841a
Tighten browser pool start URL schema
IlyaasK Jun 1, 2026
c1074ec
Clean up MCP resource handlers
IlyaasK Jun 1, 2026
c6e6427
Allow partial browser pool updates
IlyaasK Jun 1, 2026
0f021bb
Add admin cleanup MCP passthroughs
IlyaasK Jun 1, 2026
8fbfcf4
Deduplicate MCP response formatting
IlyaasK Jun 1, 2026
27afef7
Merge branch 'browser-pools-parity-mcp-tool' into admin-app-proxy-ext…
IlyaasK Jun 11, 2026
cc8e9bf
Merge branch 'main' into admin-app-proxy-extension-cleanup-mcp-tool
IlyaasK Jun 23, 2026
03135c4
Restore textResponse import in apps.ts
IlyaasK Jun 23, 2026
2d5bd1f
Fix manage_apps destructive hint and profile setup existence check
IlyaasK Jun 23, 2026
6012a5a
Drop .nullable() from project update_limits params
IlyaasK Jun 24, 2026
8a9d722
Keep empty list responses as structured JSON
IlyaasK Jun 24, 2026
70c499d
Don't show setup hint on empty profile search
IlyaasK Jun 24, 2026
af60879
Note empty profile search on the paginated path too
IlyaasK Jun 24, 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
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -264,13 +264,13 @@ Self-hosted deployments can hide sensitive tool families by setting `KERNEL_MCP_
### manage\_\* tools

- `manage_browsers` - Create, list, get, and delete browser sessions. Supports headless/stealth modes, profiles, proxies, viewports, extensions, and SSH tunneling.
- `manage_profiles` - Setup (with guided live browser session), list, and delete browser profiles for persisting cookies and logins.
- `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.
- `manage_browser_pools` - Create, list, get, delete, and flush pools of pre-warmed browsers. Acquire and release browsers from pools.
- `manage_proxies` - Create, list, and delete proxy configurations (datacenter, ISP, residential, mobile, custom).
- `manage_proxies` - Create, list, get, check, and delete proxy configurations (datacenter, ISP, residential, mobile, custom).
- `manage_extensions` - List and delete uploaded browser extensions.
- `manage_apps` - List apps, invoke actions, get/list deployments, and get invocation results.
- `manage_projects` - Create, list, get, update, and delete organization projects.
- `manage_api_keys` - Create, list, get, update, and delete Kernel API keys. Create returns the plaintext key once.
- `manage_apps` - List/search apps, invoke actions, get/list/delete deployments, and get invocation results.
- `manage_auth_connections` - Create, list, get, delete managed auth connections; start login flows (returns a hosted URL and live view); submit MFA codes or SSO selections.
- `manage_credentials` - Create, list, get, update, and delete stored credentials; fetch a current TOTP code for credentials with a configured totp_secret.
- `manage_credential_providers` - Create, list, get, update, and delete external credential providers (e.g. 1Password); list available items and test the provider connection.
Expand Down
10 changes: 6 additions & 4 deletions src/lib/mcp/responses.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,15 +29,17 @@ export function itemsJsonResponse<T, U = T>(
items: T[],
options: ItemsJsonResponseOptions<T, U> = {},
) {
if (items.length === 0 && options.emptyText) {
return textResponse(options.emptyText);
}
// Keep the response shape uniform JSON for every list outcome. When empty,
// surface emptyText as a `note` (e.g. setup guidance) rather than swapping to
// a plain-text body, so agents always get { items, has_more, next_offset }.
const note =
items.length === 0 ? (options.emptyText ?? options.note) : options.note;

return jsonResponse({
items: options.mapItem ? items.map(options.mapItem) : items,
has_more: options.has_more,
next_offset: options.next_offset,
...(options.note && { note: options.note }),
...(note && { note }),
});
}

Expand Down
29 changes: 25 additions & 4 deletions src/lib/mcp/tools/apps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
errorResponse,
jsonResponse,
paginatedJsonResponse,
textResponse,
toolErrorResponse,
} from "@/lib/mcp/responses";
import { paginationParams } from "@/lib/mcp/schemas";
Expand Down Expand Up @@ -45,14 +46,15 @@ export function registerAppCapabilities(server: McpServer) {
// manage_apps -- List apps, invoke actions, manage deployments, check invocations
server.tool(
"manage_apps",
'Manage Kernel apps when an agent needs to discover deployed app actions, invoke an app, or inspect deployment/invocation state. Use "list_apps" before invoking an unknown app, "invoke" to run an action, and get/list actions to inspect results.',
'Manage Kernel apps when an agent needs to discover deployed app actions, invoke an app, or inspect deployment/invocation state. Use "list_apps" before invoking an unknown app, "invoke" to run an action, get/list actions to inspect results, and "delete_deployment" to remove a deployment.',
{
action: z
.enum([
"list_apps",
"invoke",
"get_deployment",
"list_deployments",
"delete_deployment",
"get_invocation",
])
.describe("Operation to perform."),
Expand All @@ -65,9 +67,10 @@ export function registerAppCapabilities(server: McpServer) {
version: z
.string()
.describe(
"(list_apps, invoke) App version filter. Defaults to 'latest' for invoke.",
"(list_apps, invoke, list_deployments) App version filter. Defaults to 'latest' for invoke. Deployment version filtering requires app_name.",
)
.optional(),
query: z.string().describe("(list_apps) Search apps by name.").optional(),
action_name: z
.string()
.describe("(invoke) Action to execute within the app.")
Expand All @@ -78,7 +81,7 @@ export function registerAppCapabilities(server: McpServer) {
.optional(),
deployment_id: z
.string()
.describe("(get_deployment) Deployment ID to retrieve.")
.describe("(get_deployment, delete_deployment) Deployment ID.")
Comment thread
cursor[bot] marked this conversation as resolved.
.optional(),
invocation_id: z
.string()
Expand All @@ -89,7 +92,7 @@ export function registerAppCapabilities(server: McpServer) {
{
title: "Manage Kernel apps and invocations",
readOnlyHint: false,
destructiveHint: false,
destructiveHint: true,
idempotentHint: false,
openWorldHint: true,
},
Expand All @@ -103,6 +106,7 @@ export function registerAppCapabilities(server: McpServer) {
const page = await client.apps.list({
...(params.app_name && { app_name: params.app_name }),
...(params.version && { version: params.version }),
...(params.query && { query: params.query }),
...(params.limit !== undefined && { limit: params.limit }),
...(params.offset !== undefined && { offset: params.offset }),
});
Expand Down Expand Up @@ -164,13 +168,30 @@ export function registerAppCapabilities(server: McpServer) {
return jsonResponse(deployment);
}
case "list_deployments": {
if (params.version && !params.app_name) {
return errorResponse(
"Error: app_name is required when filtering deployments by version.",
);
}
const page = await client.deployments.list({
...(params.app_name && { app_name: params.app_name }),
...(params.version && { app_version: params.version }),
...(params.limit !== undefined && { limit: params.limit }),
...(params.offset !== undefined && { offset: params.offset }),
});
return paginatedJsonResponse(page);
}
case "delete_deployment": {
if (!params.deployment_id) {
return errorResponse(
"Error: deployment_id is required for delete_deployment.",
);
}
await client.deployments.delete(params.deployment_id);
return textResponse(
`Deployment "${params.deployment_id}" deleted successfully.`,
);
}
case "get_invocation": {
if (!params.invocation_id)
return errorResponse("Error: invocation_id is required.");
Expand Down
51 changes: 17 additions & 34 deletions src/lib/mcp/tools/extensions.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
import { createKernelClient } from "@/lib/mcp/kernel-client";
import {
errorResponse,
itemsJsonResponse,
textResponse,
toolErrorResponse,
} from "@/lib/mcp/responses";

export function registerExtensionTools(server: McpServer) {
// manage_extensions -- List and delete browser extensions
server.tool(
"manage_extensions",
'Manage browser extensions uploaded to your organization. Use "list" to see all extensions or "delete" to remove one.',
'Manage browser extensions uploaded to Kernel. Use "list" to see all extensions available to the current project or "delete" to remove one by ID or name.',
{
action: z.enum(["list", "delete"]).describe("Operation to perform."),
id_or_name: z
Expand All @@ -29,45 +35,22 @@ export function registerExtensionTools(server: McpServer) {
switch (params.action) {
case "list": {
const extensions = await client.extensions.list();
return {
content: [
{
type: "text",
text:
extensions?.length > 0
? JSON.stringify(extensions, null, 2)
: "No extensions found",
},
],
};
return itemsJsonResponse(extensions ?? [], {
has_more: false,
next_offset: null,
emptyText: "No extensions found",
});
}
case "delete": {
if (!params.id_or_name)
return {
content: [
{
type: "text",
text: "Error: id_or_name is required for delete.",
},
],
};
if (!params.id_or_name) {
return errorResponse("Error: id_or_name is required for delete.");
}
await client.extensions.delete(params.id_or_name);
return {
content: [
{ type: "text", text: "Extension deleted successfully" },
],
};
return textResponse("Extension deleted successfully");
}
}
} catch (error) {
return {
content: [
{
type: "text",
text: `Error in manage_extensions (${params.action}): ${error}`,
},
],
};
return toolErrorResponse("manage_extensions", params.action, error);
}
},
);
Expand Down
55 changes: 44 additions & 11 deletions src/lib/mcp/tools/profiles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { registerJsonResourceTemplate } from "@/lib/mcp/resource-templates";
import {
errorResponse,
itemsJsonResponse,
jsonResponse,
paginatedJsonResponse,
textResponse,
toolErrorResponse,
Expand All @@ -24,12 +25,15 @@ async function listProfiles(client: KernelClient, query?: ProfileListParams) {
return profiles;
}

function fullProfileListResponse(profiles: Profile[]) {
function fullProfileListResponse(profiles: Profile[], query?: string) {
return itemsJsonResponse(profiles, {
has_more: false,
next_offset: null,
emptyText:
"No profiles found. Use manage_profiles with action 'setup' to create one.",
// A search that matches nothing shouldn't claim the inventory is empty or
// suggest setup — other profiles may exist that just don't match the query.
emptyText: query
? `No profiles match "${query}".`
: "No profiles found. Use manage_profiles with action 'setup' to create one.",
});
}

Expand Down Expand Up @@ -65,20 +69,18 @@ export function registerProfileCapabilities(server: McpServer) {

server.tool(
"manage_profiles",
'Manage browser profiles when an agent needs persistent cookies, login state, or reusable browser state. Use "setup" for a guided login session, "list" to find a profile, and "delete" only when a profile should be removed.',
'Manage browser profiles when an agent needs persistent cookies, login state, or reusable browser state. Use "setup" for a guided login session, "list" to find a profile, "get" to retrieve one, and "delete" only when a profile should be removed.',
{
action: z
.enum(["setup", "list", "delete"])
.enum(["setup", "list", "get", "delete"])
.describe("Operation to perform."),
profile_name: z
.string()
.describe(
"(setup, delete) Profile name. For setup: 1-255 chars. For delete: name of profile to remove.",
)
.describe("(setup, get, delete) Profile name. For setup: 1-255 chars.")
.optional(),
profile_id: z
.string()
.describe("(delete) Profile ID to delete. Alternative to profile_name.")
.describe("(get, delete) Profile ID. Alternative to profile_name.")
.optional(),
update_existing: z
.boolean()
Expand Down Expand Up @@ -108,6 +110,9 @@ export function registerProfileCapabilities(server: McpServer) {
return errorResponse(
"Error: profile_name is required for setup.",
);
// Scan all profiles for an exact name match: the list `query` is a
// search and may not reliably return an exact-named profile, which
// would let setup create a duplicate.
const existingProfiles = await listProfiles(client);
const existingProfile = existingProfiles?.find(
(p) => p.name === params.profile_name,
Expand Down Expand Up @@ -153,15 +158,43 @@ export function registerProfileCapabilities(server: McpServer) {
client,
params.query ? { query: params.query } : undefined,
);
return fullProfileListResponse(profiles);
return fullProfileListResponse(profiles, params.query);
Comment thread
cursor[bot] marked this conversation as resolved.
}

const page = await client.profiles.list({
...(params.query && { query: params.query }),
...(params.limit !== undefined && { limit: params.limit }),
...(params.offset !== undefined && { offset: params.offset }),
} satisfies ProfileListParams);
return paginatedJsonResponse(page);
// On the first page of a search with no results, note the empty
// match so agents can tell a failed search from an empty org. Skip
// it past offset 0, where an empty page may just be beyond the
// matches rather than a true miss.
const emptySearch =
params.query &&
!params.offset &&
page.getPaginatedItems().length === 0;
return paginatedJsonResponse(
page,
emptySearch
? { note: `No profiles match "${params.query}".` }
: {},
);
}
case "get": {
Comment thread
cursor[bot] marked this conversation as resolved.
if (params.profile_name && params.profile_id) {
return errorResponse(
"Error: Cannot specify both profile_name and profile_id.",
);
}
const identifier = params.profile_name || params.profile_id;
if (!identifier) {
return errorResponse(
"Error: profile_name or profile_id is required for get.",
);
}
const profile = await client.profiles.retrieve(identifier);
return jsonResponse(profile);
}
case "delete": {
if (params.profile_name && params.profile_id) {
Expand Down
Loading
Loading