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

Configure these values wherever the tool expects MCP server settings.

## Tools (20 model-facing, plus 1 app-only helper)
## Tools (21 model-facing, plus 1 app-only helper)

Each Kernel feature has a single `manage_*` tool with an `action` parameter, keeping the tool set small and consistent. Standalone tools handle high-frequency and interactive workflows.

Expand All @@ -316,6 +316,7 @@ Call `get_connection_context` before deciding whether to create or select a proj
- `manage_auth_connections` - Create, list, get, update, delete, login, submit, inspect timelines, and wait for managed-auth connections in every client. Supports health-check and automatic re-auth settings, managed-auth browser configuration, and canonical interaction-bound field/choice submissions. Use domain-filtered `list` for discovery. App-capable clients additionally receive `open_auth_login`; the programmatic actions remain available there too.
- `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.
- `manage_vaults` - Create/select project-owned vaults, manage Link and AgentCard wallet/card items, perform advertised authorization operations, and inspect state and events. Attach vaults using `manage_browsers.vaults` at browser creation. These tools prepare and observe credentials; they do not submit merchant payments.

### Standalone tools

Expand Down Expand Up @@ -383,6 +384,20 @@ Example: “Log me into my Hacker News account and update my profile to add a ra

The secure App defaults `record_session` and `browser_telemetry.enabled` to `true`, recording replay video plus the operational telemetry categories (`control`, `connection`, `system`, and `captcha`) for managed-auth browser sessions. Callers can explicitly disable either setting. The programmatic `manage_auth_connections` create, update, and login actions pass browser telemetry through the API’s current nested `browser.telemetry` configuration while preserving defaults and inheritance when the MCP parameter is omitted.

### Prepare payment credentials with a vault

This surface uses a [pinned, vendored SDK preview](vendor/README.md). It requires an API deployment with the corresponding vault support enabled; installing the MCP server does not enable or deploy that API.

1. Call `get_connection_context` and select the owning project. Use `manage_vaults` with `action: "list"`, `"get"`, or `"upsert"` (`name` required for upsert) to select/create a vault. Omitted project selection uses the fixed connection project or the API default project, including for list. Ownership and the vault name are immutable.
2. Use `action: "upsert_item"`, `id_or_name`, `key`, and an `item` containing exactly `type` and `spec`. For a Link wallet, use `{"type":"wallet","spec":{"provider":"link","authorization":{"method":"oauth","client":{"type":"kernel_managed"}}}}`. Follow the returned OAuth action URL until connected. For AgentCard, use `{"type":"wallet","spec":{"provider":"agentcard"}}` and follow `card_enrollment`. An optional AgentCard `user_id` must already be enrolled by a wallet in this organization. Never send card data or OAuth tokens/codes to MCP.
3. Read the wallet with `action: "get_item"`. When advertised in `available_expansions`, pass `expand: ["payment_methods"]` to obtain display-safe funding methods. Create a card item with its provider's complete spec: Link requires `wallet`, `payment_method_id`, `merchant_name`, `merchant_url`, `amount` in minor currency units, `currency`, at least 100 characters of purchase `context`, and explicit `test: true` or `false`. AgentCard requires `wallet`, `merchant`, `amount`, and `currency`, with optional `card_id`; sandbox/live mode is deployment-configured, not an item flag. Confirm the intended mode before checkout. Permitted Link domains are returned in `state.domains`; this preview has no writable domains field.
4. For Link, read `available_operations` and their descriptions, then call `action: "perform_item_operation"` with `operation: "authorize"` only when advertised. Creating or updating a Link card does not authorize it. Follow the returned approval/collection actions on provider-hosted surfaces; if no usable surface is supplied, request user assistance rather than inventing a callback. `update_item` accepts a complete card `spec`: Link only while requested, AgentCard before/between authorizations when permitted. AgentCard checkout submission triggers its own approval-gated authorization; there is no separate AgentCard authorize operation here.
5. Create a browser in the same project with `manage_browsers`, `action: "create"`, and `vaults: [{"name":"checkout"}]` (or an ID instead of a name). Up to 20 references are supported, each with exactly one identifier and no duplicates; attachments cannot change after creation. Use returned non-secret `state.aliases` in checkout. Observe outcomes using `get_item` and `item_events`; for events, continue with `after` set to the last returned event ID. Both reads support `wait` from 0 to 60 seconds; configure the MCP client timeout above `wait + 30` seconds.

**Payment safety:** vault API calls do not submit a merchant payment or authorize arbitrary retries. Never retry a failed, timed-out, rejected, or indeterminate payment, including by creating another item/browser. `ready`, `consumed`, or an `approved` authorization alone does not prove a successful charge. Inspect typed item state, authorization outcomes, and event names before deciding what happened. Never request raw card values, OAuth tokens/codes, ciphertext, provider secrets, or sensitive provider responses.

MCP returns allowlisted metadata, actions, aliases, funding-method displays, and typed outcomes. It omits arbitrary spec metadata, event payloads, unstructured authorization reasons, unknown fields, and raw API error details. All vault SDK calls and vault-attached browser creation disable automatic retries. Deleting a vault invalidates its items; deleting a wallet also invalidates dependent cards. There is no rename, project move, callback, raw-secret, or payment-retry tool.

### Set up browser profiles for authentication

```
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 @@ -40,7 +40,7 @@
"@clerk/themes": "^2.4.19",
"@modelcontextprotocol/sdk": "1.26.0",
"@onkernel/managed-auth-react": "0.5.1",
"@onkernel/sdk": "^0.98.0",
"@onkernel/sdk": "file:vendor/kernel-sdk-768cea122220150aacc33074ea00a1c0afaf879e.tgz",
"@posthog/mcp": "0.10.1",
"@types/jsonwebtoken": "^9.0.10",
"@types/redis": "^4.0.11",
Expand Down
26 changes: 26 additions & 0 deletions src/lib/mcp/register.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ const NON_AUTH_TOOLSETS = [
"replays",
"credentials",
"credential_providers",
"vaults",
].join(",");

function captureRegistration(mcpApps: boolean) {
Expand Down Expand Up @@ -115,6 +116,30 @@ describe("MCP toolset allowlist", () => {
});
});

test("enables and disables vaults through the existing toolset selectors", () => {
const previousEnabled = process.env.KERNEL_MCP_ENABLED_TOOLSETS;
const previousDisabled = process.env.KERNEL_MCP_DISABLED_TOOLSETS;
process.env.KERNEL_MCP_ENABLED_TOOLSETS = "manage_vaults";
delete process.env.KERNEL_MCP_DISABLED_TOOLSETS;
try {
expect(captureRegistration(false).legacyTools).toEqual([
"get_connection_context",
"manage_vaults",
]);
process.env.KERNEL_MCP_DISABLED_TOOLSETS = "vaults";
expect(captureRegistration(false).legacyTools).toEqual([
"get_connection_context",
]);
} finally {
if (previousEnabled === undefined)
delete process.env.KERNEL_MCP_ENABLED_TOOLSETS;
else process.env.KERNEL_MCP_ENABLED_TOOLSETS = previousEnabled;
if (previousDisabled === undefined)
delete process.env.KERNEL_MCP_DISABLED_TOOLSETS;
else process.env.KERNEL_MCP_DISABLED_TOOLSETS = previousDisabled;
}
});

describe("project selection registration", () => {
const projectScopedTools = [
"manage_profiles",
Expand All @@ -130,6 +155,7 @@ describe("project selection registration", () => {
"manage_replays",
"manage_auth_connections",
"manage_credentials",
"manage_vaults",
"open_auth_login",
"begin_auth_login",
];
Expand Down
2 changes: 2 additions & 0 deletions src/lib/mcp/register.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { registerProxyTools } from "@/lib/mcp/tools/proxies";
import { registerReplayTools } from "@/lib/mcp/tools/replays";
import { registerShellTool } from "@/lib/mcp/tools/shell";
import { registerWebMcpTool } from "@/lib/mcp/tools/webmcp";
import { registerVaultTools } from "@/lib/mcp/tools/vaults";
type McpToolOptions = McpDependencies;
type McpRegistrationOptions = {
mcpApps?: boolean;
Expand Down Expand Up @@ -54,6 +55,7 @@ const mcpToolRegistrations = [
["auth_connections", registerManagedAuthCapabilities],
["credentials", registerCredentialTools],
["credential_providers", registerCredentialProviderTools],
["vaults", registerVaultTools],
] as const satisfies readonly (readonly [string, RegisterMcpToolset])[];

type McpToolset = (typeof mcpToolRegistrations)[number][0];
Expand Down
19 changes: 19 additions & 0 deletions src/lib/mcp/responses.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,25 @@ describe("throwToolError classification", () => {
).toContain("[code: last_active_project]");
});

test("replaces sensitive error text without losing API classification", () => {
try {
throwToolError(
"manage_vaults",
"upsert_item",
codedApiError(409, "secret-code", "secret-provider-response"),
"State conflict. Read the item; do not retry the payment.",
);
} catch (error) {
expect(error).toBeInstanceOf(Error);
expect((error as Error).name).toBe("KernelApiError409");
expect((error as Error).message).toBe(
"Error in manage_vaults (upsert_item): State conflict. Read the item; do not retry the payment.",
);
return;
}
throw new Error("throwToolError did not throw");
});

test("ignores absent and non-string API codes", () => {
const absent = apiError(409, "conflict");
expect(caught(absent).message).not.toContain("[code:");
Expand Down
3 changes: 2 additions & 1 deletion src/lib/mcp/responses.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,10 +123,11 @@ export function throwToolError(
toolName: string,
action: string,
error: unknown,
safeMessage?: string,
): never {
throw new ToolCallError(
errorName(error),
`Error in ${toolName} (${action}): ${errorMessage(error)}`,
`Error in ${toolName} (${action}): ${safeMessage ?? errorMessage(error)}`,
);
}

Expand Down
131 changes: 131 additions & 0 deletions src/lib/mcp/tools/browser-vaults.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
/// <reference types="bun-types" />

import Kernel from "@onkernel/sdk";
import { expect, test } from "bun:test";
import { connectTestMcp, toolResultJSON } from "@/lib/mcp/mcp-test-fixtures";
import { registerBrowserCapabilities } from "@/lib/mcp/tools/browsers";

test("attaches vault references only at creation and disables automatic retries", async () => {
const calls: unknown[] = [];
const vaults = [{ id: "vault_1" }, { name: "checkout" }];
const { client, close } = await connectTestMcp(registerBrowserCapabilities, {
browsers: {
create: async (...args: unknown[]) => {
calls.push(args);
return { session_id: "browser_1", vaults };
},
update: async () => {
throw new Error("must not change vault bindings");
},
},
});
try {
const result = toolResultJSON(
await client.callTool({
name: "manage_browsers",
arguments: { action: "create", vaults, headless: false },
}),
);
expect(result.browser.vaults).toEqual(vaults);
expect(calls).toEqual([
[
{ vaults, headless: false },
{ maxRetries: 0, signal: expect.any(AbortSignal) },
],
]);
const update = await client.callTool({
name: "manage_browsers",
arguments: {
action: "update",
session_id: "browser_1",
vaults: [],
name: "renamed",
},
});
expect(update.isError).toBeTrue();
expect(update.content).toEqual([
{
type: "text",
text: "Error: vaults is create-only; browser vault bindings are immutable.",
},
]);
} finally {
await close();
}
});

test("rejects ambiguous, empty, oversized, and secret-bearing vault references", async () => {
let creates = 0;
const { client, close } = await connectTestMcp(registerBrowserCapabilities, {
browsers: {
create: async () => {
creates++;
return { session_id: "browser_1" };
},
},
});
try {
for (const vaults of [
[{}],
[{ id: "vault_1", name: "checkout" }],
[{ id: "" }],
[{ name: "" }],
[{ id: "vault_1", secret: "sensitive-value" }],
Array(21).fill({ name: "checkout" }),
]) {
expect(
(
await client.callTool({
name: "manage_browsers",
arguments: { action: "create", vaults },
})
).isError,
).toBeTrue();
}
expect(creates).toBe(0);
} finally {
await close();
}
});

test("forwards browser vaults through the real preview SDK and withholds provider errors", async () => {
const requests: unknown[] = [];
const sdk = new Kernel({
apiKey: "test-key",
baseURL: "https://api.example.test",
fetch: async (input, init) => {
const request = new Request(input, init);
requests.push({
method: request.method,
path: new URL(request.url).pathname,
body: await request.json(),
});
return Response.json(
{ message: "sensitive-value", provider_secret: "sensitive-value" },
{ status: 500 },
);
},
});
const { client, close } = await connectTestMcp(
registerBrowserCapabilities,
sdk,
);
try {
const result = await client.callTool({
name: "manage_browsers",
arguments: { action: "create", vaults: [{ name: "checkout" }] },
});
expect(requests).toEqual([
{
method: "POST",
path: "/browsers",
body: { vaults: [{ name: "checkout" }] },
},
]);
expect(result.isError).toBeTrue();
expect(JSON.stringify(result)).not.toContain("sensitive-value");
expect(JSON.stringify(result)).toContain("Do not retry a payment");
} finally {
await close();
}
});
Loading
Loading