Skip to content

Commit bca50d3

Browse files
authored
Merge pull request #104 from kernel/hypeship/add-managed-auth-tools
add managed auth tools (manage_auth_connections, manage_credentials, manage_credential_providers)
2 parents 144da94 + 394ea34 commit bca50d3

5 files changed

Lines changed: 603 additions & 1 deletion

File tree

README.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -255,7 +255,7 @@ Many other MCP-capable tools accept:
255255

256256
Configure these values wherever the tool expects MCP server settings.
257257

258-
## Tools (12 total)
258+
## Tools (15 total)
259259

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

@@ -271,6 +271,9 @@ Self-hosted deployments can hide sensitive tool families by setting `KERNEL_MCP_
271271
- `manage_apps` - List apps, invoke actions, get/list deployments, and get invocation results.
272272
- `manage_projects` - Create, list, get, update, and delete organization projects.
273273
- `manage_api_keys` - Create, list, get, update, and delete Kernel API keys. Create returns the plaintext key once.
274+
- `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.
275+
- `manage_credentials` - Create, list, get, update, and delete stored credentials; fetch a current TOTP code for credentials with a configured totp_secret.
276+
- `manage_credential_providers` - Create, list, get, update, and delete external credential providers (e.g. 1Password); list available items and test the provider connection.
274277

275278
### Standalone tools
276279

src/lib/mcp/register.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,13 @@ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
22
import { registerKernelPrompts } from "@/lib/mcp/prompts";
33
import { registerAPIKeyCapabilities } from "@/lib/mcp/tools/api-keys";
44
import { registerAppCapabilities } from "@/lib/mcp/tools/apps";
5+
import { registerAuthConnectionTools } from "@/lib/mcp/tools/auth-connections";
56
import { registerBrowserPoolCapabilities } from "@/lib/mcp/tools/browser-pools";
67
import { registerBrowserCurlTool } from "@/lib/mcp/tools/browser-curl";
78
import { registerBrowserCapabilities } from "@/lib/mcp/tools/browsers";
89
import { registerComputerActionTool } from "@/lib/mcp/tools/computer-action";
10+
import { registerCredentialProviderTools } from "@/lib/mcp/tools/credential-providers";
11+
import { registerCredentialTools } from "@/lib/mcp/tools/credentials";
912
import { registerDocsTools } from "@/lib/mcp/tools/docs";
1013
import { registerExtensionTools } from "@/lib/mcp/tools/extensions";
1114
import { registerPlaywrightTool } from "@/lib/mcp/tools/playwright";
@@ -30,6 +33,9 @@ const mcpToolRegistrations = [
3033
["computer", registerComputerActionTool],
3134
["shell", registerShellTool],
3235
["playwright", registerPlaywrightTool],
36+
["auth_connections", registerAuthConnectionTools],
37+
["credentials", registerCredentialTools],
38+
["credential_providers", registerCredentialProviderTools],
3339
] as const satisfies readonly (readonly [string, RegisterMcpToolset])[];
3440

3541
type McpToolset = (typeof mcpToolRegistrations)[number][0];
Lines changed: 266 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,266 @@
1+
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2+
import { z } from "zod";
3+
import { createKernelClient } from "@/lib/mcp/kernel-client";
4+
import {
5+
errorResponse,
6+
jsonResponse,
7+
paginatedJsonResponse,
8+
textResponse,
9+
toolErrorResponse,
10+
} from "@/lib/mcp/responses";
11+
import { paginationParams } from "@/lib/mcp/schemas";
12+
13+
export function registerAuthConnectionTools(server: McpServer) {
14+
// manage_auth_connections -- Manage Kernel managed auth connections
15+
server.tool(
16+
"manage_auth_connections",
17+
'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.',
18+
{
19+
action: z
20+
.enum(["create", "list", "get", "delete", "login", "submit"])
21+
.describe("Operation to perform."),
22+
id: z
23+
.string()
24+
.describe(
25+
"Auth connection ID. Required for get, delete, login, submit.",
26+
)
27+
.optional(),
28+
domain: z
29+
.string()
30+
.describe("(create) Target domain (e.g. 'netflix.com').")
31+
.optional(),
32+
profile_name: z
33+
.string()
34+
.describe(
35+
"(create) Profile to manage auth for. (list) Filter by profile_name.",
36+
)
37+
.optional(),
38+
allowed_domains: z
39+
.array(z.string())
40+
.describe(
41+
"(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.",
42+
)
43+
.optional(),
44+
credential_name: z
45+
.string()
46+
.describe(
47+
"(create) Name of a pre-stored Kernel credential to use for automatic login.",
48+
)
49+
.optional(),
50+
credential_provider: z
51+
.string()
52+
.describe(
53+
"(create) External credential provider name (e.g. '1password'). Use with credential_path or credential_auto.",
54+
)
55+
.optional(),
56+
credential_path: z
57+
.string()
58+
.describe(
59+
"(create) Provider-specific item path (e.g. 'VaultName/ItemName').",
60+
)
61+
.optional(),
62+
credential_auto: z
63+
.boolean()
64+
.describe(
65+
"(create) If true, the provider auto-looks up credentials by domain.",
66+
)
67+
.optional(),
68+
login_url: z
69+
.string()
70+
.describe(
71+
"(create) Optional explicit login page URL to skip discovery.",
72+
)
73+
.optional(),
74+
health_check_interval: z
75+
.number()
76+
.int()
77+
.describe(
78+
"(create) Seconds between automatic re-auth checks. Plan-dependent minimum, max 86400.",
79+
)
80+
.optional(),
81+
save_credentials: z
82+
.boolean()
83+
.describe(
84+
"(create) Save credentials after each successful login. Default true.",
85+
)
86+
.optional(),
87+
proxy_id: z
88+
.string()
89+
.describe("(create, login) Proxy ID to route the auth flow through.")
90+
.optional(),
91+
proxy_name: z
92+
.string()
93+
.describe("(create, login) Proxy name to route the auth flow through.")
94+
.optional(),
95+
domain_filter: z.string().describe("(list) Filter by domain.").optional(),
96+
...paginationParams,
97+
fields: z
98+
.record(z.string(), z.string())
99+
.describe(
100+
"(submit) Map of field name to value (e.g. { mfa_code: '123456' }). Look at discovered_fields from `get` to know what to provide.",
101+
)
102+
.optional(),
103+
mfa_option_id: z
104+
.string()
105+
.describe(
106+
"(submit) ID of the MFA option to use, from mfa_options on the connection.",
107+
)
108+
.optional(),
109+
sso_button_selector: z
110+
.string()
111+
.describe(
112+
"(submit) XPath of an SSO button to click instead of submitting fields.",
113+
)
114+
.optional(),
115+
},
116+
{
117+
title: "Manage Kernel managed auth connections",
118+
readOnlyHint: false,
119+
destructiveHint: true,
120+
idempotentHint: false,
121+
openWorldHint: true,
122+
},
123+
async (params, extra) => {
124+
if (!extra.authInfo) throw new Error("Authentication required");
125+
const client = createKernelClient(extra.authInfo.token);
126+
127+
const buildProxy = () =>
128+
params.proxy_id || params.proxy_name
129+
? {
130+
...(params.proxy_id && { id: params.proxy_id }),
131+
...(params.proxy_name && { name: params.proxy_name }),
132+
}
133+
: undefined;
134+
135+
try {
136+
switch (params.action) {
137+
case "create": {
138+
if (!params.domain || !params.profile_name) {
139+
return errorResponse(
140+
"Error: domain and profile_name are required for create.",
141+
);
142+
}
143+
const hasName = !!params.credential_name;
144+
const hasProvider = !!params.credential_provider;
145+
const hasPath = !!params.credential_path;
146+
const autoTrue = params.credential_auto === true;
147+
if (hasName && (hasProvider || hasPath || autoTrue)) {
148+
return errorResponse(
149+
"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.",
150+
);
151+
}
152+
if ((hasPath || autoTrue) && !hasProvider) {
153+
return errorResponse(
154+
"Error: credential_path and credential_auto require credential_provider.",
155+
);
156+
}
157+
if (hasPath && autoTrue) {
158+
return errorResponse(
159+
"Error: credential_path and credential_auto: true are alternatives — provide exactly one.",
160+
);
161+
}
162+
if (hasProvider && !hasPath && !autoTrue) {
163+
return errorResponse(
164+
"Error: credential_provider requires either credential_path or credential_auto: true.",
165+
);
166+
}
167+
const credential =
168+
hasName || hasProvider
169+
? {
170+
...(hasName && { name: params.credential_name }),
171+
...(hasProvider && {
172+
provider: params.credential_provider,
173+
}),
174+
...(hasPath && { path: params.credential_path }),
175+
...(autoTrue && { auto: true }),
176+
}
177+
: undefined;
178+
const proxy = buildProxy();
179+
const connection = await client.auth.connections.create({
180+
domain: params.domain,
181+
profile_name: params.profile_name,
182+
...(params.allowed_domains && {
183+
allowed_domains: params.allowed_domains,
184+
}),
185+
...(credential && { credential }),
186+
...(params.login_url && { login_url: params.login_url }),
187+
...(params.health_check_interval !== undefined && {
188+
health_check_interval: params.health_check_interval,
189+
}),
190+
...(params.save_credentials !== undefined && {
191+
save_credentials: params.save_credentials,
192+
}),
193+
...(proxy && { proxy }),
194+
});
195+
if (!connection)
196+
return errorResponse("Failed to create auth connection");
197+
return jsonResponse(connection);
198+
}
199+
case "list": {
200+
const page = await client.auth.connections.list({
201+
...(params.profile_name && { profile_name: params.profile_name }),
202+
...(params.domain_filter && { domain: params.domain_filter }),
203+
...(params.limit !== undefined && { limit: params.limit }),
204+
...(params.offset !== undefined && { offset: params.offset }),
205+
});
206+
return paginatedJsonResponse(page);
207+
}
208+
case "get": {
209+
if (!params.id)
210+
return errorResponse("Error: id is required for get.");
211+
const connection = await client.auth.connections.retrieve(
212+
params.id,
213+
);
214+
return jsonResponse(connection);
215+
}
216+
case "delete": {
217+
if (!params.id)
218+
return errorResponse("Error: id is required for delete.");
219+
await client.auth.connections.delete(params.id);
220+
return textResponse("Auth connection deleted successfully");
221+
}
222+
case "login": {
223+
if (!params.id)
224+
return errorResponse("Error: id is required for login.");
225+
const proxy = buildProxy();
226+
const response = await client.auth.connections.login(
227+
params.id,
228+
proxy ? { proxy } : undefined,
229+
);
230+
return jsonResponse(response);
231+
}
232+
case "submit": {
233+
if (!params.id)
234+
return errorResponse("Error: id is required for submit.");
235+
const hasFields =
236+
!!params.fields && Object.keys(params.fields).length > 0;
237+
if (
238+
!hasFields &&
239+
!params.mfa_option_id &&
240+
!params.sso_button_selector
241+
)
242+
return errorResponse(
243+
"Error: submit requires at least one of fields (non-empty), mfa_option_id, or sso_button_selector.",
244+
);
245+
const response = await client.auth.connections.submit(params.id, {
246+
...(hasFields && { fields: params.fields }),
247+
...(params.mfa_option_id && {
248+
mfa_option_id: params.mfa_option_id,
249+
}),
250+
...(params.sso_button_selector && {
251+
sso_button_selector: params.sso_button_selector,
252+
}),
253+
});
254+
return jsonResponse(response);
255+
}
256+
}
257+
} catch (error) {
258+
return toolErrorResponse(
259+
"manage_auth_connections",
260+
params.action,
261+
error,
262+
);
263+
}
264+
},
265+
);
266+
}

0 commit comments

Comments
 (0)