Skip to content

Commit ee4a7a0

Browse files
committed
Inline vault tool schemas and restrict observation waits
1 parent 0be41fc commit ee4a7a0

9 files changed

Lines changed: 110 additions & 40 deletions

File tree

docs/vault-payments.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,8 @@ A reusable card remaining `ready` does not establish that the last payment succe
181181
- Vault lists return `{items, has_more, next_offset}`. Item lists return `{items}`.
182182
`get` with `expand: ["payment_methods"]` is equivalent to the wallet
183183
`payment_methods` action. An unavailable expansion returns an API error.
184-
- `get` and `events` accept `wait: 0..60`. Each call is one bounded observation,
184+
- Only `get` and `events` accept `wait: 0..60`; other actions reject it.
185+
`invoke` does not wait for authorization. Each observation is bounded,
185186
not a background polling loop or readiness guarantee. The SDK timeout is the
186187
wait plus 30 seconds; configure the MCP client's timeout accordingly, or use
187188
shorter waits. Request cancellation is propagated to the SDK.

src/lib/mcp/tools/browser-vaults.test.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,18 @@ import { connectTestMcp } from "@/lib/mcp/mcp-test-fixtures";
44
import { registerBrowserCapabilities } from "@/lib/mcp/tools/browsers";
55

66
describe("browser vault attachment", () => {
7+
test("advertises inline vault references without JSON Schema refs", async () => {
8+
const fixture = await connectTestMcp(registerBrowserCapabilities, {});
9+
try {
10+
const { tools } = await fixture.client.listTools();
11+
const browser = tools.find((tool) => tool.name === "manage_browsers");
12+
expect(browser?.inputSchema.properties).toHaveProperty("vaults");
13+
expect(JSON.stringify(browser?.inputSchema)).not.toContain('"$ref"');
14+
} finally {
15+
await fixture.close();
16+
}
17+
});
18+
719
test("forwards creation-only references by ID and name", async () => {
820
const requests: Array<{ body: unknown; options: unknown }> = [];
921
const fixture = await connectTestMcp(registerBrowserCapabilities, {

src/lib/mcp/tools/vault-cards.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ export function registerVaultCardTools(
2020
'Configure payment card requests, not merchant payments. "create" creates or retrieves an identical card request by immutable key. "update" replaces the ENTIRE spec, removing omitted optional fields, only when the API permits it. Neither implicitly authorizes Link: inspect available_operations with manage_vault_items and obtain explicit user approval before invoking. AgentCard authorizes at checkout. Amounts are integer minor currency units. No card data, OAuth tokens, provider secrets, or domain configuration. Never reconfigure a card to retry a failed, timed-out, rejected, or indeterminate payment. Requests are not automatically retried.',
2121
{
2222
...vaultItemSchema,
23-
key: vaultKeySchema,
23+
key: vaultKeySchema(),
2424
action: z.enum(["create", "update"]),
2525
provider: vaultProviderSchema,
2626
spec: z

src/lib/mcp/tools/vault-items.test.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,46 @@ describe("advertised vault operations", () => {
141141
});
142142

143143
describe("vault observation and deletion", () => {
144+
test("advertises wait as get/events-only", async () => {
145+
const fixture = await connectVaultTest([]);
146+
try {
147+
const { tools } = await fixture.client.listTools();
148+
const tool = tools.find((tool) => tool.name === "manage_vault_items");
149+
expect(tool?.inputSchema.properties?.wait).toMatchObject({
150+
description: expect.stringContaining("(get, events)"),
151+
minimum: 0,
152+
maximum: 60,
153+
});
154+
} finally {
155+
await fixture.close();
156+
}
157+
});
158+
159+
test.each(["list", "invoke", "delete"])(
160+
"rejects wait on %s without making a request",
161+
async (action) => {
162+
const fixture = await connectVaultTest([]);
163+
try {
164+
for (const wait of [0, 60]) {
165+
const result = await fixture.call("manage_vault_items", {
166+
action,
167+
vault: "checkout",
168+
key: "order-1",
169+
operation: "authorize",
170+
wait,
171+
});
172+
expect(result.isError).toBe(true);
173+
expect(JSON.stringify(result)).toContain(
174+
"wait is only supported for get and events",
175+
);
176+
}
177+
expect(fixture.requests).toHaveLength(0);
178+
} finally {
179+
await fixture.close();
180+
}
181+
},
182+
);
183+
144184
test("returns pending state without polling, and preserves the event cursor on an empty wait", async () => {
145185
const event = {
146186
id: "evt_2",

src/lib/mcp/tools/vault-items.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ export function registerVaultItemTools(
2828
{
2929
...vaultItemSchema,
3030
action: z.enum(["list", "get", "invoke", "events", "delete"]),
31-
key: vaultKeySchema
31+
key: vaultKeySchema()
3232
.describe("Required except for list. Immutable item key, not ID.")
3333
.optional(),
3434
operation: z
@@ -69,6 +69,15 @@ export function registerVaultItemTools(
6969
);
7070
const options = { maxRetries: 0, signal: extra.signal };
7171
try {
72+
if (
73+
params.wait !== undefined &&
74+
params.action !== "get" &&
75+
params.action !== "events"
76+
) {
77+
return errorResponse(
78+
"wait is only supported for get and events; invoke does not wait for authorization.",
79+
);
80+
}
7281
if (params.action === "list") {
7382
const items = await client.vaults.items.list(params.vault, options);
7483
return jsonResponse({

src/lib/mcp/tools/vault-wallets.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ export function registerVaultWalletTools(
2222
'Connect payment wallets without exposing secrets. "create" creates or retrieves an identical wallet by immutable key and returns a provider connection/enrollment action for the user to complete. "payment_methods" requests the advertised live payment_methods expansion (unavailable expansions return an API error). Select Link payment_method_id explicitly; never automatically choose a default. AgentCard card_id may be omitted for cardholder selection at checkout approval. Capabilities are advisory; absent means unknown. Never provide card data or OAuth codes/tokens. Requests are not automatically retried.',
2323
{
2424
...vaultItemSchema,
25-
key: vaultKeySchema,
25+
key: vaultKeySchema(),
2626
action: z.enum(["create", "payment_methods"]),
2727
provider: vaultProviderSchema
2828
.describe("(create) Payment provider.")

src/lib/mcp/tools/vaults.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ describe("vault SDK request contracts", () => {
2525
]);
2626
for (const tool of tools) {
2727
expect(tool.inputSchema.properties).toHaveProperty("project");
28+
expect(JSON.stringify(tool.inputSchema)).not.toContain('"$ref"');
2829
expect(tool.annotations).toMatchObject({
2930
readOnlyHint: false,
3031
idempotentHint: false,

src/lib/mcp/tools/vaults.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,10 +39,10 @@ export function registerVaultCapabilities(
3939
{
4040
...vaultProjectSchema,
4141
action: z.enum(["create", "list", "get", "delete"]),
42-
vault: vaultSelectorSchema
42+
vault: vaultSelectorSchema()
4343
.describe("(get, delete) Vault ID or immutable name.")
4444
.optional(),
45-
name: vaultSelectorSchema
45+
name: vaultSelectorSchema()
4646
.describe("(create) Immutable vault name.")
4747
.optional(),
4848
...paginationParams,

src/lib/mcp/vault-schemas.ts

Lines changed: 41 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
11
import { z } from "zod";
22
import { projectSelectionInputSchema } from "@/lib/mcp/project-selection";
33

4-
export const vaultSelectorSchema = z
5-
.string()
6-
.regex(/^[a-zA-Z0-9._-]{1,255}$/)
7-
.refine(
8-
(value) => value !== "." && value !== "..",
9-
"Invalid vault selector.",
10-
);
4+
// Fresh schemas per property keep tools/list contracts inline instead of emitting $refs.
5+
export function vaultSelectorSchema() {
6+
return z
7+
.string()
8+
.regex(/^[a-zA-Z0-9._-]{1,255}$/)
9+
.refine(
10+
(value) => value !== "." && value !== "..",
11+
"Invalid vault selector.",
12+
);
13+
}
1114

1215
export const vaultProjectSchema = projectSelectionInputSchema({
1316
project:
@@ -16,25 +19,27 @@ export const vaultProjectSchema = projectSelectionInputSchema({
1619

1720
export const vaultItemSchema = {
1821
...vaultProjectSchema,
19-
vault: vaultSelectorSchema.describe("Vault ID or immutable name."),
22+
vault: vaultSelectorSchema().describe("Vault ID or immutable name."),
2023
};
2124

22-
export const vaultKeySchema = vaultSelectorSchema.describe(
23-
"Immutable item key within the vault, not the item ID.",
24-
);
25+
export function vaultKeySchema() {
26+
return vaultSelectorSchema().describe(
27+
"Immutable item key within the vault, not the item ID.",
28+
);
29+
}
2530
export const vaultProviderSchema = z.enum(["link", "agentcard"]);
2631
export const vaultWaitSchema = z
2732
.number()
2833
.int()
2934
.min(0)
3035
.max(60)
3136
.describe(
32-
"One bounded server-side observation, in seconds (0-60). Pending state is returned as-is; this never retries a payment or guarantees readiness.",
37+
"(get, events) One bounded server-side observation, in seconds (0-60). Not supported for invoke, list, or delete. Pending state is returned as-is; this never retries a payment or guarantees readiness.",
3338
)
3439
.optional();
3540

36-
const integer = z.number().int().safe();
37-
const currency = z.string().regex(/^[A-Za-z]{3}$/);
41+
const integer = () => z.number().int().safe();
42+
const currency = () => z.string().regex(/^[A-Za-z]{3}$/);
3843

3944
// Keep provider specifications in sync with https://api.onkernel.com/spec.yaml.
4045
export const linkWalletSpecSchema = z
@@ -60,60 +65,62 @@ export const agentcardWalletSpecSchema = z
6065
})
6166
.strict();
6267

63-
const linkTotalSchema = z
64-
.object({
65-
type: z.string(),
66-
display_text: z.string(),
67-
amount: integer.describe("Integer minor currency units."),
68-
})
69-
.strict();
68+
function linkTotalSchema() {
69+
return z
70+
.object({
71+
type: z.string(),
72+
display_text: z.string(),
73+
amount: integer().describe("Integer minor currency units."),
74+
})
75+
.strict();
76+
}
7077

7178
const linkLineItemSchema = z
7279
.object({
7380
name: z.string(),
74-
quantity: integer.min(1).optional(),
75-
unit_amount: integer.optional(),
81+
quantity: integer().min(1).optional(),
82+
unit_amount: integer().optional(),
7683
description: z.string().optional(),
7784
sku: z.string().optional(),
7885
url: z.string().optional(),
7986
image_url: z.string().optional(),
8087
product_url: z.string().optional(),
81-
totals: z.array(linkTotalSchema).optional(),
88+
totals: z.array(linkTotalSchema()).optional(),
8289
})
8390
.strict();
8491

8592
export const linkCardSpecSchema = z
8693
.object({
8794
provider: z.literal("link").optional(),
88-
wallet: vaultKeySchema,
95+
wallet: vaultKeySchema(),
8996
payment_method_id: z
9097
.string()
9198
.min(1)
9299
.describe(
93100
"Explicitly selected ID from the wallet's payment_methods expansion.",
94101
),
95-
amount: integer
102+
amount: integer()
96103
.min(1)
97104
.max(500000)
98105
.describe("Integer minor currency units."),
99-
currency,
106+
currency: currency(),
100107
merchant_name: z.string().min(1).max(255),
101108
merchant_url: z.string().url(),
102109
context: z.string().min(100),
103110
line_items: z.array(linkLineItemSchema).optional(),
104-
totals: z.array(linkTotalSchema).optional(),
111+
totals: z.array(linkTotalSchema()).optional(),
105112
metadata: z.record(z.string()).optional(),
106-
expires_at: integer.optional(),
113+
expires_at: integer().optional(),
107114
})
108115
.strict();
109116

110117
export const agentcardCardSpecSchema = z
111118
.object({
112119
provider: z.literal("agentcard").optional(),
113-
wallet: vaultKeySchema,
120+
wallet: vaultKeySchema(),
114121
merchant: z.string().min(1).max(120),
115-
amount: integer.min(1).describe("Integer minor currency units."),
116-
currency,
122+
amount: integer().min(1).describe("Integer minor currency units."),
123+
currency: currency(),
117124
card_id: z
118125
.string()
119126
.regex(/^vc_[A-Za-z0-9_]+$/)
@@ -128,8 +135,8 @@ export const browserVaultsSchema = z
128135
.array(
129136
z
130137
.object({
131-
id: vaultSelectorSchema.optional(),
132-
name: vaultSelectorSchema.optional(),
138+
id: vaultSelectorSchema().optional(),
139+
name: vaultSelectorSchema().optional(),
133140
})
134141
.strict()
135142
.refine(

0 commit comments

Comments
 (0)