@agentwonderland/mcp 0.1.23 → 0.1.25

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 (95) hide show
  1. package/dist/core/__tests__/amount-utils.test.d.ts +1 -0
  2. package/dist/core/__tests__/amount-utils.test.js +11 -0
  3. package/dist/core/__tests__/card-setup.test.d.ts +1 -0
  4. package/dist/core/__tests__/card-setup.test.js +99 -0
  5. package/dist/core/__tests__/formatters.test.d.ts +1 -0
  6. package/dist/core/__tests__/formatters.test.js +15 -0
  7. package/dist/core/__tests__/passes.test.d.ts +1 -0
  8. package/dist/core/__tests__/passes.test.js +82 -0
  9. package/dist/core/__tests__/payments.test.d.ts +1 -0
  10. package/dist/core/__tests__/payments.test.js +101 -0
  11. package/dist/core/__tests__/principal.test.d.ts +1 -0
  12. package/dist/core/__tests__/principal.test.js +67 -0
  13. package/dist/core/__tests__/spend-policy.test.d.ts +1 -0
  14. package/dist/core/__tests__/spend-policy.test.js +40 -0
  15. package/dist/core/amount-utils.d.ts +1 -0
  16. package/dist/core/amount-utils.js +4 -0
  17. package/dist/core/api-client.d.ts +9 -4
  18. package/dist/core/api-client.js +52 -22
  19. package/dist/core/base-charge.js +3 -2
  20. package/dist/core/card-setup.d.ts +20 -13
  21. package/dist/core/card-setup.js +85 -29
  22. package/dist/core/config.d.ts +22 -0
  23. package/dist/core/config.js +46 -2
  24. package/dist/core/formatters.d.ts +4 -3
  25. package/dist/core/formatters.js +10 -8
  26. package/dist/core/index.d.ts +2 -0
  27. package/dist/core/index.js +2 -0
  28. package/dist/core/ows-adapter.d.ts +10 -2
  29. package/dist/core/ows-adapter.js +54 -10
  30. package/dist/core/passes.d.ts +40 -0
  31. package/dist/core/passes.js +32 -0
  32. package/dist/core/payments.d.ts +8 -0
  33. package/dist/core/payments.js +111 -17
  34. package/dist/core/principal.d.ts +2 -0
  35. package/dist/core/principal.js +109 -0
  36. package/dist/core/solana-charge.d.ts +9 -0
  37. package/dist/core/solana-charge.js +96 -0
  38. package/dist/core/spend-policy.d.ts +12 -0
  39. package/dist/core/spend-policy.js +53 -0
  40. package/dist/core/types.d.ts +11 -2
  41. package/dist/index.js +11 -3
  42. package/dist/prompts/index.js +4 -2
  43. package/dist/resources/agents.js +1 -1
  44. package/dist/resources/wallet.js +8 -1
  45. package/dist/tools/__tests__/_payment-confirmation.test.d.ts +1 -0
  46. package/dist/tools/__tests__/_payment-confirmation.test.js +30 -0
  47. package/dist/tools/_payment-confirmation.d.ts +6 -0
  48. package/dist/tools/_payment-confirmation.js +28 -0
  49. package/dist/tools/agent-info.js +16 -2
  50. package/dist/tools/favorites.js +1 -1
  51. package/dist/tools/index.d.ts +1 -0
  52. package/dist/tools/index.js +1 -0
  53. package/dist/tools/observability.d.ts +2 -0
  54. package/dist/tools/observability.js +20 -0
  55. package/dist/tools/passes.d.ts +2 -0
  56. package/dist/tools/passes.js +157 -0
  57. package/dist/tools/run.js +127 -53
  58. package/dist/tools/solve.js +115 -51
  59. package/dist/tools/wallet.js +110 -59
  60. package/package.json +3 -1
  61. package/src/core/__tests__/amount-utils.test.ts +13 -0
  62. package/src/core/__tests__/card-setup.test.ts +118 -0
  63. package/src/core/__tests__/formatters.test.ts +17 -0
  64. package/src/core/__tests__/passes.test.ts +94 -0
  65. package/src/core/__tests__/payments.test.ts +122 -0
  66. package/src/core/__tests__/principal.test.ts +87 -0
  67. package/src/core/__tests__/spend-policy.test.ts +58 -0
  68. package/src/core/amount-utils.ts +5 -0
  69. package/src/core/api-client.ts +70 -23
  70. package/src/core/base-charge.ts +3 -2
  71. package/src/core/card-setup.ts +109 -34
  72. package/src/core/config.ts +74 -3
  73. package/src/core/formatters.ts +13 -9
  74. package/src/core/index.ts +2 -0
  75. package/src/core/ows-adapter.ts +74 -8
  76. package/src/core/passes.ts +74 -0
  77. package/src/core/payments.ts +130 -17
  78. package/src/core/principal.ts +128 -0
  79. package/src/core/solana-charge.ts +150 -0
  80. package/src/core/spend-policy.ts +69 -0
  81. package/src/core/types.ts +11 -2
  82. package/src/index.ts +11 -3
  83. package/src/prompts/index.ts +4 -2
  84. package/src/resources/agents.ts +1 -1
  85. package/src/resources/wallet.ts +8 -1
  86. package/src/tools/__tests__/_payment-confirmation.test.ts +45 -0
  87. package/src/tools/_payment-confirmation.ts +52 -0
  88. package/src/tools/agent-info.ts +25 -2
  89. package/src/tools/favorites.ts +1 -4
  90. package/src/tools/index.ts +1 -0
  91. package/src/tools/observability.ts +43 -0
  92. package/src/tools/passes.ts +228 -0
  93. package/src/tools/run.ts +174 -57
  94. package/src/tools/solve.ts +147 -59
  95. package/src/tools/wallet.ts +132 -62
@@ -1,11 +1,14 @@
1
1
  import { z } from "zod";
2
2
  import { apiGet, apiPost, apiPostWithPayment } from "../core/api-client.js";
3
- import { initiateCardSetup, formatCardSetupBlocks } from "../core/card-setup.js";
4
- import { hasWalletConfigured, getConfiguredMethods, getAcceptedPaymentMethods, getWalletAddress, } from "../core/payments.js";
3
+ import { getOrCreatePendingCardSetup, formatCardSetupBlocks } from "../core/card-setup.js";
4
+ import { getCompatiblePaymentMethods, hasWalletConfigured, getConfiguredMethods, getAcceptedPaymentMethods, getWalletAddress, normalizePaymentMethod, toRegistryPaymentMethod, } from "../core/payments.js";
5
5
  import { requiresSpendConfirmation, getDefaultTipAmount } from "../core/config.js";
6
6
  import { agentList, formatRunResult } from "../core/formatters.js";
7
+ import { canSpend, recordSpend, requiresPolicyConfirmation } from "../core/spend-policy.js";
7
8
  import { uploadLocalFiles } from "../core/file-upload.js";
8
9
  import { storeFeedbackToken } from "./_token-cache.js";
10
+ import { formatPaymentLabel, formatSolveConfirmationCommand, makeSolvePendingKey, resolveConfirmationMethod, } from "./_payment-confirmation.js";
11
+ import { getActiveCreditPack, getCreditPackInventory, getCreditPackProgram } from "../core/passes.js";
9
12
  const POLL_INTERVAL_MS = 3000;
10
13
  const POLL_MAX_MS = 120000;
11
14
  async function pollSolveJob(jobId) {
@@ -24,7 +27,7 @@ async function pollSolveJob(jobId) {
24
27
  }
25
28
  }
26
29
  catch {
27
- // Ignore poll errors, keep trying
30
+ // Ignore poll errors and keep trying.
28
31
  }
29
32
  }
30
33
  return { status: "failed", error_code: "POLL_TIMEOUT" };
@@ -35,11 +38,12 @@ function text(t) {
35
38
  function multiText(...blocks) {
36
39
  return { content: blocks.map((t) => ({ type: "text", text: t })) };
37
40
  }
41
+ const pendingSolves = new Map();
38
42
  function feedbackAsk(jobId, agentId, cost, tipSent) {
39
43
  const defaultTip = getDefaultTipAmount();
40
- const costLine = cost != null ? `Paid $${cost.toFixed(cost < 0.01 ? 4 : 2)}.` : "";
44
+ const costLine = cost != null ? `Paid $${cost.toFixed(cost < 0.01 ? 4 : 2)}.` : "This run used your available credit pack.";
41
45
  return [
42
- `IMPORTANT: After presenting the result above, you MUST ask the user:`,
46
+ "IMPORTANT: After presenting the result above, you MUST ask the user:",
43
47
  `"${costLine} Was this result helpful? I can rate it (1-5 stars) or tip the builder for you."`,
44
48
  tipSent ?? "",
45
49
  "",
@@ -67,8 +71,22 @@ async function autoTip(jobId, agentId, agentName, feedbackToken) {
67
71
  return `Auto-tip of $${defaultTip.toFixed(2)} failed.`;
68
72
  }
69
73
  }
74
+ function buildCreditPackSummary(agent) {
75
+ const creditPacks = getCreditPackProgram(agent);
76
+ if (!creditPacks?.packs?.length)
77
+ return [];
78
+ return [
79
+ "Discounted credit packs available:",
80
+ ...creditPacks.packs.map((pack) => {
81
+ const price = Number(pack.price_usd ?? "0");
82
+ const units = pack.included_units ?? 0;
83
+ const unitPrice = units > 0 ? ` ($${(price / units).toFixed(2)}/unit)` : "";
84
+ return ` ${pack.name ?? pack.key}: ${units} units for $${(pack.price_usd ?? "0.00")}${unitPrice}`;
85
+ }),
86
+ ];
87
+ }
70
88
  export function registerSolveTools(server) {
71
- server.tool("solve", "Solve a task by finding the best agent, paying, and executing. The primary way to use the marketplace. If spending confirmation is enabled, returns a price quote first — 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 — just pass the path directly, no base64 encoding needed.", {
89
+ server.tool("solve", "Solve a task by finding the best agent, paying, and executing. The primary way to use the marketplace. If spending confirmation is enabled, returns a price quote first — call again with confirmed: true to execute. Local file paths in the input are auto-uploaded before execution.", {
72
90
  intent: z
73
91
  .string()
74
92
  .describe("What you want to accomplish (natural language)"),
@@ -86,8 +104,9 @@ export function registerSolveTools(server) {
86
104
  .describe("Maximum budget in USD"),
87
105
  pay_with: z
88
106
  .string()
89
- .optional()
90
- .describe("Payment method — wallet ID, chain name (tempo, base, etc.), or 'card'. Auto-detected if omitted."),
107
+ .trim()
108
+ .min(1)
109
+ .describe("Required payment method — wallet ID, chain name (tempo, base, etc.), or 'card'. Use wallet_status to see configured options."),
91
110
  confirmed: z
92
111
  .boolean()
93
112
  .optional()
@@ -95,9 +114,8 @@ export function registerSolveTools(server) {
95
114
  }, async ({ intent, input, budget, pay_with, confirmed }) => {
96
115
  if (!hasWalletConfigured()) {
97
116
  try {
98
- const { qr, url } = await initiateCardSetup();
99
- const blocks = formatCardSetupBlocks(qr, url);
100
- return { content: blocks.map((t) => ({ type: "text", text: t })) };
117
+ const { url } = await getOrCreatePendingCardSetup();
118
+ return multiText(...formatCardSetupBlocks(url));
101
119
  }
102
120
  catch {
103
121
  return text("No payment method configured.\n\n" +
@@ -105,8 +123,15 @@ export function registerSolveTools(server) {
105
123
  "To use crypto: wallet_setup({ action: \"create\" })");
106
124
  }
107
125
  }
108
- const method = pay_with ?? getConfiguredMethods()[0];
109
- // Path 1: If authenticated, use the platform /solve route
126
+ const pendingKey = makeSolvePendingKey(intent, input, budget);
127
+ const pending = pendingSolves.get(pendingKey);
128
+ const configuredMethods = getConfiguredMethods();
129
+ const requestedMethod = pay_with;
130
+ const normalizedRequestedMethod = normalizePaymentMethod(requestedMethod);
131
+ if (!normalizedRequestedMethod) {
132
+ return text(`Payment method "${requestedMethod}" is not configured.\n\n` +
133
+ "Use wallet_status to review your current payment methods.");
134
+ }
110
135
  let processedInput;
111
136
  try {
112
137
  const uploadResult = await uploadLocalFiles(input);
@@ -129,8 +154,9 @@ export function registerSolveTools(server) {
129
154
  storeFeedbackToken(jobId, result.feedback_token, agentId);
130
155
  }
131
156
  const cost = result.cost;
157
+ const usedCreditPack = result.consumption_mode === "credit_pack";
132
158
  const tipMsg = result.feedback_token ? await autoTip(jobId, agentId, agentName, result.feedback_token) : "";
133
- return multiText(formatRunResult(result), feedbackAsk(jobId, agentId, cost, tipMsg));
159
+ return multiText(formatRunResult(result), feedbackAsk(jobId, agentId, usedCreditPack ? undefined : cost, tipMsg));
134
160
  }
135
161
  catch (err) {
136
162
  const isAuthError = err instanceof Error &&
@@ -139,9 +165,10 @@ export function registerSolveTools(server) {
139
165
  if (!isAuthError || !hasWalletConfigured())
140
166
  throw err;
141
167
  }
142
- // Path 2: Wallet-only discovery — find agents, pick best, pay via MPP
143
168
  const params = new URLSearchParams({ q: intent, limit: "5" });
144
- const acceptedMethods = getAcceptedPaymentMethods();
169
+ const acceptedMethods = requestedMethod
170
+ ? [toRegistryPaymentMethod(requestedMethod)].filter((value) => !!value)
171
+ : getAcceptedPaymentMethods();
145
172
  if (acceptedMethods.length > 0) {
146
173
  params.set("accepted_payment_methods", acceptedMethods.join(","));
147
174
  }
@@ -149,64 +176,97 @@ export function registerSolveTools(server) {
149
176
  if (!agents || agents.length === 0) {
150
177
  return text(`No agents found matching "${intent}".`);
151
178
  }
152
- // Show discovery results
153
179
  const discovery = agentList(agents, intent);
154
- // Pick cheapest agent within budget
155
- const inputTokens = Math.ceil(JSON.stringify(input).length / 4);
156
- const affordable = agents.filter((a) => {
157
- const price = parseFloat(a.pricePer1kTokens ?? "0.01");
158
- const cost = a.pricingModel === "fixed" ? price : (inputTokens / 1000) * price;
159
- return cost <= budget;
180
+ const affordable = agents.filter((agent) => {
181
+ const price = parseFloat(agent.pricePerRunUsd ?? "0.01");
182
+ return price <= budget;
160
183
  });
161
184
  const selected = affordable[0] ?? agents[0];
162
- const selectedPrice = parseFloat(selected.pricePer1kTokens ?? "0.01");
163
- const estimatedCost = selected.pricingModel === "fixed"
164
- ? selectedPrice
165
- : (inputTokens / 1000) * selectedPrice;
166
- // Confirmation step: show discovery + price, wait for confirmed: true
167
- if (requiresSpendConfirmation() && !confirmed) {
185
+ const activeCreditPack = await getCreditPackInventory(selected.id).then(getActiveCreditPack);
186
+ const compatibleMethods = getCompatiblePaymentMethods(selected, configuredMethods);
187
+ if (!compatibleMethods.includes(normalizedRequestedMethod)) {
188
+ return text(`The best matching agent cannot be paid with "${requestedMethod}".\n\n` +
189
+ `Available payment methods for ${selected.name}: ${compatibleMethods.join(", ") || "none"}.\n` +
190
+ "Choose another payment method or refine your search.");
191
+ }
192
+ const method = resolveConfirmationMethod(pay_with, pending?.method, compatibleMethods);
193
+ const spendCheckMethod = method ?? normalizedRequestedMethod;
194
+ const selectedPrice = parseFloat(selected.pricePerRunUsd ?? "0.01");
195
+ const estimatedCost = selectedPrice;
196
+ const needsPolicyConfirmation = !activeCreditPack && requiresPolicyConfirmation(spendCheckMethod, estimatedCost);
197
+ if (!activeCreditPack && (requiresSpendConfirmation() || needsPolicyConfirmation) && !confirmed) {
198
+ pendingSolves.set(pendingKey, { method });
168
199
  return text([
169
200
  discovery,
170
201
  "",
171
202
  `Best match: ${selected.name}`,
172
203
  `Cost: $${estimatedCost.toFixed(2)}`,
173
- `Payment: ${method}`,
204
+ `Payment: ${formatPaymentLabel(method)}`,
205
+ ...(() => {
206
+ const summary = buildCreditPackSummary(selected);
207
+ return summary.length > 0 ? ["", ...summary] : [];
208
+ })(),
174
209
  "",
175
210
  "To proceed, call:",
176
- ` solve({ intent: "${intent}", input: <same>, budget: ${budget}, confirmed: true })`,
211
+ formatSolveConfirmationCommand(intent, budget, method),
177
212
  "",
178
213
  "To cancel, do nothing.",
179
214
  ].join("\n"));
180
215
  }
181
216
  let result;
182
- let processedInput2;
183
- try {
184
- const uploadResult2 = await uploadLocalFiles(input);
185
- processedInput2 = uploadResult2.input;
186
- }
187
- catch (err) {
188
- const msg = err instanceof Error ? err.message : "File upload failed";
189
- return text(`Error: ${msg}`);
217
+ if (!activeCreditPack) {
218
+ const spendCheck = canSpend({
219
+ method: spendCheckMethod,
220
+ amountUsd: estimatedCost,
221
+ });
222
+ if (!spendCheck.ok) {
223
+ return text(spendCheck.message);
224
+ }
190
225
  }
191
226
  try {
192
- result = await apiPostWithPayment(`/agents/${selected.id}/run`, { input: processedInput2 }, method);
227
+ let usedPaidMethod = !activeCreditPack;
228
+ if (activeCreditPack) {
229
+ try {
230
+ result = await apiPost(`/agents/${selected.id}/run`, { input: processedInput }, { ensureConsumerPrincipal: true });
231
+ }
232
+ catch (packErr) {
233
+ const packApiErr = packErr;
234
+ if (packApiErr?.status !== 402)
235
+ throw packErr;
236
+ result = await apiPostWithPayment(`/agents/${selected.id}/run`, { input: processedInput }, method);
237
+ usedPaidMethod = true;
238
+ }
239
+ }
240
+ else {
241
+ result = await apiPostWithPayment(`/agents/${selected.id}/run`, { input: processedInput }, method);
242
+ }
243
+ if (usedPaidMethod) {
244
+ recordSpend(spendCheckMethod, estimatedCost);
245
+ }
193
246
  }
194
247
  catch (err) {
195
248
  const apiErr = err;
196
249
  if (apiErr?.status === 402) {
197
- return text("Payment failed — your wallet may not have enough USDC.\n\n" +
198
- "Check your balance and fund your wallet, then try again.\n" +
199
- "Use wallet_status to check your current payment methods.");
250
+ return text([
251
+ "Payment failed — your wallet may not have enough funds or the selected method was rejected.",
252
+ "",
253
+ "Check your balance and try again.",
254
+ "Use wallet_status to check your current payment methods.",
255
+ ...(() => {
256
+ const summary = buildCreditPackSummary(selected);
257
+ return summary.length > 0 ? ["", ...summary] : [];
258
+ })(),
259
+ ].join("\n"));
200
260
  }
201
261
  return text(`Error: ${apiErr?.message ?? "Failed to run agent"}`);
202
262
  }
263
+ pendingSolves.delete(pendingKey);
203
264
  const jobId = result.job_id ?? "";
204
- const agentId2 = result.agent_id ?? selected.id;
265
+ const agentId = result.agent_id ?? selected.id;
205
266
  const status = result.status;
206
267
  if (result.feedback_token) {
207
- storeFeedbackToken(jobId, result.feedback_token, agentId2);
268
+ storeFeedbackToken(jobId, result.feedback_token, agentId);
208
269
  }
209
- // Async agent — poll until complete
210
270
  if (status === "processing") {
211
271
  const pollResult = await pollSolveJob(jobId);
212
272
  if (pollResult.status === "completed") {
@@ -224,9 +284,9 @@ export function registerSolveTools(server) {
224
284
  asyncFormatted,
225
285
  ].join("\n");
226
286
  const asyncCost = result.cost;
227
- return multiText(asyncOutput, feedbackAsk(jobId, agentId2, asyncCost, ""));
287
+ const usedCreditPack = result.consumption_mode === "credit_pack";
288
+ return multiText(asyncOutput, feedbackAsk(jobId, agentId, usedCreditPack ? undefined : asyncCost, ""));
228
289
  }
229
- // Async agent failed
230
290
  const failedFormatted = formatRunResult({
231
291
  ...result,
232
292
  status: "failed",
@@ -240,10 +300,14 @@ export function registerSolveTools(server) {
240
300
  "",
241
301
  failedFormatted,
242
302
  ].join("\n");
243
- return multiText(failedOutput, "The agent execution failed. A refund has been initiated automatically.");
303
+ const usedCreditPack = result.consumption_mode === "credit_pack";
304
+ return multiText(failedOutput, usedCreditPack
305
+ ? "The agent execution failed and the reserved credit-pack unit was released automatically."
306
+ : "The agent execution failed. A refund has been initiated automatically.");
244
307
  }
245
308
  const actualCost = result.cost;
246
- const tipMsg = result.feedback_token ? await autoTip(jobId, agentId2, selected.name ?? "", result.feedback_token) : "";
309
+ const usedCreditPack = result.consumption_mode === "credit_pack";
310
+ const tipMsg = result.feedback_token ? await autoTip(jobId, agentId, selected.name ?? "", result.feedback_token) : "";
247
311
  const output = [
248
312
  discovery,
249
313
  "",
@@ -252,6 +316,6 @@ export function registerSolveTools(server) {
252
316
  "",
253
317
  formatRunResult(result, { paymentMethod: method }),
254
318
  ].join("\n");
255
- return multiText(output, feedbackAsk(jobId, agentId2, actualCost, tipMsg));
319
+ return multiText(output, feedbackAsk(jobId, agentId, usedCreditPack ? undefined : actualCost, tipMsg));
256
320
  });
257
321
  }
@@ -1,30 +1,50 @@
1
1
  import { z } from "zod";
2
- import { getWallets, getCardConfig, setCardConfig, addWallet } from "../core/config.js";
2
+ import { getWallets, getCardConfig, setCardConfig, addWallet, getPendingCardSetupToken, getSpendPolicy, setSpendPolicy, } from "../core/config.js";
3
3
  import { getWalletAddress } from "../core/payments.js";
4
- import { initiateCardSetup, pollCardSetup } from "../core/card-setup.js";
5
- import { isOwsAvailable, createOwsWallet, importKeyToOws, listOwsWallets, } from "../core/ows-adapter.js";
4
+ import { getOrCreatePendingCardSetup, formatCardSetupBlocks, getCardCapabilities, pollCardSetup, } from "../core/card-setup.js";
5
+ import { isOwsAvailable, createOwsWallet, importKeyToOws, listOwsWallets, listOwsWalletsByChain, } from "../core/ows-adapter.js";
6
+ import { ensureConsumerPrincipal, getConsumerPrincipal } from "../core/principal.js";
6
7
  function text(t) {
7
8
  return { content: [{ type: "text", text: t }] };
8
9
  }
9
- // Pending card setup — stored between the QR display call and the confirmation poll
10
- let pendingCardSetup = null;
11
10
  export function registerWalletTools(server) {
12
11
  // ── wallet_status (extracted from check_wallet) ─────────────────
13
12
  server.tool("wallet_status", "Check payment readiness. Shows all configured wallets, their chains, addresses, and card status.", {}, async () => {
14
13
  const wallets = getWallets();
15
14
  const card = getCardConfig();
16
- if (wallets.length === 0 && !card) {
15
+ const pendingCardSetupToken = getPendingCardSetupToken();
16
+ const consumerPrincipal = await getConsumerPrincipal();
17
+ if (wallets.length === 0 && !card && !pendingCardSetupToken) {
17
18
  return text("No payment methods configured.\nUse wallet_setup to create or import a wallet.");
18
19
  }
19
20
  const lines = ["Payment methods:"];
20
21
  for (const w of wallets) {
21
- const addr = await getWalletAddress(w.id);
22
22
  const label = w.label ? ` (${w.label})` : "";
23
23
  const storage = w.keyType === "ows" ? " [encrypted]" : " [plaintext]";
24
- lines.push(` ${w.id}${label}${storage}: ${w.chains.join(", ")} \u2014 ${addr ?? "unknown"}`);
24
+ const chainAddresses = await Promise.all(w.chains.map(async (chainName) => {
25
+ const addr = await getWalletAddress(chainName);
26
+ return `${chainName}: ${addr ?? "unknown"}`;
27
+ }));
28
+ lines.push(` ${w.id}${label}${storage}: ${chainAddresses.join(" | ")}`);
25
29
  }
26
30
  if (card) {
27
31
  lines.push(` Card: ${card.brand} ****${card.last4}`);
32
+ const capabilities = await getCardCapabilities();
33
+ if (capabilities.spt_status === "enabled") {
34
+ lines.push(" Card MPP: ready");
35
+ }
36
+ else if (capabilities.spt_status === "unavailable") {
37
+ lines.push(` Card MPP: unavailable — ${capabilities.message ?? "Stripe Shared Payment Token access is not enabled."}`);
38
+ }
39
+ else {
40
+ lines.push(` Card MPP: unknown — ${capabilities.message ?? "Could not determine card payment readiness."}`);
41
+ }
42
+ }
43
+ if (pendingCardSetupToken) {
44
+ lines.push(" Card setup: pending confirmation");
45
+ }
46
+ if (consumerPrincipal) {
47
+ lines.push("", `Consumer principal: ${consumerPrincipal}`);
28
48
  }
29
49
  return text(lines.join("\n"));
30
50
  });
@@ -42,7 +62,7 @@ export function registerWalletTools(server) {
42
62
  .optional()
43
63
  .describe("Private key hex string (required for 'import', ignored for 'create')"),
44
64
  chain: z.enum(["tempo", "base", "solana"]).optional()
45
- .describe("Primary chain (default: tempo). Solana support is via Stripe deposit mode."),
65
+ .describe("Primary chain (default: tempo). Solana uses an OWS wallet plus Stripe deposit-mode USDC."),
46
66
  }, async ({ action, name, key, chain }) => {
47
67
  // ── Card setup flow ──────────────────────────────────────
48
68
  if (action === "add-card") {
@@ -50,48 +70,32 @@ export function registerWalletTools(server) {
50
70
  if (existing) {
51
71
  return text(`Card already connected: ${existing.brand} ****${existing.last4}\n\nTo replace it, remove the current card first.`);
52
72
  }
73
+ const pendingToken = getPendingCardSetupToken();
53
74
  // Check if there's a pending setup to complete (user said they're done)
54
- if (pendingCardSetup) {
55
- const pendingToken = pendingCardSetup.token;
56
- const result = await pollCardSetup(pendingToken, 120_000);
57
- pendingCardSetup = null;
75
+ if (pendingToken) {
76
+ const result = await pollCardSetup(pendingToken, 250);
58
77
  if (result) {
59
- return text(`Connected! ${result.brand} ****${result.last4} is ready for payments.`);
78
+ const principal = await ensureConsumerPrincipal();
79
+ return text(`Connected! ${result.brand} ****${result.last4} is ready for payments.\n\n` +
80
+ `Consumer principal: ${principal}`);
60
81
  }
61
- return text("Card setup timed out. Run wallet_setup({ action: \"add-card\" }) to try again.");
62
- }
63
- // Create new card setup session with QR code, then auto-poll
64
- try {
65
- const { qr, url, token } = await initiateCardSetup();
66
- // Auto-poll for up to 90 seconds — if the user enters their card
67
- // quickly, the tool returns success without a second call
68
- const result = await pollCardSetup(token, 90_000);
69
- if (result) {
82
+ try {
83
+ const { url } = await getOrCreatePendingCardSetup();
84
+ const blocks = formatCardSetupBlocks(url);
70
85
  return {
71
- content: [
72
- { type: "text", text: "\n" + qr.trim() },
73
- { type: "text", text: [
74
- `Open to connect a card: ${url}`,
75
- "",
76
- `Connected! ${result.brand} ****${result.last4} is ready for payments.`,
77
- ].join("\n") },
78
- ],
86
+ content: blocks.map((t) => ({ type: "text", text: t })),
79
87
  };
80
88
  }
81
- // Timed out — save token for manual follow-up
82
- pendingCardSetup = { token };
89
+ catch {
90
+ return text("Card setup timed out. Run wallet_setup({ action: \"add-card\" }) to try again.");
91
+ }
92
+ }
93
+ // Create new card setup session and return immediately so clients
94
+ // don't appear hung while the user is still in the browser flow.
95
+ try {
96
+ const { url } = await getOrCreatePendingCardSetup();
83
97
  return {
84
- content: [
85
- { type: "text", text: "\n" + qr.trim() },
86
- { type: "text", text: [
87
- `IMPORTANT: Present this link to the user to connect a payment card:`,
88
- "",
89
- url,
90
- "",
91
- `Tell the user: "Scan the QR code above or open this link to connect your card. Let me know when you're done."`,
92
- `After they confirm, call wallet_setup({ action: "add-card" }) to complete setup.`,
93
- ].join("\n") },
94
- ],
98
+ content: formatCardSetupBlocks(url).map((t) => ({ type: "text", text: t })),
95
99
  };
96
100
  }
97
101
  catch {
@@ -113,16 +117,20 @@ export function registerWalletTools(server) {
113
117
  }
114
118
  const selectedChains = chain === "solana" ? ["solana"] : ["tempo", "base"];
115
119
  const defaultCh = chain ?? "tempo";
120
+ const preferredOwsChain = defaultCh === "solana" ? "solana" : "evm";
116
121
  const owsReady = await isOwsAvailable();
117
122
  if (action === "create") {
118
123
  // Check for existing OWS wallets first
119
124
  if (owsReady) {
120
- const existing = await listOwsWallets();
125
+ const existing = preferredOwsChain === "solana"
126
+ ? await listOwsWalletsByChain("solana")
127
+ : await listOwsWallets();
121
128
  if (existing.length > 0) {
122
129
  const w = existing[0];
123
130
  // Check if already in config
124
131
  const wallets = getWallets();
125
- const alreadyLinked = wallets.find(wl => wl.owsWalletId === w.id);
132
+ const alreadyLinked = wallets.find((wl) => wl.owsWalletId === w.id);
133
+ let chainStatus = "Linked to Agent Wonderland.";
126
134
  if (!alreadyLinked) {
127
135
  addWallet({
128
136
  id: w.name,
@@ -133,14 +141,29 @@ export function registerWalletTools(server) {
133
141
  label: w.name,
134
142
  });
135
143
  }
144
+ else {
145
+ const mergedChains = [...new Set([...alreadyLinked.chains, ...selectedChains])];
146
+ if (mergedChains.length !== alreadyLinked.chains.length) {
147
+ addWallet({
148
+ ...alreadyLinked,
149
+ chains: mergedChains,
150
+ });
151
+ chainStatus = `Updated linked chains: ${mergedChains.join(", ")}.`;
152
+ }
153
+ else {
154
+ chainStatus = "Already linked to Agent Wonderland.";
155
+ }
156
+ }
157
+ const principal = await ensureConsumerPrincipal();
136
158
  return text([
137
159
  "Found existing OWS wallet:",
138
160
  ` Name: ${w.name}`,
139
161
  ` Address: ${w.address}`,
140
162
  ` Chains: ${selectedChains.join(", ")}`,
141
163
  ` Storage: ~/.ows/ (encrypted)`,
164
+ ` Consumer principal: ${principal}`,
142
165
  "",
143
- alreadyLinked ? "Already linked to Agent Wonderland." : "Linked to Agent Wonderland.",
166
+ chainStatus,
144
167
  "",
145
168
  `Fund this address with USDC on ${defaultCh === "solana" ? "Solana" : "Tempo"} to start using agents.`,
146
169
  ].join("\n"));
@@ -149,10 +172,12 @@ export function registerWalletTools(server) {
149
172
  if (!owsReady) {
150
173
  return text("Cannot create wallet: OWS (Open Wallet Standard) is not installed.\n" +
151
174
  "Install with: npm install -g @open-wallet-standard/core\n\n" +
152
- "Alternatively, use action 'import' with an existing private key.");
175
+ (defaultCh === "solana"
176
+ ? "Solana wallets currently require OWS-managed encrypted storage."
177
+ : "Alternatively, use action 'import' with an existing private key."));
153
178
  }
154
179
  const walletName = name ?? `aw-${Date.now()}`;
155
- const result = await createOwsWallet(walletName);
180
+ const result = await createOwsWallet(walletName, preferredOwsChain);
156
181
  // Persist to config so payment flows can find it
157
182
  addWallet({
158
183
  id: walletName,
@@ -162,6 +187,7 @@ export function registerWalletTools(server) {
162
187
  defaultChain: defaultCh,
163
188
  label: walletName,
164
189
  });
190
+ const principal = await ensureConsumerPrincipal();
165
191
  return text([
166
192
  `Wallet created [encrypted]:`,
167
193
  ` ID: ${result.walletId}`,
@@ -169,6 +195,7 @@ export function registerWalletTools(server) {
169
195
  ` Name: ${walletName}`,
170
196
  ` Chains: ${selectedChains.join(", ")}`,
171
197
  ` Storage: ~/.ows/ (AES-256-GCM encrypted)`,
198
+ ` Consumer principal: ${principal}`,
172
199
  "",
173
200
  `Fund this address with USDC on ${defaultCh === "solana" ? "Solana" : "Tempo"} to start using agents.`,
174
201
  "For testnet: npx mppx account fund",
@@ -177,7 +204,7 @@ export function registerWalletTools(server) {
177
204
  // action === "import"
178
205
  if (owsReady) {
179
206
  const walletName = name ?? `imported-${Date.now()}`;
180
- const result = await importKeyToOws(key, walletName);
207
+ const result = await importKeyToOws(key, walletName, preferredOwsChain);
181
208
  addWallet({
182
209
  id: walletName,
183
210
  keyType: "ows",
@@ -186,14 +213,20 @@ export function registerWalletTools(server) {
186
213
  defaultChain: defaultCh,
187
214
  label: walletName,
188
215
  });
216
+ const principal = await ensureConsumerPrincipal();
189
217
  return text([
190
218
  `Key imported to OWS [encrypted]:`,
191
219
  ` ID: ${result.walletId}`,
192
220
  ` Address: ${result.address}`,
193
221
  ` Name: ${walletName}`,
194
222
  ` Chains: ${selectedChains.join(", ")}`,
223
+ ` Consumer principal: ${principal}`,
195
224
  ].join("\n"));
196
225
  }
226
+ if (defaultCh === "solana") {
227
+ return text("Solana key import currently requires OWS (Open Wallet Standard) so the ed25519 key can be stored safely.\n" +
228
+ "Install with: npm install -g @open-wallet-standard/core");
229
+ }
197
230
  // No OWS — store plaintext
198
231
  try {
199
232
  const { privateKeyToAccount } = await import("viem/accounts");
@@ -208,11 +241,13 @@ export function registerWalletTools(server) {
208
241
  defaultChain: defaultCh,
209
242
  label: walletName,
210
243
  });
244
+ const principal = await ensureConsumerPrincipal();
211
245
  return text([
212
246
  `Key imported [plaintext] \u2014 OWS not available for encrypted storage.`,
213
247
  ` Address: ${account.address}`,
214
248
  ` Name: ${walletName}`,
215
249
  ` Chains: ${selectedChains.join(", ")}`,
250
+ ` Consumer principal: ${principal}`,
216
251
  "",
217
252
  "Install OWS for encrypted storage: npm install -g @open-wallet-standard/core",
218
253
  ].join("\n"));
@@ -234,26 +269,42 @@ export function registerWalletTools(server) {
234
269
  .positive()
235
270
  .optional()
236
271
  .describe("Maximum USD per day across all transactions"),
237
- }, async ({ wallet_id, max_per_tx, max_per_day }) => {
272
+ require_confirmation_above: z
273
+ .number()
274
+ .positive()
275
+ .optional()
276
+ .describe("Require manual confirmation above this USD amount even when auto-spend is enabled"),
277
+ }, async ({ wallet_id, max_per_tx, max_per_day, require_confirmation_above }) => {
238
278
  const wallets = getWallets();
239
279
  const wallet = wallets.find((w) => w.id === wallet_id);
240
280
  if (!wallet) {
241
281
  const available = wallets.map((w) => w.id).join(", ") || "none";
242
282
  return text(`Wallet "${wallet_id}" not found. Available wallets: ${available}`);
243
283
  }
244
- if (max_per_tx == null && max_per_day == null) {
245
- return text("No policy changes specified. Provide max_per_tx and/or max_per_day.");
284
+ if (max_per_tx == null &&
285
+ max_per_day == null &&
286
+ require_confirmation_above == null) {
287
+ return text("No policy changes specified. Provide at least one policy field.");
246
288
  }
289
+ const existing = getSpendPolicy(wallet_id) ?? {};
290
+ const nextPolicy = {
291
+ ...existing,
292
+ ...(max_per_tx != null ? { maxPerTxUsd: max_per_tx } : {}),
293
+ ...(max_per_day != null ? { maxPerDayUsd: max_per_day } : {}),
294
+ ...(require_confirmation_above != null ? { requireConfirmationAboveUsd: require_confirmation_above } : {}),
295
+ };
296
+ setSpendPolicy(wallet_id, nextPolicy);
247
297
  // Build policy summary
248
298
  const policies = [];
249
- if (max_per_tx != null) {
250
- policies.push(`Max per transaction: $${max_per_tx.toFixed(2)}`);
299
+ if (nextPolicy.maxPerTxUsd != null) {
300
+ policies.push(`Max per transaction: $${nextPolicy.maxPerTxUsd.toFixed(2)}`);
301
+ }
302
+ if (nextPolicy.maxPerDayUsd != null) {
303
+ policies.push(`Max per day: $${nextPolicy.maxPerDayUsd.toFixed(2)}`);
251
304
  }
252
- if (max_per_day != null) {
253
- policies.push(`Max per day: $${max_per_day.toFixed(2)}`);
305
+ if (nextPolicy.requireConfirmationAboveUsd != null) {
306
+ policies.push(`Require confirmation above: $${nextPolicy.requireConfirmationAboveUsd.toFixed(2)}`);
254
307
  }
255
- // Note: actual persistence depends on the config module supporting policy fields.
256
- // For now, we report what would be set. The core/config module will handle storage.
257
308
  return text([
258
309
  `Spending policy set for wallet "${wallet_id}":`,
259
310
  ...policies.map((p) => ` ${p}`),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentwonderland/mcp",
3
- "version": "0.1.23",
3
+ "version": "0.1.25",
4
4
  "type": "module",
5
5
  "description": "MCP server for the Agent Wonderland AI agent marketplace",
6
6
  "bin": {
@@ -23,6 +23,8 @@
23
23
  },
24
24
  "dependencies": {
25
25
  "@modelcontextprotocol/sdk": "^1.12.1",
26
+ "@solana/spl-token": "^0.4.14",
27
+ "@solana/web3.js": "^1.98.4",
26
28
  "mppx": "^0.4.9",
27
29
  "qrcode": "^1.5.4",
28
30
  "viem": "^2.47.6",
@@ -0,0 +1,13 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { toAtomicAmount } from "../amount-utils.js";
3
+
4
+ describe("toAtomicAmount", () => {
5
+ it("converts decimal USDC challenge amounts into atomic units", () => {
6
+ expect(toAtomicAmount("0.020000")).toBe(20000n);
7
+ expect(toAtomicAmount("1.000000")).toBe(1000000n);
8
+ });
9
+
10
+ it("supports different decimal precisions", () => {
11
+ expect(toAtomicAmount("0.50", 2)).toBe(50n);
12
+ });
13
+ });