@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
@@ -9,7 +9,7 @@
9
9
  * Users can configure multiple wallets with different chains and select
10
10
  * which to use per-request via `--pay-with <wallet-id|chain|card>`.
11
11
  */
12
- import { getConfig, getWallets, getDefaultWallet, getCardConfig, resolveWalletAndChain, getApiUrl, } from "./config.js";
12
+ import { getConfig, getWallets, getDefaultWallet, getCardConfig, getLinkConfig, resolveWalletAndChain, getApiUrl, } from "./config.js";
13
13
  // Feature flag: disable card payment for launch. Flip to true once Stripe
14
14
  // approves the live SPT issuer. Config/card state is still persisted so this
15
15
  // can be re-enabled without reconfiguring wallets.
@@ -24,12 +24,30 @@ const REGISTRY_METHOD_MAP = {
24
24
  base: "base_usdc",
25
25
  solana: "solana_usdc",
26
26
  card: "stripe_card",
27
+ link: "stripe_card",
28
+ };
29
+ const DEFAULT_LINK_APPROVAL_LIMIT_CENTS = 10_000;
30
+ const ACCEPTED_PAYMENT_ALIASES = {
31
+ tempo: ["tempo_usdc", "tempo"],
32
+ base: ["base_usdc", "base"],
33
+ solana: ["solana_usdc", "solana"],
34
+ card: ["stripe_card", "card"],
35
+ link: ["stripe_card", "card", "link"],
36
+ };
37
+ const DISCOVERY_PAYMENT_ALIASES = {
38
+ tempo: ["tempo_usdc"],
39
+ base: ["base_usdc"],
40
+ solana: ["solana_usdc"],
41
+ card: ["stripe_card", "card"],
42
+ link: ["stripe_card", "card"],
27
43
  };
28
44
  const METHOD_REGISTRY_MAP = {
29
45
  tempo_usdc: "tempo",
30
46
  base_usdc: "base",
31
47
  solana_usdc: "solana",
32
48
  stripe_card: "card",
49
+ card: "card",
50
+ link: "link",
33
51
  };
34
52
  // ── Helpers ─────────────────────────────────────────────────────
35
53
  function normalizeKey(key) {
@@ -44,6 +62,12 @@ function cardCacheKey() {
44
62
  return null;
45
63
  return `card:${getApiUrl()}:${card.consumerToken}:${card.paymentMethodId ?? ""}`;
46
64
  }
65
+ function linkCacheKey() {
66
+ const link = getLinkConfig();
67
+ if (!link)
68
+ return null;
69
+ return `link:${getApiUrl()}:${link.paymentMethodId}`;
70
+ }
47
71
  function clearStaleCardCache(activeKey) {
48
72
  for (const key of fetchCache.keys()) {
49
73
  if (key.startsWith("card:") && key !== activeKey) {
@@ -51,6 +75,13 @@ function clearStaleCardCache(activeKey) {
51
75
  }
52
76
  }
53
77
  }
78
+ function clearStaleLinkCache(activeKey) {
79
+ for (const key of fetchCache.keys()) {
80
+ if (key.startsWith("link:") && key !== activeKey) {
81
+ fetchCache.delete(key);
82
+ }
83
+ }
84
+ }
54
85
  // ── Per-protocol initializers ───────────────────────────────────
55
86
  async function initEvmMppForChain(wallet, chain) {
56
87
  const { Mppx } = await import("./mpp-client.js");
@@ -127,6 +158,60 @@ async function initCard() {
127
158
  return null;
128
159
  }
129
160
  }
161
+ function formatMinorCurrencyAmount(currency, amount) {
162
+ return `${currency.toUpperCase()} ${(Number(amount) / 100).toFixed(2)}`;
163
+ }
164
+ function getLinkApprovalLimitAmount(actualAmount) {
165
+ const actualAmountCents = Number(actualAmount);
166
+ const configuredLimit = Number(process.env.AGENTWONDERLAND_LINK_APPROVAL_LIMIT_CENTS);
167
+ const defaultLimit = Number.isFinite(configuredLimit) && configuredLimit > 0
168
+ ? Math.floor(configuredLimit)
169
+ : DEFAULT_LINK_APPROVAL_LIMIT_CENTS;
170
+ return String(Math.max(actualAmountCents, defaultLimit));
171
+ }
172
+ function buildLinkApprovalContext(params) {
173
+ const amountText = formatMinorCurrencyAmount(params.currency, params.amount);
174
+ const approvalAmountText = formatMinorCurrencyAmount(params.currency, params.approvalAmount);
175
+ const agent = params.metadata?.agent_id ? ` Agent ID: ${params.metadata.agent_id}.` : "";
176
+ const job = params.metadata?.job_id ? ` Job ID: ${params.metadata.job_id}.` : "";
177
+ return (`Approve up to ${approvalAmountText} for Agent Wonderland machine payments. ` +
178
+ `This specific user-confirmed agent run is quoted at ${amountText}; the Agent Wonderland gateway will charge the exact quote for this request, not the full approval limit unless the quote itself is that amount.${agent}${job}`);
179
+ }
180
+ async function initLink() {
181
+ const linkConfig = getLinkConfig();
182
+ if (!linkConfig)
183
+ return null;
184
+ try {
185
+ const { Mppx, stripe } = await import("./mpp-client.js");
186
+ const { createLinkSharedPaymentToken } = await import("./link-cli.js");
187
+ const mppx = Mppx.create({
188
+ methods: [stripe({
189
+ paymentMethod: linkConfig.paymentMethodId,
190
+ createToken: async (params) => {
191
+ const approvalAmount = getLinkApprovalLimitAmount(params.amount);
192
+ return createLinkSharedPaymentToken({
193
+ amount: approvalAmount,
194
+ currency: params.currency,
195
+ context: buildLinkApprovalContext({
196
+ amount: params.amount,
197
+ approvalAmount,
198
+ currency: params.currency,
199
+ metadata: params.metadata,
200
+ }),
201
+ expiresAt: params.expiresAt,
202
+ networkId: params.networkId,
203
+ paymentMethodId: linkConfig.paymentMethodId,
204
+ });
205
+ },
206
+ })],
207
+ polyfill: false,
208
+ });
209
+ return mppx.fetch.bind(mppx);
210
+ }
211
+ catch {
212
+ return null;
213
+ }
214
+ }
130
215
  /**
131
216
  * Initialize a payment-aware fetch for a given wallet + chain.
132
217
  */
@@ -151,6 +236,21 @@ function initFailureMessage(method, wallet, chain, err) {
151
236
  * @param method - wallet ID, chain name, or "card". Omit for auto-detection.
152
237
  */
153
238
  export async function getPaymentFetch(method) {
239
+ if (method === "link") {
240
+ const ck = linkCacheKey();
241
+ clearStaleLinkCache(ck ?? undefined);
242
+ if (!ck) {
243
+ throw new Error('Payment method "link" is not configured. Run wallet_setup({ action: "add-link" }) after logging into Link.');
244
+ }
245
+ if (fetchCache.has(ck))
246
+ return fetchCache.get(ck);
247
+ const pf = await initLink();
248
+ if (pf) {
249
+ fetchCache.set(ck, pf);
250
+ return pf;
251
+ }
252
+ throw new Error('Payment method "link" failed to initialize. Check Link CLI auth with: npx @stripe/link-cli auth status');
253
+ }
154
254
  // Card payment
155
255
  if (method === "card") {
156
256
  if (!ENABLE_CARD_PAYMENT) {
@@ -196,6 +296,25 @@ export async function getPaymentFetch(method) {
196
296
  const configured = getConfiguredMethods();
197
297
  const defaultMethod = getConfig().defaultPaymentMethod;
198
298
  for (const m of configured) {
299
+ if (m === "link") {
300
+ const ck = linkCacheKey();
301
+ clearStaleLinkCache(ck ?? undefined);
302
+ if (!ck)
303
+ continue;
304
+ if (fetchCache.has(ck))
305
+ return fetchCache.get(ck);
306
+ const pf = await initLink();
307
+ if (pf) {
308
+ fetchCache.set(ck, pf);
309
+ return pf;
310
+ }
311
+ if (m === defaultMethod) {
312
+ const others = configured.filter((x) => x !== m);
313
+ const altText = others.length > 0 ? ` Available alternatives: ${others.join(", ")}` : "";
314
+ throw new Error(`Link payment failed to initialize. Check Link CLI auth with wallet_status.${altText}`);
315
+ }
316
+ continue;
317
+ }
199
318
  if (m === "card") {
200
319
  const ck = cardCacheKey();
201
320
  clearStaleCardCache(ck ?? undefined);
@@ -285,6 +404,9 @@ export function getConfiguredMethods() {
285
404
  if (ENABLE_CARD_PAYMENT && getCardConfig()) {
286
405
  methods.push("card");
287
406
  }
407
+ if (getLinkConfig()) {
408
+ methods.push("link");
409
+ }
288
410
  // Respect defaultPaymentMethod — move it to front of list (ignore card when disabled)
289
411
  const defaultMethod = getConfig().defaultPaymentMethod;
290
412
  if (defaultMethod && (defaultMethod !== "card" || ENABLE_CARD_PAYMENT)) {
@@ -303,6 +425,8 @@ export function getConfiguredMethods() {
303
425
  export function normalizePaymentMethod(method) {
304
426
  if (method === "card")
305
427
  return "card";
428
+ if (method === "link")
429
+ return "link";
306
430
  const resolved = resolveWalletAndChain(method);
307
431
  return resolved?.chain ?? null;
308
432
  }
@@ -315,6 +439,7 @@ export function paymentMethodDisplayName(method) {
315
439
  case "base": return "Base USDC";
316
440
  case "solana": return "Solana USDC";
317
441
  case "card": return "Card";
442
+ case "link": return "Link";
318
443
  default: return method;
319
444
  }
320
445
  }
@@ -325,9 +450,8 @@ export function paymentMethodDisplayName(method) {
325
450
  */
326
451
  export function getAcceptedPaymentMethods() {
327
452
  const methods = getConfiguredMethods();
328
- return methods
329
- .map((m) => REGISTRY_METHOD_MAP[m])
330
- .filter(Boolean);
453
+ return [...new Set(methods
454
+ .flatMap((m) => DISCOVERY_PAYMENT_ALIASES[m] ?? [REGISTRY_METHOD_MAP[m]].filter(Boolean)))];
331
455
  }
332
456
  export function toRegistryPaymentMethod(method) {
333
457
  const normalized = normalizePaymentMethod(method);
@@ -340,16 +464,17 @@ export function getCompatiblePaymentMethods(agent, configuredMethods = getConfig
340
464
  if (!Array.isArray(acceptedPayments) || acceptedPayments.length === 0) {
341
465
  return [...configuredMethods];
342
466
  }
343
- const acceptedMethods = new Set(acceptedPayments
344
- .map((payment) => METHOD_REGISTRY_MAP[payment])
345
- .filter(Boolean));
346
- return configuredMethods.filter((method) => acceptedMethods.has(method));
467
+ const acceptedRegistryMethods = new Set(acceptedPayments);
468
+ return configuredMethods.filter((method) => {
469
+ const aliases = ACCEPTED_PAYMENT_ALIASES[method] ?? [REGISTRY_METHOD_MAP[method]].filter(Boolean);
470
+ return aliases.some((alias) => acceptedRegistryMethods.has(alias));
471
+ });
347
472
  }
348
473
  /**
349
474
  * Check whether any payment method is configured.
350
475
  */
351
476
  export function hasWalletConfigured() {
352
- return getWallets().length > 0 || getCardConfig() !== null;
477
+ return getWallets().length > 0 || getCardConfig() !== null || getLinkConfig() !== null;
353
478
  }
354
479
  /**
355
480
  * Get address for a specific method, or the first configured one.
@@ -357,7 +482,7 @@ export function hasWalletConfigured() {
357
482
  export async function getWalletAddress(method) {
358
483
  let chain;
359
484
  let wallet;
360
- if (method && method !== "card") {
485
+ if (method && method !== "card" && method !== "link") {
361
486
  const resolved = resolveWalletAndChain(method);
362
487
  wallet = resolved?.wallet;
363
488
  chain = resolved?.chain;
@@ -106,7 +106,7 @@ export async function getBaseRebatePrincipal() {
106
106
  return (await principalForChain("base")) ?? principalForChain("tempo");
107
107
  }
108
108
  export async function getConsumerPrincipalForMethod(method) {
109
- if (!method || method === "card") {
109
+ if (!method || method === "card" || method === "link") {
110
110
  return getConsumerPrincipal();
111
111
  }
112
112
  const resolved = resolveWalletAndChain(method);
@@ -129,7 +129,7 @@ export async function ensureConsumerPrincipalForMethod(method) {
129
129
  const existing = await getConsumerPrincipalForMethod(method);
130
130
  if (existing)
131
131
  return existing;
132
- if (method && method !== "card") {
132
+ if (method && method !== "card" && method !== "link") {
133
133
  throw new Error(`Could not derive a consumer principal for payment method "${method}". ` +
134
134
  "Check wallet_status and confirm that chain is configured for the active wallet.");
135
135
  }
@@ -1 +1 @@
1
- export declare const MCP_PACKAGE_VERSION = "0.1.44";
1
+ export declare const MCP_PACKAGE_VERSION = "0.1.47";
@@ -1 +1 @@
1
- export const MCP_PACKAGE_VERSION = "0.1.44";
1
+ export const MCP_PACKAGE_VERSION = "0.1.47";
package/dist/index.js CHANGED
@@ -44,11 +44,12 @@ export async function startMcpServer() {
44
44
  "3. After a successful run, rate_agent() and optionally tip_agent() if the result was useful.",
45
45
  "4. Use list_jobs() to recover state across sessions (it checks every configured wallet).",
46
46
  "",
47
- "PAYMENT (crypto-only right now):",
48
- "- Supported rails: Tempo USDC, Base USDC, Solana USDC. Card is temporarily disabled pending Stripe SPT approval.",
47
+ "PAYMENT:",
48
+ "- Supported rails: Tempo USDC, Base USDC, Solana USDC, and local Link SPT via @stripe/link-cli.",
49
+ "- Card is temporarily disabled pending Stripe SPT approval.",
49
50
  "- Tempo and Base share one EVM wallet key. Solana uses a separate ed25519 key. One OWS wallet can manage both.",
50
51
  "- If pay_with is omitted, the MCP auto-selects a compatible configured rail. Pass pay_with explicitly",
51
- " (tempo | base | solana | wallet-id) for deterministic behavior.",
52
+ " (tempo | base | solana | link | wallet-id) for deterministic behavior.",
52
53
  "- Payment is automatic: on a 402 challenge the MCP signs on-chain, submits, then retries. Failed runs are refunded.",
53
54
  "- If a specific rail fails, surface the real reason — do NOT silently retry with a different method.",
54
55
  "- Headless/automation: set wallet_set_policy() to cap max_per_tx and max_per_day so a runaway loop can't drain funds.",
@@ -63,7 +64,8 @@ export async function startMcpServer() {
63
64
  "",
64
65
  "WALLET HYGIENE:",
65
66
  "- wallet_status shows per-chain USDC balance and the active network (mainnet vs testnet).",
66
- "- To create or import a crypto wallet: wallet_setup({ action: \"create\" }) or { action: \"import\", key }.",
67
+ "- To set up payments: wallet_setup({ action: \"start\" }). Link card/bank is recommended for most users.",
68
+ "- To create or import a crypto wallet directly: wallet_setup({ action: \"create\" }) or { action: \"import\", key }.",
67
69
  "- NEVER delete or rotate keys programmatically. Direct users to edit ~/.agentwonderland/config.json or ~/.ows/ manually.",
68
70
  ].join("\n"),
69
71
  });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,66 @@
1
+ import { beforeEach, describe, expect, it, vi } from "vitest";
2
+ const mockApiGet = vi.fn();
3
+ const mockGetAcceptedPaymentMethods = vi.fn();
4
+ const mockIsFavorite = vi.fn();
5
+ vi.mock("../../core/api-client.js", () => ({
6
+ apiGet: mockApiGet,
7
+ }));
8
+ vi.mock("../../core/payments.js", () => ({
9
+ getAcceptedPaymentMethods: mockGetAcceptedPaymentMethods,
10
+ }));
11
+ vi.mock("../../core/config.js", () => ({
12
+ isFavorite: mockIsFavorite,
13
+ }));
14
+ function flattenToolText(result) {
15
+ const content = result?.content ?? [];
16
+ return content
17
+ .filter((item) => item?.type === "text")
18
+ .map((item) => item.text ?? "")
19
+ .join("\n\n");
20
+ }
21
+ function makeServerHarness() {
22
+ const handlers = new Map();
23
+ return {
24
+ handlers,
25
+ server: {
26
+ tool(name, _description, _schema, handler) {
27
+ handlers.set(name, handler);
28
+ },
29
+ },
30
+ };
31
+ }
32
+ describe("search_agents MCP tool", () => {
33
+ beforeEach(() => {
34
+ vi.resetModules();
35
+ vi.clearAllMocks();
36
+ mockGetAcceptedPaymentMethods.mockReturnValue([]);
37
+ mockIsFavorite.mockReturnValue(false);
38
+ });
39
+ it("sorts popularity by live completed job stats after API enrichment", async () => {
40
+ mockApiGet.mockResolvedValueOnce([
41
+ {
42
+ id: "agent-low",
43
+ name: "Low History",
44
+ slug: "low-history",
45
+ pricePerRunUsd: "0.100000",
46
+ stats: { completedJobs: 2, avgRating: null, ratingCount: 0 },
47
+ },
48
+ {
49
+ id: "agent-high",
50
+ name: "High History",
51
+ slug: "high-history",
52
+ pricePerRunUsd: "0.100000",
53
+ stats: { completedJobs: 6, avgRating: null, ratingCount: 0 },
54
+ },
55
+ ]);
56
+ const { registerSearchTools } = await import("../search.js");
57
+ const harness = makeServerHarness();
58
+ registerSearchTools(harness.server);
59
+ const search = harness.handlers.get("search_agents");
60
+ expect(search).toBeDefined();
61
+ const result = await search({ query: "stock research", limit: 2, sort: "popularity" });
62
+ const output = flattenToolText(result);
63
+ expect(output.indexOf("High History")).toBeLessThan(output.indexOf("Low History"));
64
+ expect(output).toContain("High History (high-history) ☆☆☆☆☆ 6 jobs");
65
+ });
66
+ });
@@ -3,8 +3,11 @@ const state = vi.hoisted(() => ({
3
3
  wallets: [],
4
4
  addedWallets: [],
5
5
  card: null,
6
+ link: null,
7
+ pendingLinkSetup: null,
6
8
  pendingCardSetupToken: null,
7
9
  spendPolicies: {},
10
+ defaultPaymentMethod: undefined,
8
11
  consumerPrincipal: "did:pkh:eip155:8453:0xabc",
9
12
  owsAvailable: true,
10
13
  owsWallets: [],
@@ -30,6 +33,12 @@ const state = vi.hoisted(() => ({
30
33
  pollCardSetupCalls: [],
31
34
  pollCardSetupResult: null,
32
35
  setCardConfigCalls: [],
36
+ setLinkConfigCalls: [],
37
+ setPendingLinkSetupCalls: [],
38
+ linkAuthenticated: false,
39
+ linkPaymentMethods: [],
40
+ linkLoginCalls: 0,
41
+ linkPaymentMethodAddCalls: 0,
33
42
  }));
34
43
  vi.mock("../../core/config.js", () => ({
35
44
  addWallet: (wallet) => {
@@ -43,6 +52,8 @@ vi.mock("../../core/config.js", () => ({
43
52
  }
44
53
  },
45
54
  getCardConfig: () => state.card,
55
+ getLinkConfig: () => state.link,
56
+ getPendingLinkSetup: () => state.pendingLinkSetup,
46
57
  getPendingCardSetupToken: () => state.pendingCardSetupToken,
47
58
  getSpendPolicy: (walletId) => state.spendPolicies[walletId] ?? null,
48
59
  getWallets: () => state.wallets,
@@ -50,6 +61,17 @@ vi.mock("../../core/config.js", () => ({
50
61
  state.card = card;
51
62
  state.setCardConfigCalls.push(card);
52
63
  },
64
+ setDefaultPaymentMethod: (method) => {
65
+ state.defaultPaymentMethod = method;
66
+ },
67
+ setLinkConfig: (link) => {
68
+ state.link = link;
69
+ state.setLinkConfigCalls.push(link);
70
+ },
71
+ setPendingLinkSetup: (pending) => {
72
+ state.pendingLinkSetup = pending;
73
+ state.setPendingLinkSetupCalls.push(pending);
74
+ },
53
75
  setSpendPolicy: (walletId, policy) => {
54
76
  state.spendPolicies[walletId] = policy;
55
77
  },
@@ -69,6 +91,22 @@ vi.mock("../../core/card-setup.js", () => ({
69
91
  return state.pollCardSetupResult;
70
92
  },
71
93
  }));
94
+ vi.mock("../../core/link-cli.js", () => ({
95
+ getLinkCliAuthStatus: async () => ({
96
+ authenticated: state.linkAuthenticated,
97
+ }),
98
+ listLinkPaymentMethods: async () => state.linkPaymentMethods,
99
+ openLinkPaymentMethodAdd: async () => {
100
+ state.linkPaymentMethodAddCalls++;
101
+ },
102
+ startLinkCliLogin: async () => {
103
+ state.linkLoginCalls++;
104
+ return {
105
+ verificationUrl: "https://app.link.com/device/setup?code=test-link-code",
106
+ phrase: "test-link-code",
107
+ };
108
+ },
109
+ }));
72
110
  vi.mock("../../core/ows-adapter.js", () => ({
73
111
  createOwsWallet: async (name, chain) => {
74
112
  state.createdWalletCalls.push({ name, chain });
@@ -90,8 +128,11 @@ function resetState() {
90
128
  state.wallets = [];
91
129
  state.addedWallets = [];
92
130
  state.card = null;
131
+ state.link = null;
132
+ state.pendingLinkSetup = null;
93
133
  state.pendingCardSetupToken = null;
94
134
  state.spendPolicies = {};
135
+ state.defaultPaymentMethod = undefined;
95
136
  state.consumerPrincipal = "did:pkh:eip155:8453:0xabc";
96
137
  state.owsAvailable = true;
97
138
  state.owsWallets = [];
@@ -117,6 +158,12 @@ function resetState() {
117
158
  state.pollCardSetupCalls = [];
118
159
  state.pollCardSetupResult = null;
119
160
  state.setCardConfigCalls = [];
161
+ state.setLinkConfigCalls = [];
162
+ state.setPendingLinkSetupCalls = [];
163
+ state.linkAuthenticated = false;
164
+ state.linkPaymentMethods = [];
165
+ state.linkLoginCalls = 0;
166
+ state.linkPaymentMethodAddCalls = 0;
120
167
  }
121
168
  async function getWalletSetupTool() {
122
169
  const tools = new Map();
@@ -158,11 +205,40 @@ describe("wallet_setup tool", () => {
158
205
  label: "launch-wallet",
159
206
  },
160
207
  ]);
208
+ expect(state.defaultPaymentMethod).toBe("base");
161
209
  expect(text).toContain("Wallet created [encrypted]:");
162
210
  expect(text).toContain("Address: 0x1111111111111111111111111111111111111111");
163
211
  expect(text).toContain("Chains: tempo, base");
164
212
  expect(text).toContain("Consumer principal: did:pkh:eip155:8453:0xabc");
165
213
  });
214
+ it("shows a guided payment setup menu", async () => {
215
+ const walletSetup = await getWalletSetupTool();
216
+ const result = await walletSetup({ action: "start" });
217
+ const text = flattenText(result);
218
+ expect(text).toContain("Set up a payment method:");
219
+ expect(text).toContain("Link card/bank (recommended)");
220
+ expect(text).toContain('wallet_setup({ action: "add-link" })');
221
+ expect(text).toContain('wallet_setup({ action: "create", chain: "tempo" })');
222
+ expect(text).toContain('wallet_setup({ action: "create", chain: "base" })');
223
+ expect(text).toContain('wallet_setup({ action: "create", chain: "solana" })');
224
+ });
225
+ it("shows the setup menu when no payment methods are configured", async () => {
226
+ const tools = new Map();
227
+ const server = {
228
+ tool: (name, _description, _schema, handler) => {
229
+ tools.set(name, handler);
230
+ },
231
+ };
232
+ const { registerWalletTools } = await import("../wallet.js");
233
+ registerWalletTools(server);
234
+ const walletStatus = tools.get("wallet_status");
235
+ if (!walletStatus)
236
+ throw new Error("wallet_status tool was not registered");
237
+ const result = await walletStatus({});
238
+ const text = flattenText(result);
239
+ expect(text).toContain("No payment methods configured.");
240
+ expect(text).toContain("Link card/bank (recommended)");
241
+ });
166
242
  it("imports a wallet into OWS encrypted storage with Base as the default chain", async () => {
167
243
  const walletSetup = await getWalletSetupTool();
168
244
  const result = await walletSetup({
@@ -185,6 +261,7 @@ describe("wallet_setup tool", () => {
185
261
  label: "imported-wallet",
186
262
  },
187
263
  ]);
264
+ expect(state.defaultPaymentMethod).toBe("base");
188
265
  expect(text).toContain("Key imported to OWS [encrypted]:");
189
266
  expect(text).toContain("Address: 0x2222222222222222222222222222222222222222");
190
267
  expect(text).toContain("Consumer principal: did:pkh:eip155:8453:0xabc");
@@ -228,4 +305,80 @@ describe("wallet_setup tool", () => {
228
305
  expect(text).toContain("Removed Visa ****4242.");
229
306
  expect(text).toContain("Card disconnected from Agent Wonderland.");
230
307
  });
308
+ it("starts Link device auth from wallet_setup", async () => {
309
+ const walletSetup = await getWalletSetupTool();
310
+ const result = await walletSetup({ action: "add-link" });
311
+ const text = flattenText(result);
312
+ expect(state.linkLoginCalls).toBe(1);
313
+ expect(state.setPendingLinkSetupCalls[0]).toMatchObject({
314
+ verificationUrl: "https://app.link.com/device/setup?code=test-link-code",
315
+ phrase: "test-link-code",
316
+ });
317
+ expect(text).toContain("Approve Agent Wonderland in Link:");
318
+ expect(text).toContain("https://app.link.com/device/setup?code=test-link-code");
319
+ expect(text).toContain("Verification phrase: test-link-code");
320
+ });
321
+ it("continues a pending Link auth without starting a new login", async () => {
322
+ state.pendingLinkSetup = {
323
+ verificationUrl: "https://app.link.com/device/setup?code=existing",
324
+ phrase: "existing",
325
+ createdAt: new Date().toISOString(),
326
+ };
327
+ const walletSetup = await getWalletSetupTool();
328
+ const result = await walletSetup({ action: "add-link" });
329
+ const text = flattenText(result);
330
+ expect(state.linkLoginCalls).toBe(0);
331
+ expect(text).toContain("https://app.link.com/device/setup?code=existing");
332
+ expect(text).toContain("Verification phrase: existing");
333
+ });
334
+ it("connects the only Link payment method after auth", async () => {
335
+ state.linkAuthenticated = true;
336
+ state.pendingLinkSetup = {
337
+ verificationUrl: "https://app.link.com/device/setup?code=existing",
338
+ phrase: "existing",
339
+ createdAt: new Date().toISOString(),
340
+ };
341
+ state.linkPaymentMethods = [
342
+ { id: "csmrpd_123", label: "Barclays Mastercard ****3688" },
343
+ ];
344
+ const walletSetup = await getWalletSetupTool();
345
+ const result = await walletSetup({ action: "add-link" });
346
+ const text = flattenText(result);
347
+ expect(state.setPendingLinkSetupCalls).toEqual([null]);
348
+ expect(state.setLinkConfigCalls).toEqual([
349
+ { paymentMethodId: "csmrpd_123", label: "Barclays Mastercard ****3688" },
350
+ ]);
351
+ expect(text).toContain("Link payment method connected.");
352
+ expect(text).toContain("Barclays Mastercard ****3688");
353
+ });
354
+ it("shows friendly names when multiple Link payment methods exist", async () => {
355
+ state.linkAuthenticated = true;
356
+ state.linkPaymentMethods = [
357
+ { id: "csmrpd_1", label: "Barclays Arrival Mastercard ****3688" },
358
+ { id: "csmrpd_2", label: "Total Checking bank ****2889" },
359
+ ];
360
+ const walletSetup = await getWalletSetupTool();
361
+ const result = await walletSetup({ action: "add-link" });
362
+ const text = flattenText(result);
363
+ expect(text).toContain("Barclays Arrival Mastercard ****3688 (csmrpd_1)");
364
+ expect(text).toContain("Total Checking bank ****2889 (csmrpd_2)");
365
+ expect(text).toContain('link_payment_method: "<name or last4>"');
366
+ });
367
+ it("resolves a friendly Link payment method selector", async () => {
368
+ state.linkAuthenticated = true;
369
+ state.linkPaymentMethods = [
370
+ { id: "csmrpd_1", label: "Barclays Arrival Mastercard ****3688", searchText: "csmrpd_1 barclays arrival mastercard 3688" },
371
+ { id: "csmrpd_2", label: "Total Checking bank ****2889", searchText: "csmrpd_2 total checking bank 2889" },
372
+ ];
373
+ const walletSetup = await getWalletSetupTool();
374
+ const result = await walletSetup({
375
+ action: "add-link",
376
+ link_payment_method: "3688",
377
+ });
378
+ const text = flattenText(result);
379
+ expect(state.setLinkConfigCalls).toEqual([
380
+ { paymentMethodId: "csmrpd_1", label: "Barclays Arrival Mastercard ****3688" },
381
+ ]);
382
+ expect(text).toContain("Barclays Arrival Mastercard ****3688 (csmrpd_1)");
383
+ });
231
384
  });
@@ -35,7 +35,7 @@ export function registerPassTools(server) {
35
35
  server.tool("buy_agent_credit_pack", "Purchase a discounted prepaid credit pack for an agent. Credit packs are agent-specific and automatically cover future runs until the included units run out.", {
36
36
  agent_id: z.string().describe("Agent ID (UUID, slug, or name)"),
37
37
  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."),
38
- pay_with: z.string().optional().describe("Payment method — wallet ID, chain name, or 'card'. Auto-detected if omitted."),
38
+ pay_with: z.string().optional().describe("Payment method — wallet ID, chain name, 'link', or 'card'. Auto-detected if omitted."),
39
39
  confirmed: z.boolean().optional().describe("Set to true to confirm the purchase after seeing the quote."),
40
40
  }, async ({ agent_id, pack_id, pay_with, confirmed }) => {
41
41
  if (!hasWalletConfigured()) {
package/dist/tools/run.js CHANGED
@@ -88,7 +88,7 @@ export function registerRunTools(server) {
88
88
  server.tool("run_agent", "Run an AI agent from the marketplace. Pays automatically via configured wallet. Returns the agent's output, cost, and job ID for tracking. If spending confirmation is enabled, first call returns a price quote — call again with confirmed: true to execute. Local file paths in the input (e.g. /Users/.../photo.jpg) are automatically uploaded to temporary storage and replaced with download URLs before execution. If a file you need isn't on this MCP server's filesystem (e.g. a sandboxed /mnt/... attachment), call upload_file first to get a presigned upload URL, PUT the bytes to it, then pass the returned GET URL as input instead — that keeps the bytes out of the conversation context.", {
89
89
  agent_id: z.string().describe("Agent ID (UUID, slug, or name)"),
90
90
  input: z.record(z.string(), z.unknown()).describe("Input payload for the agent"),
91
- pay_with: z.string().trim().min(1).optional().describe("Payment method — wallet ID, chain name (tempo, base, etc.), or 'card'. Auto-detected if omitted."),
91
+ 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."),
92
92
  confirmed: z.boolean().optional().describe("Set to true to confirm spending after seeing the price quote."),
93
93
  }, async ({ agent_id, input, pay_with, confirmed }) => {
94
94
  if (!hasWalletConfigured()) {
@@ -6,6 +6,23 @@ import { agentLine } from "../core/formatters.js";
6
6
  function text(t) {
7
7
  return { content: [{ type: "text", text: t }] };
8
8
  }
9
+ function completedJobs(agent) {
10
+ const stats = agent.stats;
11
+ return stats?.completedJobs ?? agent.totalExecutions ?? 0;
12
+ }
13
+ function averageRating(agent) {
14
+ const stats = agent.stats;
15
+ return agent.avgRating ?? stats?.avgRating ?? 0;
16
+ }
17
+ function ratingCount(agent) {
18
+ const stats = agent.stats;
19
+ const topLevel = typeof agent.ratingCount === "number" ? agent.ratingCount : undefined;
20
+ return topLevel ?? stats?.ratingCount ?? 0;
21
+ }
22
+ function price(agent) {
23
+ const parsed = Number.parseFloat(agent.pricePerRunUsd ?? "0");
24
+ return Number.isFinite(parsed) ? parsed : 0;
25
+ }
9
26
  export function registerSearchTools(server) {
10
27
  server.tool("search_agents", "Search the Agent Wonderland marketplace for AI agents. Returns a ranked list of matching agents with ratings, pricing, and job counts.", {
11
28
  query: z.string().optional().describe("Search query (natural language or keywords)"),
@@ -61,6 +78,22 @@ export function registerSearchTools(server) {
61
78
  return typeof avg === "number" && avg >= min_rating;
62
79
  });
63
80
  }
81
+ // The API enriches list rows with live stats after registry search. Do a
82
+ // final local sort for stat-based MCP views so displayed jobs/ratings and
83
+ // result order cannot disagree while registry aggregates catch up.
84
+ if (sort === "popularity") {
85
+ agents = [...agents].sort((a, b) => completedJobs(b) - completedJobs(a) ||
86
+ ratingCount(b) - ratingCount(a) ||
87
+ averageRating(b) - averageRating(a));
88
+ }
89
+ else if (sort === "rating") {
90
+ agents = [...agents].sort((a, b) => averageRating(b) - averageRating(a) ||
91
+ ratingCount(b) - ratingCount(a) ||
92
+ completedJobs(b) - completedJobs(a));
93
+ }
94
+ else if (sort === "price") {
95
+ agents = [...agents].sort((a, b) => price(a) - price(b));
96
+ }
64
97
  // Trim to requested limit after filtering
65
98
  agents = agents.slice(0, requestedLimit);
66
99
  if (agents.length === 0) {