Skip to content

Commit 085cd78

Browse files
authored
Merge pull request #182 from kernel/hypeship/mcp-vault-payments
Add vault payment MCP tools
2 parents 9dcd166 + eb31dde commit 085cd78

24 files changed

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

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
2+
import { Kernel } from "@onkernel/sdk";
23
import type { McpConnectionScopeFailureAnalytics } from "@/lib/mcp/analytics";
34
import { defaultMcpDependencies } from "@/lib/mcp/dependencies";
45

@@ -137,6 +138,109 @@ describe("connection scope failures through the handler", () => {
137138
});
138139
});
139140

141+
describe("vault entitlement routing", () => {
142+
function installKernelResponses(entitlements: (token: string) => Response) {
143+
const paths: string[] = [];
144+
defaultMcpDependencies.createKernelClient = (token) =>
145+
new Kernel({
146+
apiKey: token,
147+
baseURL: "https://api.example.test",
148+
maxRetries: 0,
149+
fetch: async (input) => {
150+
const path = new URL(String(input)).pathname;
151+
paths.push(path);
152+
if (path === "/auth/context")
153+
return Response.json({
154+
authentication: {
155+
method: "api_key",
156+
source: "api_key",
157+
credential_id: "key_test",
158+
},
159+
principal: { type: "api_key", id: "key_test" },
160+
organization: { id: token === "sk_allowed" ? "org_a" : "org_b" },
161+
authorization: {
162+
credential_scope: { project_id: null },
163+
effective_scope: { project_id: null },
164+
},
165+
});
166+
if (path === "/org/entitlements") return entitlements(token);
167+
throw new Error(`Unexpected API request: ${path}`);
168+
},
169+
});
170+
return paths;
171+
}
172+
173+
async function call(method: string, token = "sk_allowed", params?: object) {
174+
const response = await POST(
175+
new nextServer.NextRequest("https://mcp.example.test/mcp", {
176+
method: "POST",
177+
headers: {
178+
Authorization: `Bearer ${token}`,
179+
"Content-Type": "application/json",
180+
Accept: "application/json, text/event-stream",
181+
},
182+
body: JSON.stringify({ jsonrpc: "2.0", id: 1, method, params }),
183+
}),
184+
);
185+
expect(response.status).toBe(200);
186+
const text = await response.text();
187+
const event = text.split("\n").find((line) => line.startsWith("data: "));
188+
return JSON.parse(event ? event.slice(6) : text);
189+
}
190+
191+
test("selects tools per credential and rechecks access after revocation", async () => {
192+
let enabled = true;
193+
const paths = installKernelResponses((token) =>
194+
Response.json({
195+
features: { vaults: { enabled: token === "sk_allowed" && enabled } },
196+
}),
197+
);
198+
const allowed = await call("tools/list");
199+
expect(
200+
allowed.result.tools.map((tool: { name: string }) => tool.name),
201+
).toContain("manage_vaults");
202+
const denied = await call("tools/list", "sk_denied");
203+
expect(
204+
denied.result.tools.filter((tool: { name: string }) =>
205+
tool.name.startsWith("manage_vault"),
206+
),
207+
).toHaveLength(0);
208+
expect(
209+
denied.result.tools.map((tool: { name: string }) => tool.name),
210+
).toContain("manage_browsers");
211+
enabled = false;
212+
const revoked = await call("tools/call", "sk_allowed", {
213+
name: "manage_vaults",
214+
arguments: { action: "list" },
215+
});
216+
expect(JSON.stringify(revoked)).toContain("not found");
217+
expect(paths).toEqual([
218+
"/auth/context",
219+
"/org/entitlements",
220+
"/auth/context",
221+
"/org/entitlements",
222+
"/auth/context",
223+
"/org/entitlements",
224+
]);
225+
});
226+
227+
test.each([200, 404, 503])(
228+
"keeps other tools available when entitlements are absent or fail (HTTP %s)",
229+
async (status) => {
230+
installKernelResponses(() => Response.json({ features: {} }, { status }));
231+
const result = await call("tools/list");
232+
expect(
233+
result.result.tools.filter((tool: { name: string }) =>
234+
tool.name.startsWith("manage_vault"),
235+
),
236+
).toHaveLength(0);
237+
expect(
238+
result.result.tools.map((tool: { name: string }) => tool.name),
239+
).toContain("manage_browsers");
240+
},
241+
);
242+
});
243+
140244
describe("connectionScopeFailureResponse", () => {
141245
test("names an inactive project instead of blaming the credential", async () => {
142246
const response = connectionScopeFailureResponse({

0 commit comments

Comments
 (0)