diff --git a/README.md b/README.md index e779e59..3102bff 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/src/lib/mcp/tools/apps.ts b/src/lib/mcp/tools/apps.ts index 3b3cbe6..7c9c011 100644 --- a/src/lib/mcp/tools/apps.ts +++ b/src/lib/mcp/tools/apps.ts @@ -46,7 +46,7 @@ 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([ @@ -54,6 +54,7 @@ export function registerAppCapabilities(server: McpServer) { "invoke", "get_deployment", "list_deployments", + "delete_deployment", "get_invocation", ]) .describe("Operation to perform."), @@ -66,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.") @@ -79,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.") .optional(), invocation_id: z .string() @@ -97,6 +99,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 }), }); @@ -158,13 +161,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."); diff --git a/src/lib/mcp/tools/extensions.ts b/src/lib/mcp/tools/extensions.ts index b7d5312..40fdb09 100644 --- a/src/lib/mcp/tools/extensions.ts +++ b/src/lib/mcp/tools/extensions.ts @@ -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 @@ -22,45 +28,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); } }, ); diff --git a/src/lib/mcp/tools/profiles.ts b/src/lib/mcp/tools/profiles.ts index 3ef18f4..dea42cb 100644 --- a/src/lib/mcp/tools/profiles.ts +++ b/src/lib/mcp/tools/profiles.ts @@ -5,6 +5,7 @@ import { registerJsonResourceTemplate } from "@/lib/mcp/resource-templates"; import { errorResponse, itemsJsonResponse, + jsonResponse, paginatedJsonResponse, textResponse, toolErrorResponse, @@ -65,20 +66,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() @@ -101,7 +100,9 @@ export function registerProfileCapabilities(server: McpServer) { return errorResponse( "Error: profile_name is required for setup.", ); - const existingProfiles = await listProfiles(client); + const existingProfiles = await listProfiles(client, { + query: params.profile_name, + }); const existingProfile = existingProfiles?.find( (p) => p.name === params.profile_name, ); @@ -156,6 +157,21 @@ export function registerProfileCapabilities(server: McpServer) { } satisfies ProfileListParams); return paginatedJsonResponse(page); } + case "get": { + 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) { return errorResponse( diff --git a/src/lib/mcp/tools/projects.ts b/src/lib/mcp/tools/projects.ts index f3a425b..24b96c4 100644 --- a/src/lib/mcp/tools/projects.ts +++ b/src/lib/mcp/tools/projects.ts @@ -11,17 +11,27 @@ import { import { paginationParams } from "@/lib/mcp/schemas"; export function registerProjectCapabilities(server: McpServer) { - // manage_projects -- Create, list, get, update, and delete organization projects + // manage_projects -- Create, list, get, update, delete, and manage organization project limits server.tool( "manage_projects", - 'Manage Kernel projects for resource isolation within an organization. Use "create" to create a project, "list" to discover projects, "get" to retrieve one, "update" to rename or archive one, or "delete" to remove an empty project.', + 'Manage Kernel projects for resource isolation within an organization. Use "create" to create a project, "list" to discover projects, "get" to retrieve one, "update" to rename or archive one, "delete" to remove an empty project, "get_limits" to inspect project caps, or "update_limits" to change project caps.', { action: z - .enum(["create", "list", "get", "update", "delete"]) + .enum([ + "create", + "list", + "get", + "update", + "delete", + "get_limits", + "update_limits", + ]) .describe("Operation to perform."), project_id: z .string() - .describe("Project ID. Required for get, update, and delete.") + .describe( + "Project ID. Required for get, update, delete, get_limits, and update_limits.", + ) .optional(), name: z.string().describe("(create, update) Project name.").optional(), status: z @@ -35,6 +45,33 @@ export function registerProjectCapabilities(server: McpServer) { ) .optional(), ...paginationParams, + max_concurrent_invocations: z + .number() + .int() + .min(0) + .nullable() + .describe( + "(update_limits) Maximum concurrent app invocations for this project. Set 0 to remove the cap.", + ) + .optional(), + max_concurrent_sessions: z + .number() + .int() + .min(0) + .nullable() + .describe( + "(update_limits) Maximum concurrent browser sessions for this project. Set 0 to remove the cap.", + ) + .optional(), + max_pooled_sessions: z + .number() + .int() + .min(0) + .nullable() + .describe( + "(update_limits) Maximum pooled sessions capacity for this project. Set 0 to remove the cap.", + ) + .optional(), }, async (params, extra) => { if (!extra.authInfo) throw new Error("Authentication required"); @@ -90,6 +127,48 @@ export function registerProjectCapabilities(server: McpServer) { await client.projects.delete(params.project_id); return textResponse("Project deleted successfully"); } + case "get_limits": { + if (!params.project_id) { + return errorResponse( + "Error: project_id is required for get_limits.", + ); + } + const limits = await client.projects.limits.retrieve( + params.project_id, + ); + return jsonResponse(limits); + } + case "update_limits": { + if (!params.project_id) { + return errorResponse( + "Error: project_id is required for update_limits.", + ); + } + const updateParams: Parameters< + typeof client.projects.limits.update + >[1] = {}; + if (params.max_concurrent_invocations !== undefined) { + updateParams.max_concurrent_invocations = + params.max_concurrent_invocations; + } + if (params.max_concurrent_sessions !== undefined) { + updateParams.max_concurrent_sessions = + params.max_concurrent_sessions; + } + if (params.max_pooled_sessions !== undefined) { + updateParams.max_pooled_sessions = params.max_pooled_sessions; + } + if (Object.keys(updateParams).length === 0) { + return errorResponse( + "Error: at least one limit field is required for update_limits.", + ); + } + const limits = await client.projects.limits.update( + params.project_id, + updateParams, + ); + return jsonResponse(limits); + } } } catch (error) { return toolErrorResponse("manage_projects", params.action, error); diff --git a/src/lib/mcp/tools/proxies.ts b/src/lib/mcp/tools/proxies.ts index c41c5e3..209d9c6 100644 --- a/src/lib/mcp/tools/proxies.ts +++ b/src/lib/mcp/tools/proxies.ts @@ -1,17 +1,47 @@ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { z } from "zod"; import { createKernelClient } from "@/lib/mcp/kernel-client"; +import { + errorResponse, + itemsJsonResponse, + jsonResponse, + textResponse, + toolErrorResponse, +} from "@/lib/mcp/responses"; + +const httpUrlSchema = z + .string() + .url() + .refine( + (value) => { + try { + const url = new URL(value); + return url.protocol === "http:" || url.protocol === "https:"; + } catch { + return false; + } + }, + { message: "URL must use http or https." }, + ); export function registerProxyTools(server: McpServer) { - // manage_proxies -- Create, list, and delete proxy configurations + // manage_proxies -- Create, list, get, check, and delete proxy configurations server.tool( "manage_proxies", - 'Manage proxy configurations for routing browser traffic. Use "create" to add a proxy, "list" to see all proxies, or "delete" to remove one. Proxy quality for bot detection avoidance, best to worst: mobile > residential > ISP > datacenter.', + 'Manage proxy configurations for routing browser traffic. Use "create" to add a proxy, "list" to see all proxies, "get" to retrieve one, "check" to test connectivity (optionally against a target URL), or "delete" to remove one. Proxy quality for bot detection avoidance, best to worst: mobile > residential > ISP > datacenter.', { action: z - .enum(["create", "list", "delete"]) + .enum(["create", "list", "get", "check", "delete"]) .describe("Operation to perform."), - proxy_id: z.string().describe("(delete) Proxy ID to delete.").optional(), + proxy_id: z + .string() + .describe("(get, check, delete) Proxy ID.") + .optional(), + check_url: httpUrlSchema + .describe( + "(check) Optional HTTP(S) URL to test through the proxy instead of Kernel's default check target.", + ) + .optional(), type: z .enum(["datacenter", "isp", "residential", "mobile", "custom"]) .describe("(create) Proxy type.") @@ -56,23 +86,14 @@ export function registerProxyTools(server: McpServer) { switch (params.action) { case "create": { if (!params.type) - return { - content: [ - { type: "text", text: "Error: type is required for create." }, - ], - }; + return errorResponse("Error: type is required for create."); if ( params.type === "custom" && (!params.custom_host || !params.custom_port) ) { - return { - content: [ - { - type: "text", - text: "Error: custom_host and custom_port are required for custom proxy type.", - }, - ], - }; + return errorResponse( + "Error: custom_host and custom_port are required for custom proxy type.", + ); } const createParams: Parameters[0] = params.type === "custom" @@ -102,53 +123,43 @@ export function registerProxyTools(server: McpServer) { }), }; const proxy = await client.proxies.create(createParams); - if (!proxy) - return { - content: [{ type: "text", text: "Failed to create proxy" }], - }; - return { - content: [{ type: "text", text: JSON.stringify(proxy, null, 2) }], - }; + if (!proxy) return errorResponse("Failed to create proxy"); + return jsonResponse(proxy); } case "list": { const proxies = await client.proxies.list(); - return { - content: [ - { - type: "text", - text: - proxies?.length > 0 - ? JSON.stringify(proxies, null, 2) - : "No proxies found", - }, - ], - }; + return itemsJsonResponse(proxies ?? [], { + has_more: false, + next_offset: null, + emptyText: "No proxies found", + }); + } + case "get": { + if (!params.proxy_id) { + return errorResponse("Error: proxy_id is required for get."); + } + const proxy = await client.proxies.retrieve(params.proxy_id); + return jsonResponse(proxy); + } + case "check": { + if (!params.proxy_id) { + return errorResponse("Error: proxy_id is required for check."); + } + const result = await client.proxies.check( + params.proxy_id, + params.check_url ? { url: params.check_url } : undefined, + ); + return jsonResponse(result); } case "delete": { if (!params.proxy_id) - return { - content: [ - { - type: "text", - text: "Error: proxy_id is required for delete.", - }, - ], - }; + return errorResponse("Error: proxy_id is required for delete."); await client.proxies.delete(params.proxy_id); - return { - content: [{ type: "text", text: "Proxy deleted successfully" }], - }; + return textResponse("Proxy deleted successfully"); } } } catch (error) { - return { - content: [ - { - type: "text", - text: `Error in manage_proxies (${params.action}): ${error}`, - }, - ], - }; + return toolErrorResponse("manage_proxies", params.action, error); } }, );