@agentwonderland/mcp 0.1.45 → 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.
Files changed (38) hide show
  1. package/dist/core/__tests__/payments.test.js +55 -1
  2. package/dist/core/__tests__/principal.test.js +18 -0
  3. package/dist/core/config.d.ts +16 -0
  4. package/dist/core/config.js +37 -0
  5. package/dist/core/link-cli.d.ts +27 -0
  6. package/dist/core/link-cli.js +259 -0
  7. package/dist/core/mpp-client.d.ts +13 -1
  8. package/dist/core/mpp-client.js +45 -2
  9. package/dist/core/payments.js +135 -10
  10. package/dist/core/principal.js +2 -2
  11. package/dist/core/version.d.ts +1 -1
  12. package/dist/core/version.js +1 -1
  13. package/dist/index.js +6 -4
  14. package/dist/tools/__tests__/search.test.d.ts +1 -0
  15. package/dist/tools/__tests__/search.test.js +66 -0
  16. package/dist/tools/__tests__/wallet.test.js +153 -0
  17. package/dist/tools/passes.js +1 -1
  18. package/dist/tools/run.js +1 -1
  19. package/dist/tools/search.js +33 -0
  20. package/dist/tools/solve.js +1 -1
  21. package/dist/tools/wallet.js +195 -7
  22. package/package.json +1 -1
  23. package/src/core/__tests__/payments.test.ts +78 -1
  24. package/src/core/__tests__/principal.test.ts +23 -0
  25. package/src/core/config.ts +56 -0
  26. package/src/core/link-cli.ts +300 -0
  27. package/src/core/mpp-client.ts +69 -2
  28. package/src/core/payments.ts +153 -11
  29. package/src/core/principal.ts +2 -2
  30. package/src/core/version.ts +1 -1
  31. package/src/index.ts +6 -4
  32. package/src/tools/__tests__/search.test.ts +78 -0
  33. package/src/tools/__tests__/wallet.test.ts +190 -0
  34. package/src/tools/passes.ts +1 -1
  35. package/src/tools/run.ts +1 -1
  36. package/src/tools/search.ts +40 -0
  37. package/src/tools/solve.ts +1 -1
  38. package/src/tools/wallet.ts +229 -6
@@ -0,0 +1,78 @@
1
+ import { beforeEach, describe, expect, it, vi } from "vitest";
2
+
3
+ const mockApiGet = vi.fn();
4
+ const mockGetAcceptedPaymentMethods = vi.fn();
5
+ const mockIsFavorite = vi.fn();
6
+
7
+ vi.mock("../../core/api-client.js", () => ({
8
+ apiGet: mockApiGet,
9
+ }));
10
+
11
+ vi.mock("../../core/payments.js", () => ({
12
+ getAcceptedPaymentMethods: mockGetAcceptedPaymentMethods,
13
+ }));
14
+
15
+ vi.mock("../../core/config.js", () => ({
16
+ isFavorite: mockIsFavorite,
17
+ }));
18
+
19
+ function flattenToolText(result: unknown): string {
20
+ const content = (result as { content?: Array<{ type?: string; text?: string }> })?.content ?? [];
21
+ return content
22
+ .filter((item) => item?.type === "text")
23
+ .map((item) => item.text ?? "")
24
+ .join("\n\n");
25
+ }
26
+
27
+ function makeServerHarness() {
28
+ const handlers = new Map<string, (args: Record<string, unknown>) => Promise<unknown>>();
29
+ return {
30
+ handlers,
31
+ server: {
32
+ tool(name: string, _description: string, _schema: unknown, handler: (args: Record<string, unknown>) => Promise<unknown>) {
33
+ handlers.set(name, handler);
34
+ },
35
+ },
36
+ };
37
+ }
38
+
39
+ describe("search_agents MCP tool", () => {
40
+ beforeEach(() => {
41
+ vi.resetModules();
42
+ vi.clearAllMocks();
43
+ mockGetAcceptedPaymentMethods.mockReturnValue([]);
44
+ mockIsFavorite.mockReturnValue(false);
45
+ });
46
+
47
+ it("sorts popularity by live completed job stats after API enrichment", async () => {
48
+ mockApiGet.mockResolvedValueOnce([
49
+ {
50
+ id: "agent-low",
51
+ name: "Low History",
52
+ slug: "low-history",
53
+ pricePerRunUsd: "0.100000",
54
+ stats: { completedJobs: 2, avgRating: null, ratingCount: 0 },
55
+ },
56
+ {
57
+ id: "agent-high",
58
+ name: "High History",
59
+ slug: "high-history",
60
+ pricePerRunUsd: "0.100000",
61
+ stats: { completedJobs: 6, avgRating: null, ratingCount: 0 },
62
+ },
63
+ ]);
64
+
65
+ const { registerSearchTools } = await import("../search.js");
66
+ const harness = makeServerHarness();
67
+ registerSearchTools(harness.server as never);
68
+
69
+ const search = harness.handlers.get("search_agents");
70
+ expect(search).toBeDefined();
71
+
72
+ const result = await search!({ query: "stock research", limit: 2, sort: "popularity" });
73
+ const output = flattenToolText(result);
74
+
75
+ expect(output.indexOf("High History")).toBeLessThan(output.indexOf("Low History"));
76
+ expect(output).toContain("High History (high-history) ☆☆☆☆☆ 6 jobs");
77
+ });
78
+ });
@@ -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
  });
@@ -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, etc.), or 'card'. Auto-detected if omitted."),
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 }) => {
@@ -10,6 +10,27 @@ function text(t: string) {
10
10
  return { content: [{ type: "text" as const, text: t }] };
11
11
  }
12
12
 
13
+ function completedJobs(agent: AgentRecord): number {
14
+ const stats = agent.stats as { completedJobs?: number } | undefined;
15
+ return stats?.completedJobs ?? agent.totalExecutions ?? 0;
16
+ }
17
+
18
+ function averageRating(agent: AgentRecord): number {
19
+ const stats = agent.stats as { avgRating?: number | null } | undefined;
20
+ return agent.avgRating ?? stats?.avgRating ?? 0;
21
+ }
22
+
23
+ function ratingCount(agent: AgentRecord): number {
24
+ const stats = agent.stats as { ratingCount?: number } | undefined;
25
+ const topLevel = typeof agent.ratingCount === "number" ? agent.ratingCount : undefined;
26
+ return topLevel ?? stats?.ratingCount ?? 0;
27
+ }
28
+
29
+ function price(agent: AgentRecord): number {
30
+ const parsed = Number.parseFloat(agent.pricePerRunUsd ?? "0");
31
+ return Number.isFinite(parsed) ? parsed : 0;
32
+ }
33
+
13
34
  export function registerSearchTools(server: McpServer): void {
14
35
  server.tool(
15
36
  "search_agents",
@@ -71,6 +92,25 @@ export function registerSearchTools(server: McpServer): void {
71
92
  });
72
93
  }
73
94
 
95
+ // The API enriches list rows with live stats after registry search. Do a
96
+ // final local sort for stat-based MCP views so displayed jobs/ratings and
97
+ // result order cannot disagree while registry aggregates catch up.
98
+ if (sort === "popularity") {
99
+ agents = [...agents].sort((a, b) =>
100
+ completedJobs(b) - completedJobs(a) ||
101
+ ratingCount(b) - ratingCount(a) ||
102
+ averageRating(b) - averageRating(a),
103
+ );
104
+ } else if (sort === "rating") {
105
+ agents = [...agents].sort((a, b) =>
106
+ averageRating(b) - averageRating(a) ||
107
+ ratingCount(b) - ratingCount(a) ||
108
+ completedJobs(b) - completedJobs(a),
109
+ );
110
+ } else if (sort === "price") {
111
+ agents = [...agents].sort((a, b) => price(a) - price(b));
112
+ }
113
+
74
114
  // Trim to requested limit after filtering
75
115
  agents = agents.slice(0, requestedLimit);
76
116
 
@@ -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, etc.), or 'card'. Auto-detected if omitted."),
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()