@agentwonderland/mcp 0.1.24 → 0.1.26

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 (99) 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__/api-client.test.d.ts +1 -0
  4. package/dist/core/__tests__/api-client.test.js +51 -0
  5. package/dist/core/__tests__/formatters.test.js +10 -0
  6. package/dist/core/__tests__/passes-api.test.d.ts +1 -0
  7. package/dist/core/__tests__/passes-api.test.js +27 -0
  8. package/dist/core/__tests__/payments.test.js +59 -6
  9. package/dist/core/__tests__/principal.test.js +41 -4
  10. package/dist/core/__tests__/solana-charge.test.d.ts +1 -0
  11. package/dist/core/__tests__/solana-charge.test.js +50 -0
  12. package/dist/core/__tests__/spend-policy.test.d.ts +1 -0
  13. package/dist/core/__tests__/spend-policy.test.js +40 -0
  14. package/dist/core/amount-utils.d.ts +1 -0
  15. package/dist/core/amount-utils.js +4 -0
  16. package/dist/core/api-client.d.ts +1 -0
  17. package/dist/core/api-client.js +8 -3
  18. package/dist/core/balances.d.ts +1 -0
  19. package/dist/core/balances.js +56 -0
  20. package/dist/core/base-charge.js +16 -8
  21. package/dist/core/config.d.ts +19 -0
  22. package/dist/core/config.js +22 -0
  23. package/dist/core/formatters.d.ts +5 -5
  24. package/dist/core/formatters.js +12 -8
  25. package/dist/core/passes.d.ts +1 -1
  26. package/dist/core/passes.js +5 -2
  27. package/dist/core/payments.d.ts +1 -0
  28. package/dist/core/payments.js +32 -9
  29. package/dist/core/principal.d.ts +3 -0
  30. package/dist/core/principal.js +29 -1
  31. package/dist/core/settings.d.ts +20 -0
  32. package/dist/core/settings.js +19 -0
  33. package/dist/core/solana-charge.d.ts +5 -0
  34. package/dist/core/solana-charge.js +31 -8
  35. package/dist/core/spend-policy.d.ts +12 -0
  36. package/dist/core/spend-policy.js +53 -0
  37. package/dist/core/tempo-charge.d.ts +7 -0
  38. package/dist/core/tempo-charge.js +84 -0
  39. package/dist/core/types.d.ts +1 -2
  40. package/dist/index.js +9 -5
  41. package/dist/prompts/index.js +4 -2
  42. package/dist/resources/agents.js +1 -1
  43. package/dist/tools/__tests__/jobs.test.d.ts +1 -0
  44. package/dist/tools/__tests__/jobs.test.js +71 -0
  45. package/dist/tools/__tests__/run.test.d.ts +1 -0
  46. package/dist/tools/__tests__/run.test.js +149 -0
  47. package/dist/tools/__tests__/solve.test.d.ts +1 -0
  48. package/dist/tools/__tests__/solve.test.js +158 -0
  49. package/dist/tools/__tests__/wallet.test.d.ts +1 -0
  50. package/dist/tools/__tests__/wallet.test.js +230 -0
  51. package/dist/tools/_payment-confirmation.js +1 -1
  52. package/dist/tools/agent-info.js +2 -2
  53. package/dist/tools/favorites.js +1 -1
  54. package/dist/tools/jobs.js +8 -1
  55. package/dist/tools/observability.d.ts +2 -0
  56. package/dist/tools/observability.js +20 -0
  57. package/dist/tools/passes.js +11 -6
  58. package/dist/tools/run.js +45 -29
  59. package/dist/tools/solve.js +53 -40
  60. package/dist/tools/wallet.js +58 -22
  61. package/package.json +2 -2
  62. package/src/core/__tests__/amount-utils.test.ts +13 -0
  63. package/src/core/__tests__/api-client.test.ts +78 -0
  64. package/src/core/__tests__/formatters.test.ts +12 -0
  65. package/src/core/__tests__/passes-api.test.ts +33 -0
  66. package/src/core/__tests__/payments.test.ts +79 -6
  67. package/src/core/__tests__/principal.test.ts +49 -4
  68. package/src/core/__tests__/solana-charge.test.ts +59 -0
  69. package/src/core/__tests__/spend-policy.test.ts +58 -0
  70. package/src/core/amount-utils.ts +5 -0
  71. package/src/core/api-client.ts +16 -3
  72. package/src/core/balances.ts +63 -0
  73. package/src/core/base-charge.ts +16 -8
  74. package/src/core/config.ts +45 -0
  75. package/src/core/formatters.ts +16 -11
  76. package/src/core/passes.ts +5 -2
  77. package/src/core/payments.ts +37 -9
  78. package/src/core/principal.ts +42 -1
  79. package/src/core/settings.ts +36 -0
  80. package/src/core/solana-charge.ts +45 -10
  81. package/src/core/spend-policy.ts +69 -0
  82. package/src/core/tempo-charge.ts +104 -0
  83. package/src/core/types.ts +1 -2
  84. package/src/index.ts +9 -5
  85. package/src/prompts/index.ts +4 -2
  86. package/src/resources/agents.ts +1 -1
  87. package/src/tools/__tests__/jobs.test.ts +89 -0
  88. package/src/tools/__tests__/run.test.ts +176 -0
  89. package/src/tools/__tests__/solve.test.ts +186 -0
  90. package/src/tools/__tests__/wallet.test.ts +289 -0
  91. package/src/tools/_payment-confirmation.ts +1 -1
  92. package/src/tools/agent-info.ts +2 -2
  93. package/src/tools/favorites.ts +1 -4
  94. package/src/tools/jobs.ts +10 -1
  95. package/src/tools/observability.ts +43 -0
  96. package/src/tools/passes.ts +12 -12
  97. package/src/tools/run.ts +50 -41
  98. package/src/tools/solve.ts +58 -52
  99. package/src/tools/wallet.ts +60 -24
@@ -0,0 +1,20 @@
1
+ import { apiPost } from "../core/api-client.js";
2
+ function text(t) {
3
+ return { content: [{ type: "text", text: t }] };
4
+ }
5
+ export function registerObservabilityTools(server) {
6
+ server.tool("open_observability_dashboard", "Generate a secure one-click sign-in URL for the Agent Wonderland web observability dashboard. The dashboard shows your agent runs, spend, rebates, and recent activity.", {}, async () => {
7
+ const result = await apiPost("/observability/link", {}, { ensureConsumerPrincipal: true });
8
+ const lines = [
9
+ "Your secure observability link is ready:",
10
+ result.url,
11
+ "",
12
+ `Expires: ${result.expires_at}`,
13
+ ];
14
+ if (result.consumer_principal) {
15
+ lines.push(`Consumer principal: ${result.consumer_principal}`);
16
+ }
17
+ lines.push("", "Open the link in your browser to view usage metrics, spend, rebates, and recent runs.");
18
+ return text(lines.join("\n"));
19
+ });
20
+ }
@@ -1,9 +1,9 @@
1
1
  import { z } from "zod";
2
2
  import { apiGet, apiPostWithPayment } from "../core/api-client.js";
3
- import { formatCreditPack, formatCreditPackOffer, getCreditPackProgram, } from "../core/passes.js";
3
+ import { formatCreditPack, formatCreditPackOffer, getCreditPackInventory, getCreditPackProgram, } from "../core/passes.js";
4
4
  import { getCompatiblePaymentMethods, getConfiguredMethods, hasWalletConfigured, normalizePaymentMethod, } from "../core/payments.js";
5
5
  import { requiresSpendConfirmation } from "../core/config.js";
6
- import { ensureConsumerPrincipal } from "../core/principal.js";
6
+ import { ensureConsumerPrincipalForMethod } from "../core/principal.js";
7
7
  import { getOrCreatePendingCardSetup, formatCardSetupBlocks } from "../core/card-setup.js";
8
8
  import { formatPaymentChoicePrompt, formatPaymentLabel, resolveConfirmationMethod, } from "./_payment-confirmation.js";
9
9
  const pendingCreditPackPurchases = new Map();
@@ -51,7 +51,6 @@ export function registerPassTools(server) {
51
51
  }
52
52
  const agent = await getAgent(agent_id);
53
53
  const agentName = agent.name ?? agent_id;
54
- const principal = await ensureConsumerPrincipal();
55
54
  const program = getCreditPackProgram(agent);
56
55
  const offers = (program?.packs ?? [])
57
56
  .map((pack) => findOffer(agent, pack.key ?? ""))
@@ -102,6 +101,7 @@ export function registerPassTools(server) {
102
101
  ].join("\n"));
103
102
  }
104
103
  const method = resolveConfirmationMethod(pay_with, pending?.method, compatibleMethods);
104
+ const principal = await ensureConsumerPrincipalForMethod(method);
105
105
  if (requiresSpendConfirmation() && !confirmed) {
106
106
  pendingCreditPackPurchases.set(agent.id, {
107
107
  agentId: agent.id,
@@ -131,14 +131,19 @@ export function registerPassTools(server) {
131
131
  formatCreditPack(result.credit_pack),
132
132
  `Consumer principal: ${result.consumer_principal}`,
133
133
  "",
134
- "Future runs through run_agent will automatically use this credit pack while units remain.",
134
+ "Future runs for this agent will automatically use this credit pack while units remain.",
135
+ "That includes run_agent, and solve whenever it selects this same agent.",
135
136
  ].join("\n"));
136
137
  });
137
138
  server.tool("list_agent_credit_packs", "Show discounted credit-pack offers for an agent plus any balances available under the current consumer principal.", {
138
139
  agent_id: z.string().describe("Agent ID (UUID, slug, or name)"),
139
- }, async ({ agent_id }) => {
140
+ pay_with: z.string().optional().describe("Optional payment method context used to inspect the matching consumer principal."),
141
+ }, async ({ agent_id, pay_with }) => {
140
142
  const agent = await getAgent(agent_id);
141
- const result = await apiGet(`/agents/${agent.id}/credit-packs`, { ensureConsumerPrincipal: true });
143
+ const result = await getCreditPackInventory(agent.id, pay_with);
144
+ if (!result) {
145
+ return text(`Could not load credit-pack inventory for ${agent.name}.`);
146
+ }
142
147
  const lines = [
143
148
  `Credit packs for ${agent.name}`,
144
149
  ...(result.consumer_principal ? [`Consumer principal: ${result.consumer_principal}`] : []),
package/dist/tools/run.js CHANGED
@@ -5,14 +5,15 @@ import { formatCreditPackOffer, getActiveCreditPack, getCreditPackInventory, get
5
5
  import { getCompatiblePaymentMethods, getConfiguredMethods, hasWalletConfigured, getWalletAddress, normalizePaymentMethod, } from "../core/payments.js";
6
6
  import { requiresSpendConfirmation, getDefaultTipAmount } from "../core/config.js";
7
7
  import { formatRunResult } from "../core/formatters.js";
8
+ import { canSpend, recordSpend, requiresPolicyConfirmation } from "../core/spend-policy.js";
8
9
  import { storeFeedbackToken } from "./_token-cache.js";
9
10
  import { getOrCreatePendingCardSetup, formatCardSetupBlocks } from "../core/card-setup.js";
10
- import { formatPaymentChoicePrompt, formatPaymentLabel, formatRunConfirmationCommand, resolveConfirmationMethod, } from "./_payment-confirmation.js";
11
+ import { formatPaymentLabel, formatRunConfirmationCommand, resolveConfirmationMethod, } from "./_payment-confirmation.js";
11
12
  const POLL_INTERVAL_MS = 3000;
12
13
  const POLL_MAX_MS = 120000;
13
- async function pollJobUntilDone(jobId) {
14
+ async function pollJobUntilDone(jobId, paymentMethod) {
14
15
  const deadline = Date.now() + POLL_MAX_MS;
15
- const walletAddress = await getWalletAddress();
16
+ const walletAddress = await getWalletAddress(paymentMethod);
16
17
  const walletParam = walletAddress ? `?wallet=${walletAddress}` : "";
17
18
  while (Date.now() < deadline) {
18
19
  await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS));
@@ -60,7 +61,7 @@ export function registerRunTools(server) {
60
61
  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.", {
61
62
  agent_id: z.string().describe("Agent ID (UUID, slug, or name)"),
62
63
  input: z.record(z.unknown()).describe("Input payload for the agent"),
63
- pay_with: z.string().optional().describe("Payment method — wallet ID, chain name (tempo, base, etc.), or 'card'. Auto-detected if omitted."),
64
+ pay_with: z.string().trim().min(1).optional().describe("Payment method — wallet ID, chain name (tempo, base, etc.), or 'card'. Auto-detected if omitted."),
64
65
  confirmed: z.boolean().optional().describe("Set to true to confirm spending after seeing the price quote."),
65
66
  }, async ({ agent_id, input, pay_with, confirmed }) => {
66
67
  if (!hasWalletConfigured()) {
@@ -81,37 +82,40 @@ export function registerRunTools(server) {
81
82
  catch {
82
83
  return text(`Agent "${agent_id}" not found. Use search_agents to find available agents.`);
83
84
  }
84
- const price = parseFloat(agent.pricePer1kTokens ?? "0.01");
85
+ const price = parseFloat(agent.pricePerRunUsd ?? "0.01");
85
86
  const agentName = agent.name ?? agent_id;
86
- const creditPackInventory = await getCreditPackInventory(agent.id);
87
- const activeCreditPack = getActiveCreditPack(creditPackInventory);
88
- const configuredMethods = getConfiguredMethods();
89
- const compatibleMethods = getCompatiblePaymentMethods(agent, configuredMethods);
87
+ const compatibleMethods = getCompatiblePaymentMethods(agent, getConfiguredMethods());
90
88
  const pending = pendingRuns.get(agent.id);
91
89
  const requestedMethod = pay_with ?? pending?.method;
92
90
  const normalizedRequestedMethod = requestedMethod ? normalizePaymentMethod(requestedMethod) : null;
93
- if (!activeCreditPack && requestedMethod && !normalizedRequestedMethod) {
91
+ const creditPackInventory = await getCreditPackInventory(agent.id, requestedMethod);
92
+ const activeCreditPack = getActiveCreditPack(creditPackInventory);
93
+ if (requestedMethod && !normalizedRequestedMethod) {
94
94
  return text(`Payment method "${requestedMethod}" is not configured.\n\n` +
95
95
  "Use wallet_status to review your current payment methods.");
96
96
  }
97
- if (!activeCreditPack && normalizedRequestedMethod && !compatibleMethods.includes(normalizedRequestedMethod)) {
97
+ if (normalizedRequestedMethod && !compatibleMethods.includes(normalizedRequestedMethod)) {
98
98
  return text(`This agent cannot be paid with "${requestedMethod}".\n\n` +
99
99
  `Available payment methods for this agent: ${compatibleMethods.join(", ") || "none"}.\n` +
100
100
  "Use get_agent to inspect the agent details or choose another payment method.");
101
101
  }
102
- if (!activeCreditPack && !requestedMethod && compatibleMethods.length === 0) {
102
+ if (!activeCreditPack && compatibleMethods.length === 0) {
103
103
  return text(`No compatible payment methods are configured for ${agentName}.\n\n` +
104
- `Your configured methods: ${configuredMethods.join(", ") || "none"}\n` +
105
- `Agent accepts: ${agent.payment?.accepted_payments?.join(", ") || "unknown"}\n` +
106
- "Use wallet_status to review your current setup.");
104
+ "Use wallet_status to review your current payment methods.");
107
105
  }
108
- if (!activeCreditPack && !requestedMethod && compatibleMethods.length > 1) {
109
- return text(formatPaymentChoicePrompt(agentName, compatibleMethods, compatibleMethods.map((method) => formatRunConfirmationCommand(agent.id, method).replace(", confirmed: true", ""))));
106
+ const method = resolveConfirmationMethod(requestedMethod, pending?.method, compatibleMethods);
107
+ const spendCheckMethod = method ?? normalizedRequestedMethod ?? compatibleMethods[0];
108
+ if (!activeCreditPack) {
109
+ const spendCheck = canSpend({
110
+ method: spendCheckMethod,
111
+ amountUsd: price,
112
+ });
113
+ if (!spendCheck.ok) {
114
+ return text(spendCheck.message);
115
+ }
110
116
  }
111
- const method = activeCreditPack
112
- ? undefined
113
- : resolveConfirmationMethod(pay_with, pending?.method, compatibleMethods);
114
- if (!activeCreditPack && requiresSpendConfirmation() && !confirmed) {
117
+ const needsPolicyConfirmation = !activeCreditPack && requiresPolicyConfirmation(spendCheckMethod, price);
118
+ if (!activeCreditPack && (requiresSpendConfirmation() || needsPolicyConfirmation) && !confirmed) {
115
119
  pendingRuns.set(agent.id, {
116
120
  agent: { id: agent.id, name: agentName, price },
117
121
  input,
@@ -146,17 +150,29 @@ export function registerRunTools(server) {
146
150
  return text(`Error: ${msg}`);
147
151
  }
148
152
  let result;
153
+ let usedPaidMethod = !activeCreditPack;
149
154
  try {
150
- result = activeCreditPack
151
- ? await apiPost(`/agents/${agent.id}/run`, { input: processedInput }, { ensureConsumerPrincipal: true })
152
- : await apiPostWithPayment(`/agents/${agent.id}/run`, { input: processedInput }, method);
155
+ if (activeCreditPack) {
156
+ try {
157
+ result = await apiPost(`/agents/${agent.id}/run`, { input: processedInput }, { ensureConsumerPrincipal: true });
158
+ }
159
+ catch (packErr) {
160
+ const packApiErr = packErr;
161
+ if (packApiErr?.status !== 402)
162
+ throw packErr;
163
+ result = await apiPostWithPayment(`/agents/${agent.id}/run`, { input: processedInput }, method);
164
+ usedPaidMethod = true;
165
+ }
166
+ }
167
+ else {
168
+ result = await apiPostWithPayment(`/agents/${agent.id}/run`, { input: processedInput }, method);
169
+ }
170
+ if (usedPaidMethod) {
171
+ recordSpend(spendCheckMethod, price);
172
+ }
153
173
  }
154
174
  catch (err) {
155
175
  const apiErr = err;
156
- if (activeCreditPack && apiErr?.status === 402) {
157
- return text(`Your available credit packs for ${agentName} could not cover this run.\n\n` +
158
- "Use list_agent_credit_packs to inspect your balances, or retry with a payment method.");
159
- }
160
176
  if (apiErr?.status === 402) {
161
177
  const allMethods = getConfiguredMethods();
162
178
  const methodName = method ?? allMethods[0] ?? "auto";
@@ -189,7 +205,7 @@ export function registerRunTools(server) {
189
205
  const status = result.status;
190
206
  const usedCreditPack = result.consumption_mode === "credit_pack";
191
207
  if (status === "processing") {
192
- const pollResult = await pollJobUntilDone(jobId);
208
+ const pollResult = await pollJobUntilDone(jobId, method);
193
209
  if (pollResult.status === "completed") {
194
210
  const asyncFormatted = formatRunResult({
195
211
  ...result,
@@ -4,15 +4,16 @@ import { getOrCreatePendingCardSetup, formatCardSetupBlocks } from "../core/card
4
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";
9
- import { formatPaymentChoicePrompt, formatPaymentLabel, formatSolveConfirmationCommand, makeSolvePendingKey, resolveConfirmationMethod, } from "./_payment-confirmation.js";
10
+ import { formatPaymentLabel, formatSolveConfirmationCommand, makeSolvePendingKey, resolveConfirmationMethod, } from "./_payment-confirmation.js";
10
11
  import { getActiveCreditPack, getCreditPackInventory, getCreditPackProgram } from "../core/passes.js";
11
12
  const POLL_INTERVAL_MS = 3000;
12
13
  const POLL_MAX_MS = 120000;
13
- async function pollSolveJob(jobId) {
14
+ async function pollSolveJob(jobId, paymentMethod) {
14
15
  const deadline = Date.now() + POLL_MAX_MS;
15
- const walletAddress = await getWalletAddress();
16
+ const walletAddress = await getWalletAddress(paymentMethod);
16
17
  const walletParam = walletAddress ? `?wallet=${walletAddress}` : "";
17
18
  while (Date.now() < deadline) {
18
19
  await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS));
@@ -103,6 +104,8 @@ export function registerSolveTools(server) {
103
104
  .describe("Maximum budget in USD"),
104
105
  pay_with: z
105
106
  .string()
107
+ .trim()
108
+ .min(1)
106
109
  .optional()
107
110
  .describe("Payment method — wallet ID, chain name (tempo, base, etc.), or 'card'. Auto-detected if omitted."),
108
111
  confirmed: z
@@ -157,10 +160,12 @@ export function registerSolveTools(server) {
157
160
  return multiText(formatRunResult(result), feedbackAsk(jobId, agentId, usedCreditPack ? undefined : cost, tipMsg));
158
161
  }
159
162
  catch (err) {
160
- const isAuthError = err instanceof Error &&
161
- "status" in err &&
162
- err.status === 401;
163
- if (!isAuthError || !hasWalletConfigured())
163
+ const status = err instanceof Error &&
164
+ "status" in err
165
+ ? err.status
166
+ : undefined;
167
+ const isRecoverableDirectSolveError = status === 401 || status === 402;
168
+ if (!isRecoverableDirectSolveError || !hasWalletConfigured())
164
169
  throw err;
165
170
  }
166
171
  const params = new URLSearchParams({ q: intent, limit: "5" });
@@ -175,41 +180,28 @@ export function registerSolveTools(server) {
175
180
  return text(`No agents found matching "${intent}".`);
176
181
  }
177
182
  const discovery = agentList(agents, intent);
178
- const inputTokens = Math.ceil(JSON.stringify(input).length / 4);
179
183
  const affordable = agents.filter((agent) => {
180
- const price = parseFloat(agent.pricePer1kTokens ?? "0.01");
181
- const cost = agent.pricingModel === "fixed" ? price : (inputTokens / 1000) * price;
182
- return cost <= budget;
184
+ const price = parseFloat(agent.pricePerRunUsd ?? "0.01");
185
+ return price <= budget;
183
186
  });
184
187
  const selected = affordable[0] ?? agents[0];
185
- const activeCreditPack = await getCreditPackInventory(selected.id).then(getActiveCreditPack);
188
+ const activeCreditPack = await getCreditPackInventory(selected.id, requestedMethod).then(getActiveCreditPack);
186
189
  const compatibleMethods = getCompatiblePaymentMethods(selected, configuredMethods);
187
- if (!activeCreditPack && normalizedRequestedMethod && !compatibleMethods.includes(normalizedRequestedMethod)) {
190
+ if (normalizedRequestedMethod && !compatibleMethods.includes(normalizedRequestedMethod)) {
188
191
  return text(`The best matching agent cannot be paid with "${requestedMethod}".\n\n` +
189
192
  `Available payment methods for ${selected.name}: ${compatibleMethods.join(", ") || "none"}.\n` +
190
193
  "Choose another payment method or refine your search.");
191
194
  }
192
- if (!activeCreditPack && !requestedMethod && compatibleMethods.length === 0) {
195
+ if (!activeCreditPack && compatibleMethods.length === 0) {
193
196
  return text(`No compatible payment methods are configured for ${selected.name}.\n\n` +
194
- `Your configured methods: ${configuredMethods.join(", ") || "none"}\n` +
195
- `Agent accepts: ${selected.payment?.accepted_payments?.join(", ") || "unknown"}\n` +
196
- "Use wallet_status to review your current setup.");
197
- }
198
- if (!activeCreditPack && !requestedMethod && compatibleMethods.length > 1) {
199
- return text([
200
- discovery,
201
- "",
202
- formatPaymentChoicePrompt(selected.name ?? selected.id, compatibleMethods, compatibleMethods.map((method) => formatSolveConfirmationCommand(intent, budget, method).replace(", confirmed: true", ""))),
203
- ].join("\n"));
197
+ "Use wallet_status to review your current payment methods.");
204
198
  }
205
- const method = activeCreditPack
206
- ? undefined
207
- : resolveConfirmationMethod(pay_with, pending?.method, compatibleMethods);
208
- const selectedPrice = parseFloat(selected.pricePer1kTokens ?? "0.01");
209
- const estimatedCost = selected.pricingModel === "fixed"
210
- ? selectedPrice
211
- : (inputTokens / 1000) * selectedPrice;
212
- if (!activeCreditPack && requiresSpendConfirmation() && !confirmed) {
199
+ const method = resolveConfirmationMethod(requestedMethod, pending?.method, compatibleMethods);
200
+ const spendCheckMethod = method ?? normalizedRequestedMethod ?? compatibleMethods[0];
201
+ const selectedPrice = parseFloat(selected.pricePerRunUsd ?? "0.01");
202
+ const estimatedCost = selectedPrice;
203
+ const needsPolicyConfirmation = !activeCreditPack && requiresPolicyConfirmation(spendCheckMethod, estimatedCost);
204
+ if (!activeCreditPack && (requiresSpendConfirmation() || needsPolicyConfirmation) && !confirmed) {
213
205
  pendingSolves.set(pendingKey, { method });
214
206
  return text([
215
207
  discovery,
@@ -229,17 +221,38 @@ export function registerSolveTools(server) {
229
221
  ].join("\n"));
230
222
  }
231
223
  let result;
224
+ if (!activeCreditPack) {
225
+ const spendCheck = canSpend({
226
+ method: spendCheckMethod,
227
+ amountUsd: estimatedCost,
228
+ });
229
+ if (!spendCheck.ok) {
230
+ return text(spendCheck.message);
231
+ }
232
+ }
232
233
  try {
233
- result = activeCreditPack
234
- ? await apiPost(`/agents/${selected.id}/run`, { input: processedInput }, { ensureConsumerPrincipal: true })
235
- : await apiPostWithPayment(`/agents/${selected.id}/run`, { input: processedInput }, method);
234
+ let usedPaidMethod = !activeCreditPack;
235
+ if (activeCreditPack) {
236
+ try {
237
+ result = await apiPost(`/agents/${selected.id}/run`, { input: processedInput }, { ensureConsumerPrincipal: true });
238
+ }
239
+ catch (packErr) {
240
+ const packApiErr = packErr;
241
+ if (packApiErr?.status !== 402)
242
+ throw packErr;
243
+ result = await apiPostWithPayment(`/agents/${selected.id}/run`, { input: processedInput }, method);
244
+ usedPaidMethod = true;
245
+ }
246
+ }
247
+ else {
248
+ result = await apiPostWithPayment(`/agents/${selected.id}/run`, { input: processedInput }, method);
249
+ }
250
+ if (usedPaidMethod) {
251
+ recordSpend(spendCheckMethod, estimatedCost);
252
+ }
236
253
  }
237
254
  catch (err) {
238
255
  const apiErr = err;
239
- if (activeCreditPack && apiErr?.status === 402) {
240
- return text(`Your available credit packs for ${selected.name} could not cover this run.\n\n` +
241
- "Use list_agent_credit_packs to inspect your balances, or retry with a payment method.");
242
- }
243
256
  if (apiErr?.status === 402) {
244
257
  return text([
245
258
  "Payment failed — your wallet may not have enough funds or the selected method was rejected.",
@@ -262,7 +275,7 @@ export function registerSolveTools(server) {
262
275
  storeFeedbackToken(jobId, result.feedback_token, agentId);
263
276
  }
264
277
  if (status === "processing") {
265
- const pollResult = await pollSolveJob(jobId);
278
+ const pollResult = await pollSolveJob(jobId, method);
266
279
  if (pollResult.status === "completed") {
267
280
  const asyncFormatted = formatRunResult({
268
281
  ...result,
@@ -1,6 +1,8 @@
1
1
  import { z } from "zod";
2
- import { getWallets, getCardConfig, setCardConfig, addWallet, getPendingCardSetupToken, } from "../core/config.js";
3
- import { getWalletAddress } from "../core/payments.js";
2
+ import { getWallets, getCardConfig, setCardConfig, addWallet, getPendingCardSetupToken, getSpendPolicy, setSpendPolicy, } from "../core/config.js";
3
+ import { getWalletAddress, isCardPaymentEnabled } from "../core/payments.js";
4
+ import { fetchUsdcBalance } from "../core/balances.js";
5
+ import { getSettings } from "../core/settings.js";
4
6
  import { getOrCreatePendingCardSetup, formatCardSetupBlocks, getCardCapabilities, pollCardSetup, } from "../core/card-setup.js";
5
7
  import { isOwsAvailable, createOwsWallet, importKeyToOws, listOwsWallets, listOwsWalletsByChain, } from "../core/ows-adapter.js";
6
8
  import { ensureConsumerPrincipal, getConsumerPrincipal } from "../core/principal.js";
@@ -17,18 +19,33 @@ export function registerWalletTools(server) {
17
19
  if (wallets.length === 0 && !card && !pendingCardSetupToken) {
18
20
  return text("No payment methods configured.\nUse wallet_setup to create or import a wallet.");
19
21
  }
20
- const lines = ["Payment methods:"];
22
+ const settings = await getSettings();
23
+ const networkLabel = settings ? ` (${settings.network})` : "";
24
+ const lines = [`Payment methods${networkLabel}:`];
21
25
  for (const w of wallets) {
22
26
  const label = w.label ? ` (${w.label})` : "";
23
27
  const storage = w.keyType === "ows" ? " [encrypted]" : " [plaintext]";
24
- const chainAddresses = await Promise.all(w.chains.map(async (chainName) => {
28
+ const chainLines = await Promise.all(w.chains.map(async (chainName) => {
25
29
  const addr = await getWalletAddress(chainName);
26
- return `${chainName}: ${addr ?? "unknown"}`;
30
+ if (!addr)
31
+ return `${chainName}: unknown`;
32
+ let balanceStr = "";
33
+ if (chainName === "tempo" || chainName === "base" || chainName === "solana") {
34
+ const balance = await fetchUsdcBalance(chainName, addr);
35
+ if (balance !== null) {
36
+ const num = Number(balance);
37
+ balanceStr = ` ${Number.isFinite(num) ? num.toFixed(4).replace(/\.?0+$/, "") : balance} USDC`;
38
+ }
39
+ }
40
+ return `${chainName}: ${addr}${balanceStr}`;
27
41
  }));
28
- lines.push(` ${w.id}${label}${storage}: ${chainAddresses.join(" | ")}`);
42
+ lines.push(` ${w.id}${label}${storage}:`);
43
+ for (const line of chainLines)
44
+ lines.push(` ${line}`);
29
45
  }
30
- if (card) {
31
- lines.push(` Card: ${card.brand} ****${card.last4}`);
46
+ if (card && isCardPaymentEnabled()) {
47
+ const stripeMode = settings?.stripe.mode === "test" ? " [Stripe test mode]" : "";
48
+ lines.push(` Card: ${card.brand} ****${card.last4}${stripeMode}`);
32
49
  const capabilities = await getCardCapabilities();
33
50
  if (capabilities.spt_status === "enabled") {
34
51
  lines.push(" Card MPP: ready");
@@ -49,7 +66,7 @@ export function registerWalletTools(server) {
49
66
  return text(lines.join("\n"));
50
67
  });
51
68
  // ── wallet_setup (NEW) ──────────────────────────────────────────
52
- server.tool("wallet_setup", "Set up or manage payment methods. Options: 'add-card' to connect a credit/debit card (recommended), 'remove-card' to disconnect a card, 'create' a crypto wallet, or 'import' an existing key. For crypto wallet changes (removal, key rotation), direct users to edit their config files manually — never handle private keys programmatically.", {
69
+ server.tool("wallet_setup", "Set up or manage payment methods. Options: 'add-card' to connect a credit/debit card, 'remove-card' to disconnect a card, 'create' a crypto wallet, or 'import' an existing key. After card setup, use wallet_status to confirm whether card-backed MPP is ready. For crypto wallet changes (removal, key rotation), direct users to edit their config files manually — never handle private keys programmatically.", {
53
70
  action: z
54
71
  .enum(["create", "import", "add-card", "remove-card"])
55
72
  .describe("'add-card' to connect a card, 'remove-card' to disconnect it, 'create' a crypto wallet, or 'import' an existing key"),
@@ -66,6 +83,9 @@ export function registerWalletTools(server) {
66
83
  }, async ({ action, name, key, chain }) => {
67
84
  // ── Card setup flow ──────────────────────────────────────
68
85
  if (action === "add-card") {
86
+ if (!isCardPaymentEnabled()) {
87
+ return text("Card payments are temporarily unavailable. Use wallet_setup({ action: \"create\" }) for a crypto wallet instead.");
88
+ }
69
89
  const existing = getCardConfig();
70
90
  if (existing) {
71
91
  return text(`Card already connected: ${existing.brand} ****${existing.last4}\n\nTo replace it, remove the current card first.`);
@@ -257,8 +277,8 @@ export function registerWalletTools(server) {
257
277
  }
258
278
  });
259
279
  // ── wallet_set_policy (NEW) ─────────────────────────────────────
260
- server.tool("wallet_set_policy", "Set spending limits on a wallet to control agent costs. Limits reset daily.", {
261
- wallet_id: z.string().describe("Wallet ID to set policy on"),
280
+ server.tool("wallet_set_policy", "Set client-side spending limits on a wallet to control agent costs in this MCP client. Limits reset daily.", {
281
+ wallet_id: z.string().describe("Wallet ID to set local policy on"),
262
282
  max_per_tx: z
263
283
  .number()
264
284
  .positive()
@@ -269,31 +289,47 @@ export function registerWalletTools(server) {
269
289
  .positive()
270
290
  .optional()
271
291
  .describe("Maximum USD per day across all transactions"),
272
- }, async ({ wallet_id, max_per_tx, max_per_day }) => {
292
+ require_confirmation_above: z
293
+ .number()
294
+ .positive()
295
+ .optional()
296
+ .describe("Require manual confirmation above this USD amount even when auto-spend is enabled"),
297
+ }, async ({ wallet_id, max_per_tx, max_per_day, require_confirmation_above }) => {
273
298
  const wallets = getWallets();
274
299
  const wallet = wallets.find((w) => w.id === wallet_id);
275
300
  if (!wallet) {
276
301
  const available = wallets.map((w) => w.id).join(", ") || "none";
277
302
  return text(`Wallet "${wallet_id}" not found. Available wallets: ${available}`);
278
303
  }
279
- if (max_per_tx == null && max_per_day == null) {
280
- return text("No policy changes specified. Provide max_per_tx and/or max_per_day.");
304
+ if (max_per_tx == null &&
305
+ max_per_day == null &&
306
+ require_confirmation_above == null) {
307
+ return text("No policy changes specified. Provide at least one policy field.");
281
308
  }
309
+ const existing = getSpendPolicy(wallet_id) ?? {};
310
+ const nextPolicy = {
311
+ ...existing,
312
+ ...(max_per_tx != null ? { maxPerTxUsd: max_per_tx } : {}),
313
+ ...(max_per_day != null ? { maxPerDayUsd: max_per_day } : {}),
314
+ ...(require_confirmation_above != null ? { requireConfirmationAboveUsd: require_confirmation_above } : {}),
315
+ };
316
+ setSpendPolicy(wallet_id, nextPolicy);
282
317
  // Build policy summary
283
318
  const policies = [];
284
- if (max_per_tx != null) {
285
- policies.push(`Max per transaction: $${max_per_tx.toFixed(2)}`);
319
+ if (nextPolicy.maxPerTxUsd != null) {
320
+ policies.push(`Max per transaction: $${nextPolicy.maxPerTxUsd.toFixed(2)}`);
321
+ }
322
+ if (nextPolicy.maxPerDayUsd != null) {
323
+ policies.push(`Max per day: $${nextPolicy.maxPerDayUsd.toFixed(2)}`);
286
324
  }
287
- if (max_per_day != null) {
288
- policies.push(`Max per day: $${max_per_day.toFixed(2)}`);
325
+ if (nextPolicy.requireConfirmationAboveUsd != null) {
326
+ policies.push(`Require confirmation above: $${nextPolicy.requireConfirmationAboveUsd.toFixed(2)}`);
289
327
  }
290
- // Note: actual persistence depends on the config module supporting policy fields.
291
- // For now, we report what would be set. The core/config module will handle storage.
292
328
  return text([
293
- `Spending policy set for wallet "${wallet_id}":`,
329
+ `Local spending policy set for wallet "${wallet_id}":`,
294
330
  ...policies.map((p) => ` ${p}`),
295
331
  "",
296
- "Policy will be enforced on all future transactions from this wallet.",
332
+ "Policy is stored in this MCP client's local config and enforced on future transactions from this wallet on this client.",
297
333
  ].join("\n"));
298
334
  });
299
335
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentwonderland/mcp",
3
- "version": "0.1.24",
3
+ "version": "0.1.26",
4
4
  "type": "module",
5
5
  "description": "MCP server for the Agent Wonderland AI agent marketplace",
6
6
  "bin": {
@@ -25,7 +25,7 @@
25
25
  "@modelcontextprotocol/sdk": "^1.12.1",
26
26
  "@solana/spl-token": "^0.4.14",
27
27
  "@solana/web3.js": "^1.98.4",
28
- "mppx": "^0.4.9",
28
+ "mppx": "^0.5.10",
29
29
  "qrcode": "^1.5.4",
30
30
  "viem": "^2.47.6",
31
31
  "zod": "^3.24.0"
@@ -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
+ });
@@ -0,0 +1,78 @@
1
+ import { beforeEach, describe, expect, it, vi } from "vitest";
2
+
3
+ const {
4
+ mockGetApiUrl,
5
+ mockGetApiKey,
6
+ mockGetPaymentFetch,
7
+ mockEnsureConsumerPrincipalForMethod,
8
+ mockGetConsumerPrincipalForMethod,
9
+ mockGetBaseRebatePrincipal,
10
+ mockPaymentFetch,
11
+ } = vi.hoisted(() => ({
12
+ mockGetApiUrl: vi.fn(),
13
+ mockGetApiKey: vi.fn(),
14
+ mockGetPaymentFetch: vi.fn(),
15
+ mockEnsureConsumerPrincipalForMethod: vi.fn(),
16
+ mockGetConsumerPrincipalForMethod: vi.fn(),
17
+ mockGetBaseRebatePrincipal: vi.fn(),
18
+ mockPaymentFetch: vi.fn(),
19
+ }));
20
+
21
+ vi.mock("../config.js", () => ({
22
+ getApiUrl: () => mockGetApiUrl(),
23
+ getApiKey: () => mockGetApiKey(),
24
+ }));
25
+
26
+ vi.mock("../payments.js", () => ({
27
+ getPaymentFetch: (...args: unknown[]) => mockGetPaymentFetch(...args),
28
+ }));
29
+
30
+ vi.mock("../principal.js", () => ({
31
+ ensureConsumerPrincipal: vi.fn(),
32
+ ensureConsumerPrincipalForMethod: (...args: unknown[]) => mockEnsureConsumerPrincipalForMethod(...args),
33
+ getConsumerPrincipal: vi.fn(),
34
+ getConsumerPrincipalForMethod: (...args: unknown[]) => mockGetConsumerPrincipalForMethod(...args),
35
+ getBaseRebatePrincipal: (...args: unknown[]) => mockGetBaseRebatePrincipal(...args),
36
+ }));
37
+
38
+ describe("api-client headers", () => {
39
+ beforeEach(() => {
40
+ vi.clearAllMocks();
41
+ mockGetApiUrl.mockReturnValue("https://api.agentwonderland.test");
42
+ mockGetApiKey.mockReturnValue(null);
43
+ mockGetPaymentFetch.mockResolvedValue(mockPaymentFetch);
44
+ mockEnsureConsumerPrincipalForMethod.mockResolvedValue(
45
+ "did:pkh:solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp:42W2HfLfveSm1T5et9WTLp2CZ2QXdF2EYCUvyJ2gPpxF",
46
+ );
47
+ mockGetConsumerPrincipalForMethod.mockResolvedValue(
48
+ "did:pkh:eip155:8453:0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
49
+ );
50
+ mockGetBaseRebatePrincipal.mockResolvedValue(
51
+ "did:pkh:eip155:8453:0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
52
+ );
53
+ mockPaymentFetch.mockResolvedValue(new Response(JSON.stringify({ ok: true }), {
54
+ status: 200,
55
+ headers: { "content-type": "application/json" },
56
+ }));
57
+ });
58
+
59
+ it("includes the Base rebate principal alongside the method-specific consumer principal", async () => {
60
+ const { apiPostWithPayment } = await import("../api-client.js");
61
+
62
+ await apiPostWithPayment("/agents/agent-1/run", { input: { text: "hello" } }, "solana");
63
+
64
+ expect(mockPaymentFetch).toHaveBeenCalledWith(
65
+ "https://api.agentwonderland.test/agents/agent-1/run",
66
+ expect.objectContaining({
67
+ headers: expect.objectContaining({
68
+ "Content-Type": "application/json",
69
+ Accept: "application/json",
70
+ "X-AW-Consumer-Principal":
71
+ "did:pkh:solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp:42W2HfLfveSm1T5et9WTLp2CZ2QXdF2EYCUvyJ2gPpxF",
72
+ "X-AW-Rebate-Principal":
73
+ "did:pkh:eip155:8453:0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
74
+ }),
75
+ }),
76
+ );
77
+ });
78
+ });
@@ -14,4 +14,16 @@ describe("formatRunResult", () => {
14
14
  expect(output).toContain("Covered by credit pack (pack-123)");
15
15
  expect(output).not.toContain("Paid:");
16
16
  });
17
+
18
+ it("formats string-valued settled amounts from job lookups", () => {
19
+ const output = formatRunResult({
20
+ agent_name: "Async Analyzer",
21
+ status: "completed",
22
+ settled_amount: "0.100000",
23
+ job_id: "job-123",
24
+ }, { paymentMethod: "card" });
25
+
26
+ expect(output).toContain("Paid: $0.10 via card");
27
+ expect(output).toContain("Job ID: job-123");
28
+ });
17
29
  });