Skip to content

Commit 0be41fc

Browse files
committed
Expose vault payment tools through MCP
1 parent 9dcd166 commit 0be41fc

19 files changed

Lines changed: 2442 additions & 5 deletions

README.md

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

293293
Configure these values wherever the tool expects MCP server settings.
294294

295-
## Tools (20 model-facing, plus 1 app-only helper)
295+
## Tools
296296

297297
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.
298298

@@ -316,6 +316,12 @@ Call `get_connection_context` before deciding whether to create or select a proj
316316
- `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.
317317
- `manage_credentials` - Create, list, get, update, and delete stored credentials; fetch a current TOTP code for credentials with a configured totp_secret.
318318
- `manage_credential_providers` - Create, list, get, update, and delete external credential providers (e.g. 1Password); list available items and test the provider connection.
319+
- `manage_vaults` - Create, list, get, and delete project-owned payment vaults.
320+
- `manage_vault_wallets` - Connect Link or AgentCard wallets and inspect live payment methods.
321+
- `manage_vault_cards` - Create card requests or replace their full specification; does not implicitly authorize Link cards.
322+
- `manage_vault_items` - List, get, invoke advertised operations, observe events, and delete vault items. Provider approvals remain user actions; ready does not mean paid.
323+
324+
See [Vault payments](docs/vault-payments.md) for both provider flows, safety rules, and response shapes. `manage_browsers` accepts creation-only `vaults` references (max 20); existing sessions and pools cannot gain vault bindings. The four vault tools share the `vaults` toolset and prepare/observe credentials rather than submitting merchant payments.
319325

320326
### Standalone tools
321327

bun.lock

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

docs/vault-payments.md

Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
# Vault payments
2+
3+
The vault tools prepare and observe payment credentials. They do **not** submit
4+
merchant payments, expose real card values, or complete provider approval actions.
5+
They use the same vault API as the Kernel CLI.
6+
7+
## Tools and scope
8+
9+
| Tool | Actions |
10+
| ---------------------- | ------------------------------------------- |
11+
| `manage_vaults` | `create`, `list`, `get`, `delete` |
12+
| `manage_vault_wallets` | `create`, `payment_methods` |
13+
| `manage_vault_cards` | `create`, `update` |
14+
| `manage_vault_items` | `list`, `get`, `invoke`, `events`, `delete` |
15+
16+
Every tool accepts an optional `project` name or ID. Vaults are project-owned;
17+
omitting `project` uses the API's effective default project, **not** all projects.
18+
Project-scoped connections cannot switch projects. Use `get_connection_context`
19+
to inspect the connection's scope.
20+
21+
`vault` accepts an ID or immutable name. `key` is an immutable item key within that
22+
vault, not the item ID. Vault names, item keys, and project ownership cannot be renamed.
23+
24+
Wallet/card writes take a `provider` (`link` or `agentcard`) and a JSON `spec`
25+
**object**, not a string or a `{type, spec}` envelope. The tool injects `provider`;
26+
if present in `spec`, it must match. Tool schemas describe the provider-specific
27+
fields and reject unknown fields, including nested ones. No defaults or currency
28+
normalization are applied. Amounts are integer minor currency units. All integer
29+
inputs, including `expires_at`, must fit JavaScript's safe integer range; unsafe
30+
numbers are rejected, not silently rounded. The API enforces provider/state rules.
31+
32+
These capabilities use the existing MCP authentication and deployment. To expose
33+
only payment tools on a self-hosted server, set:
34+
35+
```sh
36+
KERNEL_MCP_ENABLED_TOOLSETS=vaults
37+
```
38+
39+
For browser checkout automation too, use `vaults browsers playwright computer`.
40+
To hide the payment tools, set `KERNEL_MCP_DISABLED_TOOLSETS=vaults`.
41+
This filters discovery; API authorization still enforces resource access.
42+
43+
## Link flow
44+
45+
1. Create or retrieve a vault with `manage_vaults`:
46+
47+
```json
48+
{ "action": "create", "name": "checkout" }
49+
```
50+
51+
2. Connect a wallet with `manage_vault_wallets`:
52+
53+
```json
54+
{
55+
"action": "create",
56+
"vault": "checkout",
57+
"key": "wallet-1",
58+
"provider": "link",
59+
"spec": {
60+
"authorization": {
61+
"method": "oauth",
62+
"client": { "type": "kernel_managed" }
63+
}
64+
}
65+
}
66+
```
67+
68+
Give the returned `item.action.url` to the user to complete with the provider.
69+
Do not ask for card details or OAuth codes/tokens in chat. Observe the wallet
70+
with `manage_vault_items`, `action: "get"`, the same vault/key, and `wait: 30`.
71+
72+
3. Once connected, call `manage_vault_wallets` with `action: "payment_methods"`
73+
and the same vault/key. Explicitly select a returned method ID with the user;
74+
do not automatically choose the default. Capabilities are advisory: absent
75+
means unknown, not ineligible.
76+
77+
4. Create the purchase request with `manage_vault_cards`, replacing
78+
`pm_selected` with the selected returned ID:
79+
80+
```json
81+
{
82+
"action": "create",
83+
"vault": "checkout",
84+
"key": "order-1",
85+
"provider": "link",
86+
"spec": {
87+
"wallet": "wallet-1",
88+
"payment_method_id": "pm_selected",
89+
"amount": 1234,
90+
"currency": "usd",
91+
"merchant_name": "Example Shop",
92+
"merchant_url": "https://shop.example",
93+
"context": "Purchase the selected office supplies from Example Shop for the approved order, with a total spending limit of 1234 minor currency units."
94+
}
95+
}
96+
```
97+
98+
Link also supports `line_items`, `totals`, `metadata`, and `expires_at`.
99+
Creating or updating the card does **not** implicitly authorize it.
100+
101+
5. Read `available_operations` with `manage_vault_items`, `action: "get"`.
102+
Read the operation description and obtain explicit user approval before
103+
invoking an advertised operation:
104+
105+
```json
106+
{
107+
"action": "invoke",
108+
"vault": "checkout",
109+
"key": "order-1",
110+
"operation": "authorize"
111+
}
112+
```
113+
114+
The tool fetches the item again and submits only a currently advertised
115+
operation. The current API accepts only `{"type":"authorize"}`; there are no
116+
operation parameters. New parameterless operation names can be forwarded when
117+
the API advertises them. Follow any returned provider action and observe state.
118+
OAuth, enrollment, MFA, and approval actions are for the user, not operation names.
119+
120+
6. When ready, create a new browser with `manage_browsers`:
121+
122+
```json
123+
{
124+
"action": "create",
125+
"vaults": [{ "name": "checkout" }]
126+
}
127+
```
128+
129+
Use only returned `item.state.aliases` through the browser tools in **that
130+
browser**, respecting returned permitted domains. Merchant checkout submission
131+
is a separate browser action and requires the user's authorization.
132+
133+
## AgentCard flow
134+
135+
Use a separate vault or different immutable item keys. Create the vault as above,
136+
then connect a wallet with `manage_vault_wallets`:
137+
138+
```json
139+
{
140+
"action": "create",
141+
"vault": "checkout",
142+
"key": "agentcard-wallet",
143+
"provider": "agentcard",
144+
"spec": {}
145+
}
146+
```
147+
148+
Complete the returned enrollment action. Alternatively, `spec.user_id` may refer
149+
to a user already enrolled in this organization. Once connected, configure a card
150+
with `manage_vault_cards`:
151+
152+
```json
153+
{
154+
"action": "create",
155+
"vault": "checkout",
156+
"key": "agentcard-order",
157+
"provider": "agentcard",
158+
"spec": {
159+
"wallet": "agentcard-wallet",
160+
"merchant": "Example Shop",
161+
"amount": 1234,
162+
"currency": "usd"
163+
}
164+
}
165+
```
166+
167+
AgentCard uses `merchant`, not Link's `merchant_name`. Optionally inspect wallet
168+
payment methods and provide a returned `card_id`; otherwise the cardholder selects
169+
one at approval. AgentCard currently does not advertise `authorize`: authorization
170+
happens at checkout. Attach the vault to a new browser and use returned aliases.
171+
Observe the card for its checkout authorization and any approval URL for the user.
172+
A reusable card remaining `ready` does not establish that the last payment succeeded.
173+
174+
## Observation, updates, and safety
175+
176+
- Single-item responses are JSON text containing `{item, guidance}`. They preserve
177+
public state, non-secret aliases, masks, safe action/approval URLs, advertised
178+
operations/expansions, and payment outcomes. Unknown provider fields, opaque
179+
event data, free-form metadata, and URLs carrying OAuth codes/tokens are omitted.
180+
There is no raw-output or raw-card tool.
181+
- Vault lists return `{items, has_more, next_offset}`. Item lists return `{items}`.
182+
`get` with `expand: ["payment_methods"]` is equivalent to the wallet
183+
`payment_methods` action. An unavailable expansion returns an API error.
184+
- `get` and `events` accept `wait: 0..60`. Each call is one bounded observation,
185+
not a background polling loop or readiness guarantee. The SDK timeout is the
186+
wait plus 30 seconds; configure the MCP client's timeout accordingly, or use
187+
shorter waits. Request cancellation is propagated to the SDK.
188+
- `events` accepts `after` and returns `{events, next_after, guidance}`. Pass
189+
`next_after` on the next call for the same vault/key. An empty result preserves
190+
the input cursor (or returns `null` without one).
191+
- **Ready does not mean paid.** Inspect state and immutable events for outcomes.
192+
No vault request is automatically retried. After a failed, timed-out, rejected,
193+
or indeterminate payment, inspect state/events; do not replay checkout, invoke
194+
again, or reconfigure a card to retry it.
195+
- Card `update` replaces the **entire spec**; omitted optional fields are removed.
196+
The API decides when a card can be reconfigured.
197+
- Browser attachments accept at most 20 references, each containing exactly one
198+
`id` or `name`. They are creation-only and unavailable for browser pools. You
199+
cannot add vaults to an existing browser. Vault-bound browser creation also
200+
disables automatic SDK retries.
201+
- Provider-assigned permitted domains are not configurable through these tools.
202+
- Vault/item deletion invalidates the affected credentials. Confirm with the user
203+
first. Any HTTP 404 returns `deleted_or_not_found`, including a missing project;
204+
other errors fail. Non-delete 404s remain errors.
205+
- The existing analytics filter omits tool inputs, outputs, and error messages;
206+
do not add payment payloads or action URLs to application logs.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@
4040
"@clerk/themes": "^2.4.19",
4141
"@modelcontextprotocol/sdk": "1.26.0",
4242
"@onkernel/managed-auth-react": "0.5.1",
43-
"@onkernel/sdk": "^0.98.0",
43+
"@onkernel/sdk": "^0.100.0",
4444
"@posthog/mcp": "0.10.1",
4545
"@types/jsonwebtoken": "^9.0.10",
4646
"@types/redis": "^4.0.11",

src/lib/mcp/analytics.test.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -341,6 +341,27 @@ describe("sanitizeMcpAnalyticsEvent", () => {
341341
expect(result?.properties[PostHogMCPAnalyticsProperty.IsError]).toBe(false);
342342
});
343343

344+
test("drops vault specs, aliases, provider actions, and error bodies", async () => {
345+
const event = toolCallEvent({
346+
[PostHogMCPAnalyticsProperty.ToolName]: "manage_vault_cards",
347+
[PostHogMCPAnalyticsProperty.Parameters]: {
348+
spec: { metadata: { order: "private-order" } },
349+
},
350+
[PostHogMCPAnalyticsProperty.Response]: {
351+
state: { aliases: { number: "private-alias" } },
352+
action: { url: "https://provider.example/approve?code=private-code" },
353+
},
354+
[PostHogMCPAnalyticsProperty.ErrorMessage]: "private-provider-body",
355+
});
356+
357+
const result = await sanitizeMcpAnalyticsEvent(event);
358+
359+
expect(JSON.stringify(result)).not.toContain("private-");
360+
expect(result?.properties[PostHogMCPAnalyticsProperty.ToolName]).toBe(
361+
"manage_vault_cards",
362+
);
363+
});
364+
344365
test("drops $set so no person properties can flow", async () => {
345366
const event = toolCallEvent({ $set: { email: "agent@example.com" } });
346367

src/lib/mcp/register.test.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ const NON_AUTH_TOOLSETS = [
2020
"replays",
2121
"credentials",
2222
"credential_providers",
23+
"vaults",
2324
].join(",");
2425

2526
function captureRegistration(mcpApps: boolean) {
@@ -85,6 +86,33 @@ describe("MCP Apps additive registration", () => {
8586
});
8687

8788
describe("MCP toolset allowlist", () => {
89+
test("enables the vault toolset independently and honors its denylist", () => {
90+
const previousEnabled = process.env.KERNEL_MCP_ENABLED_TOOLSETS;
91+
const previousDisabled = process.env.KERNEL_MCP_DISABLED_TOOLSETS;
92+
process.env.KERNEL_MCP_ENABLED_TOOLSETS = "vaults";
93+
delete process.env.KERNEL_MCP_DISABLED_TOOLSETS;
94+
try {
95+
expect(captureRegistration(false).legacyTools).toEqual([
96+
"get_connection_context",
97+
"manage_vault_wallets",
98+
"manage_vault_cards",
99+
"manage_vault_items",
100+
"manage_vaults",
101+
]);
102+
process.env.KERNEL_MCP_DISABLED_TOOLSETS = "vaults";
103+
expect(captureRegistration(false).legacyTools).toEqual([
104+
"get_connection_context",
105+
]);
106+
} finally {
107+
if (previousEnabled === undefined)
108+
delete process.env.KERNEL_MCP_ENABLED_TOOLSETS;
109+
else process.env.KERNEL_MCP_ENABLED_TOOLSETS = previousEnabled;
110+
if (previousDisabled === undefined)
111+
delete process.env.KERNEL_MCP_DISABLED_TOOLSETS;
112+
else process.env.KERNEL_MCP_DISABLED_TOOLSETS = previousDisabled;
113+
}
114+
});
115+
88116
test("keeps connection context and only the selected browser controls", () => {
89117
const previousEnabled = process.env.KERNEL_MCP_ENABLED_TOOLSETS;
90118
const previousDisabled = process.env.KERNEL_MCP_DISABLED_TOOLSETS;
@@ -130,6 +158,10 @@ describe("project selection registration", () => {
130158
"manage_replays",
131159
"manage_auth_connections",
132160
"manage_credentials",
161+
"manage_vaults",
162+
"manage_vault_wallets",
163+
"manage_vault_cards",
164+
"manage_vault_items",
133165
"open_auth_login",
134166
"begin_auth_login",
135167
];

src/lib/mcp/register.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import { registerProxyTools } from "@/lib/mcp/tools/proxies";
2424
import { registerReplayTools } from "@/lib/mcp/tools/replays";
2525
import { registerShellTool } from "@/lib/mcp/tools/shell";
2626
import { registerWebMcpTool } from "@/lib/mcp/tools/webmcp";
27+
import { registerVaultCapabilities } from "@/lib/mcp/tools/vaults";
2728
type McpToolOptions = McpDependencies;
2829
type McpRegistrationOptions = {
2930
mcpApps?: boolean;
@@ -54,6 +55,7 @@ const mcpToolRegistrations = [
5455
["auth_connections", registerManagedAuthCapabilities],
5556
["credentials", registerCredentialTools],
5657
["credential_providers", registerCredentialProviderTools],
58+
["vaults", registerVaultCapabilities],
5759
] as const satisfies readonly (readonly [string, RegisterMcpToolset])[];
5860

5961
type McpToolset = (typeof mcpToolRegistrations)[number][0];

0 commit comments

Comments
 (0)