@agentwonderland/mcp 0.1.46 → 0.1.47
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/core/__tests__/payments.test.js +55 -1
- package/dist/core/__tests__/principal.test.js +18 -0
- package/dist/core/config.d.ts +16 -0
- package/dist/core/config.js +37 -0
- package/dist/core/link-cli.d.ts +27 -0
- package/dist/core/link-cli.js +259 -0
- package/dist/core/mpp-client.d.ts +13 -1
- package/dist/core/mpp-client.js +45 -2
- package/dist/core/payments.js +135 -10
- package/dist/core/principal.js +2 -2
- package/dist/core/version.d.ts +1 -1
- package/dist/core/version.js +1 -1
- package/dist/index.js +6 -4
- package/dist/tools/__tests__/wallet.test.js +153 -0
- package/dist/tools/passes.js +1 -1
- package/dist/tools/run.js +1 -1
- package/dist/tools/solve.js +1 -1
- package/dist/tools/wallet.js +195 -7
- package/package.json +1 -1
- package/src/core/__tests__/payments.test.ts +78 -1
- package/src/core/__tests__/principal.test.ts +23 -0
- package/src/core/config.ts +56 -0
- package/src/core/link-cli.ts +300 -0
- package/src/core/mpp-client.ts +69 -2
- package/src/core/payments.ts +153 -11
- package/src/core/principal.ts +2 -2
- package/src/core/version.ts +1 -1
- package/src/index.ts +6 -4
- package/src/tools/__tests__/wallet.test.ts +190 -0
- package/src/tools/passes.ts +1 -1
- package/src/tools/run.ts +1 -1
- package/src/tools/solve.ts +1 -1
- package/src/tools/wallet.ts +229 -6
|
@@ -18,6 +18,17 @@ type CardConfig = {
|
|
|
18
18
|
brand: string;
|
|
19
19
|
} | null;
|
|
20
20
|
|
|
21
|
+
type LinkConfig = {
|
|
22
|
+
paymentMethodId: string;
|
|
23
|
+
label?: string;
|
|
24
|
+
} | null;
|
|
25
|
+
|
|
26
|
+
type PendingLinkSetup = {
|
|
27
|
+
verificationUrl: string;
|
|
28
|
+
phrase: string;
|
|
29
|
+
createdAt: string;
|
|
30
|
+
} | null;
|
|
31
|
+
|
|
21
32
|
type WalletToolResult = {
|
|
22
33
|
content: Array<{ type: "text"; text: string }>;
|
|
23
34
|
};
|
|
@@ -26,8 +37,11 @@ const state = vi.hoisted(() => ({
|
|
|
26
37
|
wallets: [] as WalletEntry[],
|
|
27
38
|
addedWallets: [] as WalletEntry[],
|
|
28
39
|
card: null as CardConfig,
|
|
40
|
+
link: null as LinkConfig,
|
|
41
|
+
pendingLinkSetup: null as PendingLinkSetup,
|
|
29
42
|
pendingCardSetupToken: null as string | null,
|
|
30
43
|
spendPolicies: {} as Record<string, unknown>,
|
|
44
|
+
defaultPaymentMethod: undefined as string | undefined,
|
|
31
45
|
consumerPrincipal: "did:pkh:eip155:8453:0xabc",
|
|
32
46
|
owsAvailable: true,
|
|
33
47
|
owsWallets: [] as Array<{ id: string; name: string; address: string }>,
|
|
@@ -59,6 +73,12 @@ const state = vi.hoisted(() => ({
|
|
|
59
73
|
}
|
|
60
74
|
| null,
|
|
61
75
|
setCardConfigCalls: [] as CardConfig[],
|
|
76
|
+
setLinkConfigCalls: [] as LinkConfig[],
|
|
77
|
+
setPendingLinkSetupCalls: [] as PendingLinkSetup[],
|
|
78
|
+
linkAuthenticated: false,
|
|
79
|
+
linkPaymentMethods: [] as Array<{ id: string; label?: string; searchText?: string }>,
|
|
80
|
+
linkLoginCalls: 0,
|
|
81
|
+
linkPaymentMethodAddCalls: 0,
|
|
62
82
|
}));
|
|
63
83
|
|
|
64
84
|
vi.mock("../../core/config.js", () => ({
|
|
@@ -72,6 +92,8 @@ vi.mock("../../core/config.js", () => ({
|
|
|
72
92
|
}
|
|
73
93
|
},
|
|
74
94
|
getCardConfig: () => state.card,
|
|
95
|
+
getLinkConfig: () => state.link,
|
|
96
|
+
getPendingLinkSetup: () => state.pendingLinkSetup,
|
|
75
97
|
getPendingCardSetupToken: () => state.pendingCardSetupToken,
|
|
76
98
|
getSpendPolicy: (walletId: string) => state.spendPolicies[walletId] ?? null,
|
|
77
99
|
getWallets: () => state.wallets,
|
|
@@ -79,6 +101,17 @@ vi.mock("../../core/config.js", () => ({
|
|
|
79
101
|
state.card = card;
|
|
80
102
|
state.setCardConfigCalls.push(card);
|
|
81
103
|
},
|
|
104
|
+
setDefaultPaymentMethod: (method: string | undefined) => {
|
|
105
|
+
state.defaultPaymentMethod = method;
|
|
106
|
+
},
|
|
107
|
+
setLinkConfig: (link: LinkConfig) => {
|
|
108
|
+
state.link = link;
|
|
109
|
+
state.setLinkConfigCalls.push(link);
|
|
110
|
+
},
|
|
111
|
+
setPendingLinkSetup: (pending: PendingLinkSetup) => {
|
|
112
|
+
state.pendingLinkSetup = pending;
|
|
113
|
+
state.setPendingLinkSetupCalls.push(pending);
|
|
114
|
+
},
|
|
82
115
|
setSpendPolicy: (walletId: string, policy: unknown) => {
|
|
83
116
|
state.spendPolicies[walletId] = policy;
|
|
84
117
|
},
|
|
@@ -101,6 +134,23 @@ vi.mock("../../core/card-setup.js", () => ({
|
|
|
101
134
|
},
|
|
102
135
|
}));
|
|
103
136
|
|
|
137
|
+
vi.mock("../../core/link-cli.js", () => ({
|
|
138
|
+
getLinkCliAuthStatus: async () => ({
|
|
139
|
+
authenticated: state.linkAuthenticated,
|
|
140
|
+
}),
|
|
141
|
+
listLinkPaymentMethods: async () => state.linkPaymentMethods,
|
|
142
|
+
openLinkPaymentMethodAdd: async () => {
|
|
143
|
+
state.linkPaymentMethodAddCalls++;
|
|
144
|
+
},
|
|
145
|
+
startLinkCliLogin: async () => {
|
|
146
|
+
state.linkLoginCalls++;
|
|
147
|
+
return {
|
|
148
|
+
verificationUrl: "https://app.link.com/device/setup?code=test-link-code",
|
|
149
|
+
phrase: "test-link-code",
|
|
150
|
+
};
|
|
151
|
+
},
|
|
152
|
+
}));
|
|
153
|
+
|
|
104
154
|
vi.mock("../../core/ows-adapter.js", () => ({
|
|
105
155
|
createOwsWallet: async (name: string, chain: "evm" | "solana") => {
|
|
106
156
|
state.createdWalletCalls.push({ name, chain });
|
|
@@ -124,8 +174,11 @@ function resetState(): void {
|
|
|
124
174
|
state.wallets = [];
|
|
125
175
|
state.addedWallets = [];
|
|
126
176
|
state.card = null;
|
|
177
|
+
state.link = null;
|
|
178
|
+
state.pendingLinkSetup = null;
|
|
127
179
|
state.pendingCardSetupToken = null;
|
|
128
180
|
state.spendPolicies = {};
|
|
181
|
+
state.defaultPaymentMethod = undefined;
|
|
129
182
|
state.consumerPrincipal = "did:pkh:eip155:8453:0xabc";
|
|
130
183
|
state.owsAvailable = true;
|
|
131
184
|
state.owsWallets = [];
|
|
@@ -151,6 +204,12 @@ function resetState(): void {
|
|
|
151
204
|
state.pollCardSetupCalls = [];
|
|
152
205
|
state.pollCardSetupResult = null;
|
|
153
206
|
state.setCardConfigCalls = [];
|
|
207
|
+
state.setLinkConfigCalls = [];
|
|
208
|
+
state.setPendingLinkSetupCalls = [];
|
|
209
|
+
state.linkAuthenticated = false;
|
|
210
|
+
state.linkPaymentMethods = [];
|
|
211
|
+
state.linkLoginCalls = 0;
|
|
212
|
+
state.linkPaymentMethodAddCalls = 0;
|
|
154
213
|
}
|
|
155
214
|
|
|
156
215
|
async function getWalletSetupTool(): Promise<(args: Record<string, unknown>) => Promise<WalletToolResult>> {
|
|
@@ -205,12 +264,52 @@ describe("wallet_setup tool", () => {
|
|
|
205
264
|
label: "launch-wallet",
|
|
206
265
|
},
|
|
207
266
|
]);
|
|
267
|
+
expect(state.defaultPaymentMethod).toBe("base");
|
|
208
268
|
expect(text).toContain("Wallet created [encrypted]:");
|
|
209
269
|
expect(text).toContain("Address: 0x1111111111111111111111111111111111111111");
|
|
210
270
|
expect(text).toContain("Chains: tempo, base");
|
|
211
271
|
expect(text).toContain("Consumer principal: did:pkh:eip155:8453:0xabc");
|
|
212
272
|
});
|
|
213
273
|
|
|
274
|
+
it("shows a guided payment setup menu", async () => {
|
|
275
|
+
const walletSetup = await getWalletSetupTool();
|
|
276
|
+
|
|
277
|
+
const result = await walletSetup({ action: "start" });
|
|
278
|
+
const text = flattenText(result);
|
|
279
|
+
|
|
280
|
+
expect(text).toContain("Set up a payment method:");
|
|
281
|
+
expect(text).toContain("Link card/bank (recommended)");
|
|
282
|
+
expect(text).toContain('wallet_setup({ action: "add-link" })');
|
|
283
|
+
expect(text).toContain('wallet_setup({ action: "create", chain: "tempo" })');
|
|
284
|
+
expect(text).toContain('wallet_setup({ action: "create", chain: "base" })');
|
|
285
|
+
expect(text).toContain('wallet_setup({ action: "create", chain: "solana" })');
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
it("shows the setup menu when no payment methods are configured", async () => {
|
|
289
|
+
const tools = new Map<string, (args: Record<string, unknown>) => Promise<WalletToolResult>>();
|
|
290
|
+
const server = {
|
|
291
|
+
tool: (
|
|
292
|
+
name: string,
|
|
293
|
+
_description: string,
|
|
294
|
+
_schema: unknown,
|
|
295
|
+
handler: (args: Record<string, unknown>) => Promise<WalletToolResult>,
|
|
296
|
+
) => {
|
|
297
|
+
tools.set(name, handler);
|
|
298
|
+
},
|
|
299
|
+
} as unknown as McpServer;
|
|
300
|
+
|
|
301
|
+
const { registerWalletTools } = await import("../wallet.js");
|
|
302
|
+
registerWalletTools(server);
|
|
303
|
+
const walletStatus = tools.get("wallet_status");
|
|
304
|
+
if (!walletStatus) throw new Error("wallet_status tool was not registered");
|
|
305
|
+
|
|
306
|
+
const result = await walletStatus({});
|
|
307
|
+
const text = flattenText(result);
|
|
308
|
+
|
|
309
|
+
expect(text).toContain("No payment methods configured.");
|
|
310
|
+
expect(text).toContain("Link card/bank (recommended)");
|
|
311
|
+
});
|
|
312
|
+
|
|
214
313
|
it("imports a wallet into OWS encrypted storage with Base as the default chain", async () => {
|
|
215
314
|
const walletSetup = await getWalletSetupTool();
|
|
216
315
|
|
|
@@ -235,6 +334,7 @@ describe("wallet_setup tool", () => {
|
|
|
235
334
|
label: "imported-wallet",
|
|
236
335
|
},
|
|
237
336
|
]);
|
|
337
|
+
expect(state.defaultPaymentMethod).toBe("base");
|
|
238
338
|
expect(text).toContain("Key imported to OWS [encrypted]:");
|
|
239
339
|
expect(text).toContain("Address: 0x2222222222222222222222222222222222222222");
|
|
240
340
|
expect(text).toContain("Consumer principal: did:pkh:eip155:8453:0xabc");
|
|
@@ -287,4 +387,94 @@ describe("wallet_setup tool", () => {
|
|
|
287
387
|
expect(text).toContain("Removed Visa ****4242.");
|
|
288
388
|
expect(text).toContain("Card disconnected from Agent Wonderland.");
|
|
289
389
|
});
|
|
390
|
+
|
|
391
|
+
it("starts Link device auth from wallet_setup", async () => {
|
|
392
|
+
const walletSetup = await getWalletSetupTool();
|
|
393
|
+
const result = await walletSetup({ action: "add-link" });
|
|
394
|
+
const text = flattenText(result);
|
|
395
|
+
|
|
396
|
+
expect(state.linkLoginCalls).toBe(1);
|
|
397
|
+
expect(state.setPendingLinkSetupCalls[0]).toMatchObject({
|
|
398
|
+
verificationUrl: "https://app.link.com/device/setup?code=test-link-code",
|
|
399
|
+
phrase: "test-link-code",
|
|
400
|
+
});
|
|
401
|
+
expect(text).toContain("Approve Agent Wonderland in Link:");
|
|
402
|
+
expect(text).toContain("https://app.link.com/device/setup?code=test-link-code");
|
|
403
|
+
expect(text).toContain("Verification phrase: test-link-code");
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
it("continues a pending Link auth without starting a new login", async () => {
|
|
407
|
+
state.pendingLinkSetup = {
|
|
408
|
+
verificationUrl: "https://app.link.com/device/setup?code=existing",
|
|
409
|
+
phrase: "existing",
|
|
410
|
+
createdAt: new Date().toISOString(),
|
|
411
|
+
};
|
|
412
|
+
|
|
413
|
+
const walletSetup = await getWalletSetupTool();
|
|
414
|
+
const result = await walletSetup({ action: "add-link" });
|
|
415
|
+
const text = flattenText(result);
|
|
416
|
+
|
|
417
|
+
expect(state.linkLoginCalls).toBe(0);
|
|
418
|
+
expect(text).toContain("https://app.link.com/device/setup?code=existing");
|
|
419
|
+
expect(text).toContain("Verification phrase: existing");
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
it("connects the only Link payment method after auth", async () => {
|
|
423
|
+
state.linkAuthenticated = true;
|
|
424
|
+
state.pendingLinkSetup = {
|
|
425
|
+
verificationUrl: "https://app.link.com/device/setup?code=existing",
|
|
426
|
+
phrase: "existing",
|
|
427
|
+
createdAt: new Date().toISOString(),
|
|
428
|
+
};
|
|
429
|
+
state.linkPaymentMethods = [
|
|
430
|
+
{ id: "csmrpd_123", label: "Barclays Mastercard ****3688" },
|
|
431
|
+
];
|
|
432
|
+
|
|
433
|
+
const walletSetup = await getWalletSetupTool();
|
|
434
|
+
const result = await walletSetup({ action: "add-link" });
|
|
435
|
+
const text = flattenText(result);
|
|
436
|
+
|
|
437
|
+
expect(state.setPendingLinkSetupCalls).toEqual([null]);
|
|
438
|
+
expect(state.setLinkConfigCalls).toEqual([
|
|
439
|
+
{ paymentMethodId: "csmrpd_123", label: "Barclays Mastercard ****3688" },
|
|
440
|
+
]);
|
|
441
|
+
expect(text).toContain("Link payment method connected.");
|
|
442
|
+
expect(text).toContain("Barclays Mastercard ****3688");
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
it("shows friendly names when multiple Link payment methods exist", async () => {
|
|
446
|
+
state.linkAuthenticated = true;
|
|
447
|
+
state.linkPaymentMethods = [
|
|
448
|
+
{ id: "csmrpd_1", label: "Barclays Arrival Mastercard ****3688" },
|
|
449
|
+
{ id: "csmrpd_2", label: "Total Checking bank ****2889" },
|
|
450
|
+
];
|
|
451
|
+
|
|
452
|
+
const walletSetup = await getWalletSetupTool();
|
|
453
|
+
const result = await walletSetup({ action: "add-link" });
|
|
454
|
+
const text = flattenText(result);
|
|
455
|
+
|
|
456
|
+
expect(text).toContain("Barclays Arrival Mastercard ****3688 (csmrpd_1)");
|
|
457
|
+
expect(text).toContain("Total Checking bank ****2889 (csmrpd_2)");
|
|
458
|
+
expect(text).toContain('link_payment_method: "<name or last4>"');
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
it("resolves a friendly Link payment method selector", async () => {
|
|
462
|
+
state.linkAuthenticated = true;
|
|
463
|
+
state.linkPaymentMethods = [
|
|
464
|
+
{ id: "csmrpd_1", label: "Barclays Arrival Mastercard ****3688", searchText: "csmrpd_1 barclays arrival mastercard 3688" },
|
|
465
|
+
{ id: "csmrpd_2", label: "Total Checking bank ****2889", searchText: "csmrpd_2 total checking bank 2889" },
|
|
466
|
+
];
|
|
467
|
+
|
|
468
|
+
const walletSetup = await getWalletSetupTool();
|
|
469
|
+
const result = await walletSetup({
|
|
470
|
+
action: "add-link",
|
|
471
|
+
link_payment_method: "3688",
|
|
472
|
+
});
|
|
473
|
+
const text = flattenText(result);
|
|
474
|
+
|
|
475
|
+
expect(state.setLinkConfigCalls).toEqual([
|
|
476
|
+
{ paymentMethodId: "csmrpd_1", label: "Barclays Arrival Mastercard ****3688" },
|
|
477
|
+
]);
|
|
478
|
+
expect(text).toContain("Barclays Arrival Mastercard ****3688 (csmrpd_1)");
|
|
479
|
+
});
|
|
290
480
|
});
|
package/src/tools/passes.ts
CHANGED
|
@@ -67,7 +67,7 @@ export function registerPassTools(server: McpServer): void {
|
|
|
67
67
|
{
|
|
68
68
|
agent_id: z.string().describe("Agent ID (UUID, slug, or name)"),
|
|
69
69
|
pack_id: z.string().optional().describe("Specific pack key to buy, like 'starter' or 'growth'. If omitted and only one pack exists, it is selected automatically."),
|
|
70
|
-
pay_with: z.string().optional().describe("Payment method — wallet ID, chain name, or 'card'. Auto-detected if omitted."),
|
|
70
|
+
pay_with: z.string().optional().describe("Payment method — wallet ID, chain name, 'link', or 'card'. Auto-detected if omitted."),
|
|
71
71
|
confirmed: z.boolean().optional().describe("Set to true to confirm the purchase after seeing the quote."),
|
|
72
72
|
},
|
|
73
73
|
async ({ agent_id, pack_id, pay_with, confirmed }) => {
|
package/src/tools/run.ts
CHANGED
|
@@ -127,7 +127,7 @@ export function registerRunTools(server: McpServer): void {
|
|
|
127
127
|
{
|
|
128
128
|
agent_id: z.string().describe("Agent ID (UUID, slug, or name)"),
|
|
129
129
|
input: z.record(z.string(), z.unknown()).describe("Input payload for the agent"),
|
|
130
|
-
pay_with: z.string().trim().min(1).optional().describe("Payment method — wallet ID, chain name (tempo, base,
|
|
130
|
+
pay_with: z.string().trim().min(1).optional().describe("Payment method — wallet ID, chain name (tempo, base, solana), 'link', or 'card'. Auto-detected if omitted."),
|
|
131
131
|
confirmed: z.boolean().optional().describe("Set to true to confirm spending after seeing the price quote."),
|
|
132
132
|
},
|
|
133
133
|
async ({ agent_id, input, pay_with, confirmed }) => {
|
package/src/tools/solve.ts
CHANGED
|
@@ -159,7 +159,7 @@ export function registerSolveTools(server: McpServer): void {
|
|
|
159
159
|
.trim()
|
|
160
160
|
.min(1)
|
|
161
161
|
.optional()
|
|
162
|
-
.describe("Payment method — wallet ID, chain name (tempo, base,
|
|
162
|
+
.describe("Payment method — wallet ID, chain name (tempo, base, solana), 'link', or 'card'. Auto-detected if omitted."),
|
|
163
163
|
confirmed: z
|
|
164
164
|
.boolean()
|
|
165
165
|
.optional()
|
package/src/tools/wallet.ts
CHANGED
|
@@ -3,10 +3,15 @@ import { z } from "zod";
|
|
|
3
3
|
import {
|
|
4
4
|
getWallets,
|
|
5
5
|
getCardConfig,
|
|
6
|
+
getLinkConfig,
|
|
7
|
+
getPendingLinkSetup,
|
|
8
|
+
setPendingLinkSetup,
|
|
9
|
+
setLinkConfig,
|
|
6
10
|
setCardConfig,
|
|
7
11
|
addWallet,
|
|
8
12
|
getPendingCardSetupToken,
|
|
9
13
|
getSpendPolicy,
|
|
14
|
+
setDefaultPaymentMethod,
|
|
10
15
|
setSpendPolicy,
|
|
11
16
|
} from "../core/config.js";
|
|
12
17
|
import { getWalletAddress, isCardPaymentEnabled } from "../core/payments.js";
|
|
@@ -18,6 +23,12 @@ import {
|
|
|
18
23
|
getCardCapabilities,
|
|
19
24
|
pollCardSetup,
|
|
20
25
|
} from "../core/card-setup.js";
|
|
26
|
+
import {
|
|
27
|
+
getLinkCliAuthStatus,
|
|
28
|
+
listLinkPaymentMethods,
|
|
29
|
+
openLinkPaymentMethodAdd,
|
|
30
|
+
startLinkCliLogin,
|
|
31
|
+
} from "../core/link-cli.js";
|
|
21
32
|
import {
|
|
22
33
|
isOwsAvailable,
|
|
23
34
|
createOwsWallet,
|
|
@@ -34,6 +45,49 @@ function text(t: string) {
|
|
|
34
45
|
return { content: [{ type: "text" as const, text: t }] };
|
|
35
46
|
}
|
|
36
47
|
|
|
48
|
+
function formatLinkApprovalPrompt(pending: { verificationUrl: string; phrase: string }): string {
|
|
49
|
+
return [
|
|
50
|
+
"Approve Agent Wonderland in Link:",
|
|
51
|
+
"",
|
|
52
|
+
pending.verificationUrl,
|
|
53
|
+
"",
|
|
54
|
+
`Verification phrase: ${pending.phrase}`,
|
|
55
|
+
"",
|
|
56
|
+
"After approving, run wallet_setup({ action: \"add-link\" }) again and I will finish connecting the payment method.",
|
|
57
|
+
].join("\n");
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function isFreshLinkSetup(pending: { createdAt: string }): boolean {
|
|
61
|
+
const createdAt = Date.parse(pending.createdAt);
|
|
62
|
+
if (!Number.isFinite(createdAt)) return false;
|
|
63
|
+
return Date.now() - createdAt < 10 * 60 * 1000;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function formatPaymentSetupMenu(): string {
|
|
67
|
+
return [
|
|
68
|
+
"Set up a payment method:",
|
|
69
|
+
"",
|
|
70
|
+
"1. Link card/bank (recommended)",
|
|
71
|
+
" wallet_setup({ action: \"add-link\" })",
|
|
72
|
+
"",
|
|
73
|
+
"2. Tempo USDC",
|
|
74
|
+
" wallet_setup({ action: \"create\", chain: \"tempo\" })",
|
|
75
|
+
"",
|
|
76
|
+
"3. Base USDC",
|
|
77
|
+
" wallet_setup({ action: \"create\", chain: \"base\" })",
|
|
78
|
+
"",
|
|
79
|
+
"4. Solana USDC",
|
|
80
|
+
" wallet_setup({ action: \"create\", chain: \"solana\" })",
|
|
81
|
+
"",
|
|
82
|
+
"5. Import an existing wallet",
|
|
83
|
+
" wallet_setup({ action: \"import\", chain: \"base\", key: \"<private key>\" })",
|
|
84
|
+
].join("\n");
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function setDefaultCryptoPaymentMethod(chain: string): void {
|
|
88
|
+
setDefaultPaymentMethod(chain === "solana" ? "solana" : chain === "base" ? "base" : "tempo");
|
|
89
|
+
}
|
|
90
|
+
|
|
37
91
|
export function registerWalletTools(server: McpServer): void {
|
|
38
92
|
// ── wallet_status (extracted from check_wallet) ─────────────────
|
|
39
93
|
server.tool(
|
|
@@ -43,12 +97,13 @@ export function registerWalletTools(server: McpServer): void {
|
|
|
43
97
|
async () => {
|
|
44
98
|
const wallets = getWallets();
|
|
45
99
|
const card = getCardConfig();
|
|
100
|
+
const link = getLinkConfig();
|
|
46
101
|
const pendingCardSetupToken = getPendingCardSetupToken();
|
|
47
102
|
const consumerPrincipal = await getConsumerPrincipal();
|
|
48
103
|
|
|
49
|
-
if (wallets.length === 0 && !card && !pendingCardSetupToken) {
|
|
104
|
+
if (wallets.length === 0 && !card && !link && !pendingCardSetupToken) {
|
|
50
105
|
return text(
|
|
51
|
-
|
|
106
|
+
`No payment methods configured.\n\n${formatPaymentSetupMenu()}`,
|
|
52
107
|
);
|
|
53
108
|
}
|
|
54
109
|
|
|
@@ -95,6 +150,15 @@ export function registerWalletTools(server: McpServer): void {
|
|
|
95
150
|
}
|
|
96
151
|
}
|
|
97
152
|
|
|
153
|
+
if (link) {
|
|
154
|
+
const auth = await getLinkCliAuthStatus();
|
|
155
|
+
const label = link.label ? ` (${link.label})` : "";
|
|
156
|
+
lines.push(` Link${label}: ${link.paymentMethodId}`);
|
|
157
|
+
lines.push(auth.authenticated
|
|
158
|
+
? " Link CLI: authenticated"
|
|
159
|
+
: " Link CLI: not authenticated — run npx @stripe/link-cli auth login");
|
|
160
|
+
}
|
|
161
|
+
|
|
98
162
|
if (pendingCardSetupToken) {
|
|
99
163
|
lines.push(" Card setup: pending confirmation");
|
|
100
164
|
}
|
|
@@ -121,11 +185,11 @@ export function registerWalletTools(server: McpServer): void {
|
|
|
121
185
|
// ── wallet_setup ────────────────────────────────────────────────
|
|
122
186
|
server.tool(
|
|
123
187
|
"wallet_setup",
|
|
124
|
-
"Set up or manage
|
|
188
|
+
"Set up or manage an Agent Wonderland payment method. Use 'start' for a guided setup menu. Link card/bank is recommended for most users. 'create' makes a new crypto wallet (encrypted via OWS if available, otherwise plaintext — run 'enable-ows' to upgrade). 'import' takes an existing private key. 'enable-ows' installs the Open Wallet Standard native module for encrypted at-rest storage. Tempo/Base share one EVM key; Solana uses a separate ed25519 key. NEVER delete or rotate keys programmatically; direct users to edit ~/.agentwonderland/config.json or ~/.ows/ manually.",
|
|
125
189
|
{
|
|
126
190
|
action: z
|
|
127
|
-
.enum(["create", "import", "add-card", "remove-card", "enable-ows"])
|
|
128
|
-
.describe("'create' a wallet, 'import' an existing key, 'enable-ows'
|
|
191
|
+
.enum(["start", "create", "import", "add-card", "remove-card", "add-link", "remove-link", "enable-ows"])
|
|
192
|
+
.describe("'start' shows the guided payment setup menu, 'add-link' connects Link card/bank, 'create' makes a crypto wallet, 'import' imports an existing key, 'enable-ows' installs encrypted key storage"),
|
|
129
193
|
name: z
|
|
130
194
|
.string()
|
|
131
195
|
.optional()
|
|
@@ -138,8 +202,160 @@ export function registerWalletTools(server: McpServer): void {
|
|
|
138
202
|
),
|
|
139
203
|
chain: z.enum(["tempo", "base", "solana"]).optional()
|
|
140
204
|
.describe("Primary chain (default: tempo). Tempo/Base use a shared EVM wallet; Solana uses a separate OWS wallet."),
|
|
205
|
+
link_payment_method_id: z.string().optional()
|
|
206
|
+
.describe("Link payment method ID from `npx @stripe/link-cli payment-methods list`; used with action 'add-link'."),
|
|
207
|
+
link_payment_method: z.string().optional()
|
|
208
|
+
.describe("Friendly Link payment method selector, such as a card/bank name or last4; used with action 'add-link'."),
|
|
141
209
|
},
|
|
142
|
-
async ({ action, name, key, chain }) => {
|
|
210
|
+
async ({ action, name, key, chain, link_payment_method_id, link_payment_method }) => {
|
|
211
|
+
if (action === "start") {
|
|
212
|
+
return text(formatPaymentSetupMenu());
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// ── Link setup flow ──────────────────────────────────────
|
|
216
|
+
if (action === "add-link") {
|
|
217
|
+
let auth = await getLinkCliAuthStatus();
|
|
218
|
+
if (!auth.authenticated) {
|
|
219
|
+
const pending = getPendingLinkSetup();
|
|
220
|
+
if (pending && isFreshLinkSetup(pending)) {
|
|
221
|
+
return text(formatLinkApprovalPrompt(pending));
|
|
222
|
+
}
|
|
223
|
+
if (pending) {
|
|
224
|
+
setPendingLinkSetup(null);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
try {
|
|
228
|
+
const login = await startLinkCliLogin();
|
|
229
|
+
setPendingLinkSetup({
|
|
230
|
+
verificationUrl: login.verificationUrl,
|
|
231
|
+
phrase: login.phrase,
|
|
232
|
+
createdAt: new Date().toISOString(),
|
|
233
|
+
});
|
|
234
|
+
auth = await getLinkCliAuthStatus();
|
|
235
|
+
if (!auth.authenticated) {
|
|
236
|
+
return text(formatLinkApprovalPrompt({
|
|
237
|
+
verificationUrl: login.verificationUrl,
|
|
238
|
+
phrase: login.phrase,
|
|
239
|
+
}));
|
|
240
|
+
}
|
|
241
|
+
} catch (err) {
|
|
242
|
+
return text(
|
|
243
|
+
[
|
|
244
|
+
"Link authentication could not start from the MCP session.",
|
|
245
|
+
`Reason: ${err instanceof Error ? err.message : String(err)}`,
|
|
246
|
+
"",
|
|
247
|
+
"Run wallet_setup({ action: \"add-link\" }) again to retry.",
|
|
248
|
+
].join("\n"),
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
setPendingLinkSetup(null);
|
|
253
|
+
|
|
254
|
+
const explicitLinkSelector = link_payment_method_id ?? link_payment_method;
|
|
255
|
+
if (explicitLinkSelector) {
|
|
256
|
+
const methods = await listLinkPaymentMethods();
|
|
257
|
+
const normalizedSelector = explicitLinkSelector.toLowerCase().trim();
|
|
258
|
+
const exact = methods.find((method) => method.id === explicitLinkSelector);
|
|
259
|
+
const matches = exact
|
|
260
|
+
? [exact]
|
|
261
|
+
: methods.filter((method) => method.searchText?.includes(normalizedSelector));
|
|
262
|
+
if (matches.length === 0 && !link_payment_method_id) {
|
|
263
|
+
return text(
|
|
264
|
+
[
|
|
265
|
+
`No Link payment method matched "${explicitLinkSelector}".`,
|
|
266
|
+
"",
|
|
267
|
+
"Available methods:",
|
|
268
|
+
...methods.map((method) => ` ${method.label ?? method.id}${method.label ? ` (${method.id})` : ""}`),
|
|
269
|
+
].join("\n"),
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
if (matches.length > 1) {
|
|
273
|
+
return text(
|
|
274
|
+
[
|
|
275
|
+
`Multiple Link payment methods matched "${explicitLinkSelector}". Choose one:`,
|
|
276
|
+
...matches.map((method) => ` ${method.label ?? method.id}${method.label ? ` (${method.id})` : ""}`),
|
|
277
|
+
"",
|
|
278
|
+
"Then run:",
|
|
279
|
+
" wallet_setup({ action: \"add-link\", link_payment_method: \"<name or last4>\" })",
|
|
280
|
+
].join("\n"),
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
const selected = matches[0] ?? { id: link_payment_method_id!, label: name };
|
|
284
|
+
setLinkConfig({
|
|
285
|
+
paymentMethodId: selected.id,
|
|
286
|
+
label: name ?? selected.label,
|
|
287
|
+
});
|
|
288
|
+
const principal = await ensureConsumerPrincipal();
|
|
289
|
+
return text(
|
|
290
|
+
[
|
|
291
|
+
"Link payment method connected.",
|
|
292
|
+
` Payment method: ${selected.label ?? selected.id}${selected.label ? ` (${selected.id})` : ""}`,
|
|
293
|
+
` Consumer principal: ${principal}`,
|
|
294
|
+
"",
|
|
295
|
+
"Use pay_with: \"link\" for agent runs.",
|
|
296
|
+
].join("\n"),
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
let methods = await listLinkPaymentMethods();
|
|
301
|
+
if (methods.length === 0) {
|
|
302
|
+
try {
|
|
303
|
+
await openLinkPaymentMethodAdd();
|
|
304
|
+
methods = await listLinkPaymentMethods();
|
|
305
|
+
} catch (err) {
|
|
306
|
+
return text(
|
|
307
|
+
[
|
|
308
|
+
"No Link payment methods are available yet.",
|
|
309
|
+
`Reason: ${err instanceof Error ? err.message : String(err)}`,
|
|
310
|
+
"",
|
|
311
|
+
"Run wallet_setup({ action: \"add-link\" }) again after adding a payment method in Link.",
|
|
312
|
+
].join("\n"),
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
if (methods.length === 0) {
|
|
316
|
+
return text("Link payment-method setup was started. Run wallet_setup({ action: \"add-link\" }) again after adding a payment method in Link.");
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
if (methods.length === 1) {
|
|
321
|
+
const method = methods[0];
|
|
322
|
+
setLinkConfig({
|
|
323
|
+
paymentMethodId: method.id,
|
|
324
|
+
label: method.label,
|
|
325
|
+
});
|
|
326
|
+
const principal = await ensureConsumerPrincipal();
|
|
327
|
+
return text(
|
|
328
|
+
[
|
|
329
|
+
"Link payment method connected.",
|
|
330
|
+
` Payment method: ${method.id}${method.label ? ` (${method.label})` : ""}`,
|
|
331
|
+
` Consumer principal: ${principal}`,
|
|
332
|
+
"",
|
|
333
|
+
"Use pay_with: \"link\" for agent runs.",
|
|
334
|
+
].join("\n"),
|
|
335
|
+
);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
return text(
|
|
339
|
+
[
|
|
340
|
+
"Multiple Link payment methods found. Choose one:",
|
|
341
|
+
...methods.map((method) => ` ${method.label ?? method.id}${method.label ? ` (${method.id})` : ""}`),
|
|
342
|
+
"",
|
|
343
|
+
"Then run:",
|
|
344
|
+
" wallet_setup({ action: \"add-link\", link_payment_method: \"<name or last4>\" })",
|
|
345
|
+
].join("\n"),
|
|
346
|
+
);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
if (action === "remove-link") {
|
|
350
|
+
const existing = getLinkConfig();
|
|
351
|
+
if (!existing) {
|
|
352
|
+
return text("No Link payment method is currently connected.");
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
setLinkConfig(null);
|
|
356
|
+
return text(`Removed Link payment method ${existing.paymentMethodId}.`);
|
|
357
|
+
}
|
|
358
|
+
|
|
143
359
|
// ── Card setup flow ──────────────────────────────────────
|
|
144
360
|
if (action === "add-card") {
|
|
145
361
|
if (!isCardPaymentEnabled()) {
|
|
@@ -273,6 +489,7 @@ export function registerWalletTools(server: McpServer): void {
|
|
|
273
489
|
chainStatus = "Already linked to Agent Wonderland.";
|
|
274
490
|
}
|
|
275
491
|
}
|
|
492
|
+
setDefaultCryptoPaymentMethod(defaultCh);
|
|
276
493
|
const principal = await ensureConsumerPrincipal();
|
|
277
494
|
return text([
|
|
278
495
|
"Found existing OWS wallet:",
|
|
@@ -307,6 +524,7 @@ export function registerWalletTools(server: McpServer): void {
|
|
|
307
524
|
defaultChain: "solana",
|
|
308
525
|
label: walletName,
|
|
309
526
|
});
|
|
527
|
+
setDefaultCryptoPaymentMethod("solana");
|
|
310
528
|
const principal = await ensureConsumerPrincipal();
|
|
311
529
|
const owsNudge = platformSupportsOws()
|
|
312
530
|
? "\n\n⚠ Key stored in plaintext at ~/.agentwonderland/config.json. Run wallet_setup({ action: \"enable-ows\" }) to install encrypted at-rest storage — takes ~30s."
|
|
@@ -336,6 +554,7 @@ export function registerWalletTools(server: McpServer): void {
|
|
|
336
554
|
defaultChain: defaultCh === "solana" ? "tempo" : defaultCh,
|
|
337
555
|
label: walletName,
|
|
338
556
|
});
|
|
557
|
+
setDefaultCryptoPaymentMethod(defaultCh);
|
|
339
558
|
const principal = await ensureConsumerPrincipal();
|
|
340
559
|
const owsNudge = platformSupportsOws()
|
|
341
560
|
? "\n\n⚠ Key stored in plaintext at ~/.agentwonderland/config.json. Run wallet_setup({ action: \"enable-ows\" }) to install encrypted at-rest storage — takes ~30s."
|
|
@@ -367,6 +586,7 @@ export function registerWalletTools(server: McpServer): void {
|
|
|
367
586
|
defaultChain: defaultCh,
|
|
368
587
|
label: walletName,
|
|
369
588
|
});
|
|
589
|
+
setDefaultCryptoPaymentMethod(defaultCh);
|
|
370
590
|
const principal = await ensureConsumerPrincipal();
|
|
371
591
|
|
|
372
592
|
return text(
|
|
@@ -400,6 +620,7 @@ export function registerWalletTools(server: McpServer): void {
|
|
|
400
620
|
defaultChain: defaultCh,
|
|
401
621
|
label: walletName,
|
|
402
622
|
});
|
|
623
|
+
setDefaultCryptoPaymentMethod(defaultCh);
|
|
403
624
|
const principal = await ensureConsumerPrincipal();
|
|
404
625
|
|
|
405
626
|
return text(
|
|
@@ -432,6 +653,7 @@ export function registerWalletTools(server: McpServer): void {
|
|
|
432
653
|
defaultChain: "solana",
|
|
433
654
|
label: walletName,
|
|
434
655
|
});
|
|
656
|
+
setDefaultCryptoPaymentMethod("solana");
|
|
435
657
|
const principal = await ensureConsumerPrincipal();
|
|
436
658
|
const owsNudge = platformSupportsOws()
|
|
437
659
|
? "\n\n⚠ Key stored in plaintext at ~/.agentwonderland/config.json. Run wallet_setup({ action: \"enable-ows\" }) to install encrypted at-rest storage — takes ~30s."
|
|
@@ -472,6 +694,7 @@ export function registerWalletTools(server: McpServer): void {
|
|
|
472
694
|
defaultChain: defaultCh,
|
|
473
695
|
label: walletName,
|
|
474
696
|
});
|
|
697
|
+
setDefaultCryptoPaymentMethod(defaultCh);
|
|
475
698
|
const principal = await ensureConsumerPrincipal();
|
|
476
699
|
|
|
477
700
|
const owsNudge = platformSupportsOws()
|