Skip to content
Merged
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
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -255,7 +255,7 @@ Many other MCP-capable tools accept:

Configure these values wherever the tool expects MCP server settings.

## Tools (12 total)
## Tools (15 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.

Expand All @@ -271,6 +271,9 @@ Self-hosted deployments can hide sensitive tool families by setting `KERNEL_MCP_
- `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_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.
Comment thread
masnwilliams marked this conversation as resolved.

### Standalone tools

Expand Down
6 changes: 6 additions & 0 deletions src/lib/mcp/register.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,13 @@ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { registerKernelPrompts } from "@/lib/mcp/prompts";
import { registerAPIKeyCapabilities } from "@/lib/mcp/tools/api-keys";
import { registerAppCapabilities } from "@/lib/mcp/tools/apps";
import { registerAuthConnectionTools } from "@/lib/mcp/tools/auth-connections";
import { registerBrowserPoolCapabilities } from "@/lib/mcp/tools/browser-pools";
import { registerBrowserCurlTool } from "@/lib/mcp/tools/browser-curl";
import { registerBrowserCapabilities } from "@/lib/mcp/tools/browsers";
import { registerComputerActionTool } from "@/lib/mcp/tools/computer-action";
import { registerCredentialProviderTools } from "@/lib/mcp/tools/credential-providers";
import { registerCredentialTools } from "@/lib/mcp/tools/credentials";
import { registerDocsTools } from "@/lib/mcp/tools/docs";
import { registerExtensionTools } from "@/lib/mcp/tools/extensions";
import { registerPlaywrightTool } from "@/lib/mcp/tools/playwright";
Expand All @@ -30,6 +33,9 @@ const mcpToolRegistrations = [
["computer", registerComputerActionTool],
["shell", registerShellTool],
["playwright", registerPlaywrightTool],
["auth_connections", registerAuthConnectionTools],
["credentials", registerCredentialTools],
["credential_providers", registerCredentialProviderTools],
] as const satisfies readonly (readonly [string, RegisterMcpToolset])[];

type McpToolset = (typeof mcpToolRegistrations)[number][0];
Expand Down
266 changes: 266 additions & 0 deletions src/lib/mcp/tools/auth-connections.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,266 @@
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
import { createKernelClient } from "@/lib/mcp/kernel-client";
import {
errorResponse,
jsonResponse,
paginatedJsonResponse,
textResponse,
toolErrorResponse,
} from "@/lib/mcp/responses";
import { paginationParams } from "@/lib/mcp/schemas";

export function registerAuthConnectionTools(server: McpServer) {
// manage_auth_connections -- Manage Kernel managed auth connections
server.tool(
"manage_auth_connections",
'Manage Kernel managed auth connections for keeping a profile logged into a third-party site. Use "create" to start managing auth for a profile + domain (optionally referencing a stored credential), "login" to begin a login flow (returns a hosted_url to share with the user, plus live_view_url to watch), "submit" to provide field values or pick an MFA option when a flow is awaiting input, "get" to poll flow state, "list" to see connections, or "delete" to remove one.',
{
action: z
.enum(["create", "list", "get", "delete", "login", "submit"])
.describe("Operation to perform."),
id: z
.string()
.describe(
"Auth connection ID. Required for get, delete, login, submit.",
)
.optional(),
domain: z
.string()
.describe("(create) Target domain (e.g. 'netflix.com').")
.optional(),
profile_name: z
.string()
.describe(
"(create) Profile to manage auth for. (list) Filter by profile_name.",
)
.optional(),
allowed_domains: z
.array(z.string())
.describe(
"(create) Additional domains valid for this auth flow. Common SSO providers (Google, Microsoft, Okta, Auth0, Apple, GitHub, Facebook, LinkedIn, Cognito, OneLogin, Ping) are allowed by default.",
)
.optional(),
credential_name: z
.string()
.describe(
"(create) Name of a pre-stored Kernel credential to use for automatic login.",
)
.optional(),
credential_provider: z
.string()
.describe(
"(create) External credential provider name (e.g. '1password'). Use with credential_path or credential_auto.",
)
.optional(),
credential_path: z
.string()
.describe(
"(create) Provider-specific item path (e.g. 'VaultName/ItemName').",
)
.optional(),
credential_auto: z
.boolean()
.describe(
"(create) If true, the provider auto-looks up credentials by domain.",
)
.optional(),
login_url: z
.string()
.describe(
"(create) Optional explicit login page URL to skip discovery.",
)
.optional(),
health_check_interval: z
.number()
.int()
.describe(
"(create) Seconds between automatic re-auth checks. Plan-dependent minimum, max 86400.",
)
Comment thread
cursor[bot] marked this conversation as resolved.
.optional(),
save_credentials: z
.boolean()
.describe(
"(create) Save credentials after each successful login. Default true.",
)
.optional(),
proxy_id: z
.string()
.describe("(create, login) Proxy ID to route the auth flow through.")
.optional(),
proxy_name: z
.string()
.describe("(create, login) Proxy name to route the auth flow through.")
.optional(),
domain_filter: z.string().describe("(list) Filter by domain.").optional(),
...paginationParams,
fields: z
.record(z.string(), z.string())
.describe(
"(submit) Map of field name to value (e.g. { mfa_code: '123456' }). Look at discovered_fields from `get` to know what to provide.",
)
.optional(),
mfa_option_id: z
.string()
.describe(
"(submit) ID of the MFA option to use, from mfa_options on the connection.",
)
.optional(),
sso_button_selector: z
.string()
.describe(
"(submit) XPath of an SSO button to click instead of submitting fields.",
)
.optional(),
},
{
title: "Manage Kernel managed auth connections",
readOnlyHint: false,
destructiveHint: true,
idempotentHint: false,
openWorldHint: true,
},
async (params, extra) => {
if (!extra.authInfo) throw new Error("Authentication required");
const client = createKernelClient(extra.authInfo.token);

const buildProxy = () =>
params.proxy_id || params.proxy_name
? {
...(params.proxy_id && { id: params.proxy_id }),
...(params.proxy_name && { name: params.proxy_name }),
}
: undefined;

try {
switch (params.action) {
case "create": {
if (!params.domain || !params.profile_name) {
return errorResponse(
"Error: domain and profile_name are required for create.",
);
}
const hasName = !!params.credential_name;
const hasProvider = !!params.credential_provider;
const hasPath = !!params.credential_path;
const autoTrue = params.credential_auto === true;
if (hasName && (hasProvider || hasPath || autoTrue)) {
return errorResponse(
"Error: credential_name cannot be combined with credential_provider, credential_path, or credential_auto. Use one of: { credential_name } for Kernel credentials, { credential_provider, credential_path } for an external provider item, or { credential_provider, credential_auto: true } for provider domain lookup.",
);
}
if ((hasPath || autoTrue) && !hasProvider) {
return errorResponse(
"Error: credential_path and credential_auto require credential_provider.",
);
}
if (hasPath && autoTrue) {
return errorResponse(
"Error: credential_path and credential_auto: true are alternatives — provide exactly one.",
);
}
if (hasProvider && !hasPath && !autoTrue) {
return errorResponse(
"Error: credential_provider requires either credential_path or credential_auto: true.",
);
}
const credential =
hasName || hasProvider
? {
...(hasName && { name: params.credential_name }),
...(hasProvider && {
provider: params.credential_provider,
}),
...(hasPath && { path: params.credential_path }),
...(autoTrue && { auto: true }),
}
: undefined;
const proxy = buildProxy();
const connection = await client.auth.connections.create({
domain: params.domain,
profile_name: params.profile_name,
...(params.allowed_domains && {
allowed_domains: params.allowed_domains,
}),
...(credential && { credential }),
...(params.login_url && { login_url: params.login_url }),
...(params.health_check_interval !== undefined && {
health_check_interval: params.health_check_interval,
}),
...(params.save_credentials !== undefined && {
save_credentials: params.save_credentials,
}),
...(proxy && { proxy }),
});
if (!connection)
return errorResponse("Failed to create auth connection");
return jsonResponse(connection);
}
case "list": {
const page = await client.auth.connections.list({
...(params.profile_name && { profile_name: params.profile_name }),
...(params.domain_filter && { domain: params.domain_filter }),
...(params.limit !== undefined && { limit: params.limit }),
...(params.offset !== undefined && { offset: params.offset }),
});
return paginatedJsonResponse(page);
}
case "get": {
if (!params.id)
return errorResponse("Error: id is required for get.");
const connection = await client.auth.connections.retrieve(
params.id,
);
return jsonResponse(connection);
}
case "delete": {
if (!params.id)
return errorResponse("Error: id is required for delete.");
await client.auth.connections.delete(params.id);
return textResponse("Auth connection deleted successfully");
}
case "login": {
if (!params.id)
return errorResponse("Error: id is required for login.");
const proxy = buildProxy();
const response = await client.auth.connections.login(
params.id,
proxy ? { proxy } : undefined,
);
return jsonResponse(response);
}
case "submit": {
if (!params.id)
return errorResponse("Error: id is required for submit.");
const hasFields =
!!params.fields && Object.keys(params.fields).length > 0;
if (
!hasFields &&
!params.mfa_option_id &&
!params.sso_button_selector
)
return errorResponse(
"Error: submit requires at least one of fields (non-empty), mfa_option_id, or sso_button_selector.",
);
const response = await client.auth.connections.submit(params.id, {
...(hasFields && { fields: params.fields }),
...(params.mfa_option_id && {
mfa_option_id: params.mfa_option_id,
}),
...(params.sso_button_selector && {
sso_button_selector: params.sso_button_selector,
}),
});
return jsonResponse(response);
}
}
} catch (error) {
return toolErrorResponse(
"manage_auth_connections",
params.action,
error,
);
}
},
);
}
Loading
Loading