Skip to content

Commit 0ac1b82

Browse files
committed
Inline vault input schemas for MCP clients
1 parent a01031b commit 0ac1b82

4 files changed

Lines changed: 174 additions & 148 deletions

File tree

‎src/lib/mcp/tools/vault-responses.ts‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -78,8 +78,8 @@ export const vaultItemOutputSchema = z.discriminatedUnion("type", [
7878
...itemFields,
7979
type: z.literal("wallet"),
8080
spec: z.discriminatedUnion("provider", [
81-
linkWalletSpecSchema.strip(),
82-
agentCardWalletSpecSchema.strip(),
81+
linkWalletSpecSchema().strip(),
82+
agentCardWalletSpecSchema().strip(),
8383
]),
8484
state: z.discriminatedUnion("provider", [
8585
z.object({
@@ -132,8 +132,8 @@ export const vaultItemOutputSchema = z.discriminatedUnion("type", [
132132
...itemFields,
133133
type: z.literal("card"),
134134
spec: z.discriminatedUnion("provider", [
135-
linkCardSpecSchema.omit({ metadata: true }).strip(),
136-
agentCardCardSpecSchema.strip(),
135+
linkCardSpecSchema().omit({ metadata: true }).strip(),
136+
agentCardCardSpecSchema().strip(),
137137
]),
138138
state: z.discriminatedUnion("provider", [
139139
z.object({

‎src/lib/mcp/tools/vault-schemas.ts‎

Lines changed: 161 additions & 139 deletions
Original file line numberDiff line numberDiff line change
@@ -1,151 +1,173 @@
11
import { z } from "zod";
22

3-
const itemKey = z.string().regex(/^[a-zA-Z0-9._-]{1,255}$/);
4-
const currency = z.string().regex(/^[A-Za-z]{3}$/);
3+
// Fresh instances keep tools/list schemas inline for clients that cannot resolve $ref.
4+
export function vaultItemKeySchema() {
5+
return z.string().regex(/^[a-zA-Z0-9._-]{1,255}$/);
6+
}
57

6-
const linkTotal = z
7-
.object({
8-
type: z.string(),
9-
display_text: z.string(),
10-
amount: z.number().int().describe("Amount in minor currency units."),
11-
})
12-
.strict();
8+
function currencySchema() {
9+
return z.string().regex(/^[A-Za-z]{3}$/);
10+
}
1311

14-
const linkLineItem = z
15-
.object({
16-
name: z.string(),
17-
quantity: z.number().int().min(1).optional(),
18-
unit_amount: z.number().int().optional(),
19-
description: z.string().optional(),
20-
sku: z.string().optional(),
21-
url: z.string().optional(),
22-
image_url: z.string().optional(),
23-
product_url: z.string().optional(),
24-
totals: z.array(linkTotal).optional(),
25-
})
26-
.strict();
27-
28-
export const linkWalletSpecSchema = z
29-
.object({
30-
provider: z.literal("link"),
31-
authorization: z
32-
.object({
33-
method: z.literal("oauth"),
34-
client: z.object({ type: z.literal("kernel_managed") }).strict(),
35-
})
36-
.strict()
37-
.describe(
38-
"Kernel-managed OAuth only. Follow the returned action URL; never supply OAuth codes, tokens, or client secrets.",
39-
),
40-
})
41-
.strict();
12+
function linkTotalSchema() {
13+
return z
14+
.object({
15+
type: z.string(),
16+
display_text: z.string(),
17+
amount: z.number().int().describe("Amount in minor currency units."),
18+
})
19+
.strict();
20+
}
4221

43-
export const agentCardWalletSpecSchema = z
44-
.object({
45-
provider: z.literal("agentcard"),
46-
user_id: z
47-
.string()
48-
.regex(/^usr_[A-Za-z0-9_]+$/)
49-
.describe(
50-
"Optional user ID already enrolled by a wallet in this organization. Otherwise follow the returned card_enrollment action. Sandbox/live mode is deployment-configured, not an item option.",
51-
)
52-
.optional(),
53-
})
54-
.strict();
22+
function linkLineItemSchema() {
23+
return z
24+
.object({
25+
name: z.string(),
26+
quantity: z.number().int().min(1).optional(),
27+
unit_amount: z.number().int().optional(),
28+
description: z.string().optional(),
29+
sku: z.string().optional(),
30+
url: z.string().optional(),
31+
image_url: z.string().optional(),
32+
product_url: z.string().optional(),
33+
totals: z.array(linkTotalSchema()).optional(),
34+
})
35+
.strict();
36+
}
5537

56-
export const linkCardSpecSchema = z
57-
.object({
58-
provider: z.literal("link"),
59-
wallet: itemKey.describe("Connected Link wallet item key in this vault."),
60-
payment_method_id: z
61-
.string()
62-
.min(1)
63-
.describe(
64-
"Select an ID from this wallet's advertised payment_methods expansion. Missing capability data is unknown, not ineligible; the provider decides eligibility.",
65-
),
66-
amount: z
67-
.number()
68-
.int()
69-
.min(1)
70-
.max(500000)
71-
.describe(
72-
"Requested amount in minor currency units, not a decimal price.",
73-
),
74-
currency,
75-
merchant_name: z.string().min(1).max(255),
76-
merchant_url: z
77-
.string()
78-
.url()
79-
.describe(
80-
"Merchant URL for this purchase. Permitted domains are returned in state.domains; there is no writable domains field.",
81-
),
82-
context: z
83-
.string()
84-
.min(100)
85-
.describe(
86-
"At least 100 characters of non-sensitive purchase context for the approval request.",
87-
),
88-
test: z
89-
.boolean()
90-
.describe(
91-
"Required explicit intent: true requests test credentials; false requests a live credential. Neither submits a merchant payment.",
92-
),
93-
expires_at: z.number().int().optional(),
94-
line_items: z.array(linkLineItem).optional(),
95-
totals: z.array(linkTotal).optional(),
96-
metadata: z
97-
.record(z.string())
98-
.describe(
99-
"Non-sensitive purchase metadata only. Never include card data, tokens, codes, ciphertext, or provider secrets. Omitted from MCP responses.",
100-
)
101-
.optional(),
102-
})
103-
.strict();
38+
export function linkWalletSpecSchema() {
39+
return z
40+
.object({
41+
provider: z.literal("link"),
42+
authorization: z
43+
.object({
44+
method: z.literal("oauth"),
45+
client: z.object({ type: z.literal("kernel_managed") }).strict(),
46+
})
47+
.strict()
48+
.describe(
49+
"Kernel-managed OAuth only. Follow the returned action URL; never supply OAuth codes, tokens, or client secrets.",
50+
),
51+
})
52+
.strict();
53+
}
10454

105-
export const agentCardCardSpecSchema = z
106-
.object({
107-
provider: z.literal("agentcard"),
108-
wallet: itemKey.describe(
109-
"AgentCard wallet item key in this vault. Complete its enrollment before checkout.",
110-
),
111-
merchant: z
112-
.string()
113-
.min(1)
114-
.max(120)
115-
.describe("Merchant shown on the cardholder's approval screen."),
116-
amount: z
117-
.number()
118-
.int()
119-
.min(1)
120-
.max(Number.MAX_SAFE_INTEGER)
121-
.describe("Amount in minor currency units for the checkout approval."),
122-
currency,
123-
card_id: z
124-
.string()
125-
.regex(/^vc_[A-Za-z0-9_]+$/)
126-
.describe(
127-
"Optional vaulted card ID from the wallet's payment_methods expansion; omit to let the cardholder choose on the approval screen. No per-item test flag: confirm deployment sandbox/live mode before checkout.",
128-
)
129-
.optional(),
130-
})
131-
.strict();
55+
export function agentCardWalletSpecSchema() {
56+
return z
57+
.object({
58+
provider: z.literal("agentcard"),
59+
user_id: z
60+
.string()
61+
.regex(/^usr_[A-Za-z0-9_]+$/)
62+
.describe(
63+
"Optional user ID already enrolled by a wallet in this organization. Otherwise follow the returned card_enrollment action. Sandbox/live mode is deployment-configured, not an item option.",
64+
)
65+
.optional(),
66+
})
67+
.strict();
68+
}
13269

133-
export const cardSpecSchema = z.discriminatedUnion("provider", [
134-
linkCardSpecSchema,
135-
agentCardCardSpecSchema,
136-
]);
70+
export function linkCardSpecSchema() {
71+
return z
72+
.object({
73+
provider: z.literal("link"),
74+
wallet: vaultItemKeySchema().describe(
75+
"Connected Link wallet item key in this vault.",
76+
),
77+
payment_method_id: z
78+
.string()
79+
.min(1)
80+
.describe(
81+
"Select an ID from this wallet's advertised payment_methods expansion. Missing capability data is unknown, not ineligible; the provider decides eligibility.",
82+
),
83+
amount: z
84+
.number()
85+
.int()
86+
.min(1)
87+
.max(500000)
88+
.describe(
89+
"Requested amount in minor currency units, not a decimal price.",
90+
),
91+
currency: currencySchema(),
92+
merchant_name: z.string().min(1).max(255),
93+
merchant_url: z
94+
.string()
95+
.url()
96+
.describe(
97+
"Merchant URL for this purchase. Permitted domains are returned in state.domains; there is no writable domains field.",
98+
),
99+
context: z
100+
.string()
101+
.min(100)
102+
.describe(
103+
"At least 100 characters of non-sensitive purchase context for the approval request.",
104+
),
105+
test: z
106+
.boolean()
107+
.describe(
108+
"Required explicit intent: true requests test credentials; false requests a live credential. Neither submits a merchant payment.",
109+
),
110+
expires_at: z.number().int().optional(),
111+
line_items: z.array(linkLineItemSchema()).optional(),
112+
totals: z.array(linkTotalSchema()).optional(),
113+
metadata: z
114+
.record(z.string())
115+
.describe(
116+
"Non-sensitive purchase metadata only. Never include card data, tokens, codes, ciphertext, or provider secrets. Omitted from MCP responses.",
117+
)
118+
.optional(),
119+
})
120+
.strict();
121+
}
137122

138-
export const vaultItemInputSchema = z.discriminatedUnion("type", [
139-
z
123+
export function agentCardCardSpecSchema() {
124+
return z
140125
.object({
141-
type: z.literal("wallet"),
142-
spec: z.discriminatedUnion("provider", [
143-
linkWalletSpecSchema,
144-
agentCardWalletSpecSchema,
145-
]),
126+
provider: z.literal("agentcard"),
127+
wallet: vaultItemKeySchema().describe(
128+
"AgentCard wallet item key in this vault. Complete its enrollment before checkout.",
129+
),
130+
merchant: z
131+
.string()
132+
.min(1)
133+
.max(120)
134+
.describe("Merchant shown on the cardholder's approval screen."),
135+
amount: z
136+
.number()
137+
.int()
138+
.min(1)
139+
.max(Number.MAX_SAFE_INTEGER)
140+
.describe("Amount in minor currency units for the checkout approval."),
141+
currency: currencySchema(),
142+
card_id: z
143+
.string()
144+
.regex(/^vc_[A-Za-z0-9_]+$/)
145+
.describe(
146+
"Optional vaulted card ID from the wallet's payment_methods expansion; omit to let the cardholder choose on the approval screen. No per-item test flag: confirm deployment sandbox/live mode before checkout.",
147+
)
148+
.optional(),
146149
})
147-
.strict(),
148-
z.object({ type: z.literal("card"), spec: cardSpecSchema }).strict(),
149-
]);
150+
.strict();
151+
}
150152

151-
export const vaultItemKeySchema = itemKey;
153+
export function cardSpecSchema() {
154+
return z.discriminatedUnion("provider", [
155+
linkCardSpecSchema(),
156+
agentCardCardSpecSchema(),
157+
]);
158+
}
159+
160+
export function vaultItemInputSchema() {
161+
return z.discriminatedUnion("type", [
162+
z
163+
.object({
164+
type: z.literal("wallet"),
165+
spec: z.discriminatedUnion("provider", [
166+
linkWalletSpecSchema(),
167+
agentCardWalletSpecSchema(),
168+
]),
169+
})
170+
.strict(),
171+
z.object({ type: z.literal("card"), spec: cardSpecSchema() }).strict(),
172+
]);
173+
}

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

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,14 @@ import type {
1010
import { describe, expect, test } from "bun:test";
1111
import { connectTestMcp, toolResultJSON } from "@/lib/mcp/mcp-test-fixtures";
1212
import {
13-
cardSpecSchema,
14-
vaultItemInputSchema,
13+
cardSpecSchema as createCardSpecSchema,
14+
vaultItemInputSchema as createVaultItemInputSchema,
1515
} from "@/lib/mcp/tools/vault-schemas";
1616
import { registerVaultTools } from "@/lib/mcp/tools/vaults";
1717

18+
const cardSpecSchema = createCardSpecSchema();
19+
const vaultItemInputSchema = createVaultItemInputSchema();
20+
1821
const dates = {
1922
created_at: "2026-09-01T00:00:00Z",
2023
updated_at: "2026-09-01T00:00:00Z",
@@ -173,6 +176,7 @@ describe("manage_vaults", () => {
173176
const { tools } = await client.listTools();
174177
expect(tools.map(({ name }) => name)).toEqual(["manage_vaults"]);
175178
const tool = tools[0];
179+
expect(JSON.stringify(tool.inputSchema)).not.toContain('"$ref"');
176180
expect(tool.annotations).toMatchObject({
177181
readOnlyHint: false,
178182
destructiveHint: true,

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -75,17 +75,17 @@ export function registerVaultTools(
7575
"(upsert) Immutable vault name, unique in the project; cannot be a cuid-like ID. Creates or retrieves, never renames.",
7676
)
7777
.optional(),
78-
key: vaultItemKeySchema
78+
key: vaultItemKeySchema()
7979
.describe(
8080
"Immutable item key within this vault. Required for item operations except list_items.",
8181
)
8282
.optional(),
83-
item: vaultItemInputSchema
83+
item: vaultItemInputSchema()
8484
.describe(
8585
"(upsert_item) Exactly type and provider-discriminated spec. Identical PUTs may retrieve an existing item; changed specs conflict, and authorized Link cards cannot be replaced. Use get_item to inspect, not repeated writes.",
8686
)
8787
.optional(),
88-
spec: cardSpecSchema
88+
spec: cardSpecSchema()
8989
.describe(
9090
"(update_item) Complete replacement card spec, not a partial patch. The API enforces provider and lifecycle constraints. Never use this to repeat an uncertain payment.",
9191
)

0 commit comments

Comments
 (0)