@agentwonderland/mcp 0.1.25 → 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 (71) hide show
  1. package/dist/core/__tests__/api-client.test.d.ts +1 -0
  2. package/dist/core/__tests__/api-client.test.js +51 -0
  3. package/dist/core/__tests__/formatters.test.js +10 -0
  4. package/dist/core/__tests__/passes-api.test.d.ts +1 -0
  5. package/dist/core/__tests__/passes-api.test.js +27 -0
  6. package/dist/core/__tests__/payments.test.js +10 -6
  7. package/dist/core/__tests__/principal.test.js +41 -4
  8. package/dist/core/__tests__/solana-charge.test.d.ts +1 -0
  9. package/dist/core/__tests__/solana-charge.test.js +50 -0
  10. package/dist/core/api-client.d.ts +1 -0
  11. package/dist/core/api-client.js +8 -3
  12. package/dist/core/balances.d.ts +1 -0
  13. package/dist/core/balances.js +56 -0
  14. package/dist/core/base-charge.js +13 -6
  15. package/dist/core/formatters.d.ts +3 -2
  16. package/dist/core/formatters.js +7 -1
  17. package/dist/core/passes.d.ts +1 -1
  18. package/dist/core/passes.js +5 -2
  19. package/dist/core/payments.d.ts +1 -0
  20. package/dist/core/payments.js +20 -7
  21. package/dist/core/principal.d.ts +3 -0
  22. package/dist/core/principal.js +29 -1
  23. package/dist/core/settings.d.ts +20 -0
  24. package/dist/core/settings.js +19 -0
  25. package/dist/core/solana-charge.d.ts +5 -0
  26. package/dist/core/solana-charge.js +29 -7
  27. package/dist/core/tempo-charge.d.ts +7 -0
  28. package/dist/core/tempo-charge.js +84 -0
  29. package/dist/index.js +5 -4
  30. package/dist/tools/__tests__/jobs.test.d.ts +1 -0
  31. package/dist/tools/__tests__/jobs.test.js +71 -0
  32. package/dist/tools/__tests__/run.test.d.ts +1 -0
  33. package/dist/tools/__tests__/run.test.js +149 -0
  34. package/dist/tools/__tests__/solve.test.d.ts +1 -0
  35. package/dist/tools/__tests__/solve.test.js +158 -0
  36. package/dist/tools/__tests__/wallet.test.d.ts +1 -0
  37. package/dist/tools/__tests__/wallet.test.js +230 -0
  38. package/dist/tools/_payment-confirmation.js +1 -1
  39. package/dist/tools/jobs.js +8 -1
  40. package/dist/tools/passes.js +11 -6
  41. package/dist/tools/run.js +16 -12
  42. package/dist/tools/solve.js +22 -15
  43. package/dist/tools/wallet.js +32 -12
  44. package/package.json +2 -2
  45. package/src/core/__tests__/api-client.test.ts +78 -0
  46. package/src/core/__tests__/formatters.test.ts +12 -0
  47. package/src/core/__tests__/passes-api.test.ts +33 -0
  48. package/src/core/__tests__/payments.test.ts +17 -6
  49. package/src/core/__tests__/principal.test.ts +49 -4
  50. package/src/core/__tests__/solana-charge.test.ts +59 -0
  51. package/src/core/api-client.ts +16 -3
  52. package/src/core/balances.ts +63 -0
  53. package/src/core/base-charge.ts +13 -6
  54. package/src/core/formatters.ts +10 -3
  55. package/src/core/passes.ts +5 -2
  56. package/src/core/payments.ts +22 -7
  57. package/src/core/principal.ts +42 -1
  58. package/src/core/settings.ts +36 -0
  59. package/src/core/solana-charge.ts +43 -9
  60. package/src/core/tempo-charge.ts +104 -0
  61. package/src/index.ts +5 -4
  62. package/src/tools/__tests__/jobs.test.ts +89 -0
  63. package/src/tools/__tests__/run.test.ts +176 -0
  64. package/src/tools/__tests__/solve.test.ts +186 -0
  65. package/src/tools/__tests__/wallet.test.ts +289 -0
  66. package/src/tools/_payment-confirmation.ts +1 -1
  67. package/src/tools/jobs.ts +10 -1
  68. package/src/tools/passes.ts +11 -5
  69. package/src/tools/run.ts +19 -11
  70. package/src/tools/solve.ts +25 -14
  71. package/src/tools/wallet.ts +30 -14
@@ -0,0 +1,230 @@
1
+ import { beforeEach, describe, expect, it, vi } from "vitest";
2
+ const state = vi.hoisted(() => ({
3
+ wallets: [],
4
+ addedWallets: [],
5
+ card: null,
6
+ pendingCardSetupToken: null,
7
+ spendPolicies: {},
8
+ consumerPrincipal: "did:pkh:eip155:8453:0xabc",
9
+ owsAvailable: true,
10
+ owsWallets: [],
11
+ owsWalletsByChain: [],
12
+ createdWalletCalls: [],
13
+ importWalletCalls: [],
14
+ createdWalletResult: {
15
+ walletId: "ows-wallet-created",
16
+ address: "0x1111111111111111111111111111111111111111",
17
+ },
18
+ importWalletResult: {
19
+ walletId: "ows-wallet-imported",
20
+ address: "0x2222222222222222222222222222222222222222",
21
+ },
22
+ cardSetup: {
23
+ url: "https://api.agentwonderland.com/card/handoff/setup-token",
24
+ token: "setup-token",
25
+ isNew: true,
26
+ },
27
+ cardSetupBlocks: [
28
+ "Open this setup page to connect your card:\n\nhttps://api.agentwonderland.com/card/handoff/setup-token",
29
+ ],
30
+ pollCardSetupCalls: [],
31
+ pollCardSetupResult: null,
32
+ setCardConfigCalls: [],
33
+ }));
34
+ vi.mock("../../core/config.js", () => ({
35
+ addWallet: (wallet) => {
36
+ state.addedWallets.push(wallet);
37
+ const existing = state.wallets.findIndex((entry) => entry.id === wallet.id);
38
+ if (existing >= 0) {
39
+ state.wallets[existing] = wallet;
40
+ }
41
+ else {
42
+ state.wallets.push(wallet);
43
+ }
44
+ },
45
+ getCardConfig: () => state.card,
46
+ getPendingCardSetupToken: () => state.pendingCardSetupToken,
47
+ getSpendPolicy: (walletId) => state.spendPolicies[walletId] ?? null,
48
+ getWallets: () => state.wallets,
49
+ setCardConfig: (card) => {
50
+ state.card = card;
51
+ state.setCardConfigCalls.push(card);
52
+ },
53
+ setSpendPolicy: (walletId, policy) => {
54
+ state.spendPolicies[walletId] = policy;
55
+ },
56
+ }));
57
+ vi.mock("../../core/payments.js", () => ({
58
+ getWalletAddress: async () => null,
59
+ }));
60
+ vi.mock("../../core/card-setup.js", () => ({
61
+ formatCardSetupBlocks: () => state.cardSetupBlocks,
62
+ getCardCapabilities: async () => ({
63
+ spt_status: "enabled",
64
+ }),
65
+ getOrCreatePendingCardSetup: async () => state.cardSetup,
66
+ pollCardSetup: async (token, timeoutMs) => {
67
+ state.pollCardSetupCalls.push({ token, timeoutMs });
68
+ return state.pollCardSetupResult;
69
+ },
70
+ }));
71
+ vi.mock("../../core/ows-adapter.js", () => ({
72
+ createOwsWallet: async (name, chain) => {
73
+ state.createdWalletCalls.push({ name, chain });
74
+ return state.createdWalletResult;
75
+ },
76
+ importKeyToOws: async (privateKey, name, chain) => {
77
+ state.importWalletCalls.push({ privateKey, name, chain });
78
+ return state.importWalletResult;
79
+ },
80
+ isOwsAvailable: async () => state.owsAvailable,
81
+ listOwsWallets: async () => state.owsWallets,
82
+ listOwsWalletsByChain: async () => state.owsWalletsByChain,
83
+ }));
84
+ vi.mock("../../core/principal.js", () => ({
85
+ ensureConsumerPrincipal: async () => state.consumerPrincipal,
86
+ getConsumerPrincipal: async () => state.consumerPrincipal,
87
+ }));
88
+ function resetState() {
89
+ state.wallets = [];
90
+ state.addedWallets = [];
91
+ state.card = null;
92
+ state.pendingCardSetupToken = null;
93
+ state.spendPolicies = {};
94
+ state.consumerPrincipal = "did:pkh:eip155:8453:0xabc";
95
+ state.owsAvailable = true;
96
+ state.owsWallets = [];
97
+ state.owsWalletsByChain = [];
98
+ state.createdWalletCalls = [];
99
+ state.importWalletCalls = [];
100
+ state.createdWalletResult = {
101
+ walletId: "ows-wallet-created",
102
+ address: "0x1111111111111111111111111111111111111111",
103
+ };
104
+ state.importWalletResult = {
105
+ walletId: "ows-wallet-imported",
106
+ address: "0x2222222222222222222222222222222222222222",
107
+ };
108
+ state.cardSetup = {
109
+ url: "https://api.agentwonderland.com/card/handoff/setup-token",
110
+ token: "setup-token",
111
+ isNew: true,
112
+ };
113
+ state.cardSetupBlocks = [
114
+ "Open this setup page to connect your card:\n\nhttps://api.agentwonderland.com/card/handoff/setup-token",
115
+ ];
116
+ state.pollCardSetupCalls = [];
117
+ state.pollCardSetupResult = null;
118
+ state.setCardConfigCalls = [];
119
+ }
120
+ async function getWalletSetupTool() {
121
+ const tools = new Map();
122
+ const server = {
123
+ tool: (name, _description, _schema, handler) => {
124
+ tools.set(name, handler);
125
+ },
126
+ };
127
+ const { registerWalletTools } = await import("../wallet.js");
128
+ registerWalletTools(server);
129
+ const walletSetup = tools.get("wallet_setup");
130
+ if (!walletSetup) {
131
+ throw new Error("wallet_setup tool was not registered");
132
+ }
133
+ return walletSetup;
134
+ }
135
+ function flattenText(result) {
136
+ return result.content.map((item) => item.text).join("\n");
137
+ }
138
+ describe("wallet_setup tool", () => {
139
+ beforeEach(() => {
140
+ vi.resetModules();
141
+ resetState();
142
+ });
143
+ it("creates an encrypted OWS wallet for Tempo/Base with Base as the default chain", async () => {
144
+ const walletSetup = await getWalletSetupTool();
145
+ const result = await walletSetup({ action: "create", name: "launch-wallet", chain: "base" });
146
+ const text = flattenText(result);
147
+ expect(state.createdWalletCalls).toEqual([
148
+ { name: "launch-wallet", chain: "evm" },
149
+ ]);
150
+ expect(state.addedWallets).toEqual([
151
+ {
152
+ id: "launch-wallet",
153
+ keyType: "ows",
154
+ owsWalletId: "ows-wallet-created",
155
+ chains: ["tempo", "base"],
156
+ defaultChain: "base",
157
+ label: "launch-wallet",
158
+ },
159
+ ]);
160
+ expect(text).toContain("Wallet created [encrypted]:");
161
+ expect(text).toContain("Address: 0x1111111111111111111111111111111111111111");
162
+ expect(text).toContain("Chains: tempo, base");
163
+ expect(text).toContain("Consumer principal: did:pkh:eip155:8453:0xabc");
164
+ });
165
+ it("imports a wallet into OWS encrypted storage with Base as the default chain", async () => {
166
+ const walletSetup = await getWalletSetupTool();
167
+ const result = await walletSetup({
168
+ action: "import",
169
+ key: "0x1234",
170
+ name: "imported-wallet",
171
+ chain: "base",
172
+ });
173
+ const text = flattenText(result);
174
+ expect(state.importWalletCalls).toEqual([
175
+ { privateKey: "0x1234", name: "imported-wallet", chain: "evm" },
176
+ ]);
177
+ expect(state.addedWallets).toEqual([
178
+ {
179
+ id: "imported-wallet",
180
+ keyType: "ows",
181
+ owsWalletId: "ows-wallet-imported",
182
+ chains: ["tempo", "base"],
183
+ defaultChain: "base",
184
+ label: "imported-wallet",
185
+ },
186
+ ]);
187
+ expect(text).toContain("Key imported to OWS [encrypted]:");
188
+ expect(text).toContain("Address: 0x2222222222222222222222222222222222222222");
189
+ expect(text).toContain("Consumer principal: did:pkh:eip155:8453:0xabc");
190
+ });
191
+ it("starts card setup when no card is connected", async () => {
192
+ const walletSetup = await getWalletSetupTool();
193
+ const result = await walletSetup({ action: "add-card" });
194
+ const text = flattenText(result);
195
+ expect(text).toContain("Open this setup page to connect your card:");
196
+ expect(text).toContain("https://api.agentwonderland.com/card/handoff/setup-token");
197
+ expect(state.pollCardSetupCalls).toEqual([]);
198
+ });
199
+ it("completes a pending card setup when Stripe handoff has finished", async () => {
200
+ state.pendingCardSetupToken = "setup-token";
201
+ state.pollCardSetupResult = {
202
+ brand: "Visa",
203
+ last4: "4242",
204
+ consumerToken: "consumer-token",
205
+ };
206
+ const walletSetup = await getWalletSetupTool();
207
+ const result = await walletSetup({ action: "add-card" });
208
+ const text = flattenText(result);
209
+ expect(state.pollCardSetupCalls).toEqual([
210
+ { token: "setup-token", timeoutMs: 250 },
211
+ ]);
212
+ expect(text).toContain("Connected! Visa ****4242 is ready for payments.");
213
+ expect(text).toContain("Consumer principal: did:pkh:eip155:8453:0xabc");
214
+ });
215
+ it("removes the connected card", async () => {
216
+ state.card = {
217
+ consumerToken: "consumer-token",
218
+ paymentMethodId: "pm_123",
219
+ last4: "4242",
220
+ brand: "Visa",
221
+ };
222
+ const walletSetup = await getWalletSetupTool();
223
+ const result = await walletSetup({ action: "remove-card" });
224
+ const text = flattenText(result);
225
+ expect(state.setCardConfigCalls).toEqual([null]);
226
+ expect(state.card).toBeNull();
227
+ expect(text).toContain("Removed Visa ****4242.");
228
+ expect(text).toContain("Card disconnected from Agent Wonderland.");
229
+ });
230
+ });
@@ -23,6 +23,6 @@ export function formatPaymentChoicePrompt(subject, methods, commands) {
23
23
  ...commands,
24
24
  "",
25
25
  `Available methods: ${methods.map((method) => `"${method}"`).join(", ")}`,
26
- "For fully agentic execution, include pay_with explicitly.",
26
+ "You can omit pay_with to use the default compatible method.",
27
27
  ].join("\n");
28
28
  }
@@ -11,7 +11,14 @@ export function registerJobTools(server) {
11
11
  server.tool("get_job", "Get the status and output of a job by ID. Use to poll async jobs until they complete.", {
12
12
  job_id: z.string().describe("Job ID (UUID)"),
13
13
  }, async ({ job_id }) => {
14
- const result = await apiGet(`/jobs/${job_id}`);
14
+ let url = `/jobs/${job_id}`;
15
+ if (!isAuthenticated() && hasWalletConfigured()) {
16
+ const address = await getWalletAddress();
17
+ if (address) {
18
+ url += `?wallet=${encodeURIComponent(address)}`;
19
+ }
20
+ }
21
+ const result = await apiGet(url);
15
22
  if (result.status === "processing") {
16
23
  return text(`Job ${job_id} is still processing...`);
17
24
  }
@@ -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
@@ -11,9 +11,9 @@ import { getOrCreatePendingCardSetup, formatCardSetupBlocks } from "../core/card
11
11
  import { formatPaymentLabel, formatRunConfirmationCommand, resolveConfirmationMethod, } from "./_payment-confirmation.js";
12
12
  const POLL_INTERVAL_MS = 3000;
13
13
  const POLL_MAX_MS = 120000;
14
- async function pollJobUntilDone(jobId) {
14
+ async function pollJobUntilDone(jobId, paymentMethod) {
15
15
  const deadline = Date.now() + POLL_MAX_MS;
16
- const walletAddress = await getWalletAddress();
16
+ const walletAddress = await getWalletAddress(paymentMethod);
17
17
  const walletParam = walletAddress ? `?wallet=${walletAddress}` : "";
18
18
  while (Date.now() < deadline) {
19
19
  await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS));
@@ -61,7 +61,7 @@ export function registerRunTools(server) {
61
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.", {
62
62
  agent_id: z.string().describe("Agent ID (UUID, slug, or name)"),
63
63
  input: z.record(z.unknown()).describe("Input payload for the agent"),
64
- pay_with: z.string().trim().min(1).describe("Required payment method — wallet ID, chain name (tempo, base, etc.), or 'card'. Use wallet_status to see configured options."),
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."),
65
65
  confirmed: z.boolean().optional().describe("Set to true to confirm spending after seeing the price quote."),
66
66
  }, async ({ agent_id, input, pay_with, confirmed }) => {
67
67
  if (!hasWalletConfigured()) {
@@ -84,23 +84,27 @@ export function registerRunTools(server) {
84
84
  }
85
85
  const price = parseFloat(agent.pricePerRunUsd ?? "0.01");
86
86
  const agentName = agent.name ?? agent_id;
87
- const creditPackInventory = await getCreditPackInventory(agent.id);
88
- const activeCreditPack = getActiveCreditPack(creditPackInventory);
89
87
  const compatibleMethods = getCompatiblePaymentMethods(agent, getConfiguredMethods());
90
88
  const pending = pendingRuns.get(agent.id);
91
- const requestedMethod = pay_with;
92
- const normalizedRequestedMethod = normalizePaymentMethod(requestedMethod);
93
- if (!normalizedRequestedMethod) {
89
+ const requestedMethod = pay_with ?? pending?.method;
90
+ const normalizedRequestedMethod = requestedMethod ? normalizePaymentMethod(requestedMethod) : null;
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 (!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
- const method = resolveConfirmationMethod(pay_with, pending?.method, compatibleMethods);
103
- const spendCheckMethod = method ?? normalizedRequestedMethod;
102
+ if (!activeCreditPack && compatibleMethods.length === 0) {
103
+ return text(`No compatible payment methods are configured for ${agentName}.\n\n` +
104
+ "Use wallet_status to review your current payment methods.");
105
+ }
106
+ const method = resolveConfirmationMethod(requestedMethod, pending?.method, compatibleMethods);
107
+ const spendCheckMethod = method ?? normalizedRequestedMethod ?? compatibleMethods[0];
104
108
  if (!activeCreditPack) {
105
109
  const spendCheck = canSpend({
106
110
  method: spendCheckMethod,
@@ -201,7 +205,7 @@ export function registerRunTools(server) {
201
205
  const status = result.status;
202
206
  const usedCreditPack = result.consumption_mode === "credit_pack";
203
207
  if (status === "processing") {
204
- const pollResult = await pollJobUntilDone(jobId);
208
+ const pollResult = await pollJobUntilDone(jobId, method);
205
209
  if (pollResult.status === "completed") {
206
210
  const asyncFormatted = formatRunResult({
207
211
  ...result,
@@ -11,9 +11,9 @@ import { formatPaymentLabel, formatSolveConfirmationCommand, makeSolvePendingKey
11
11
  import { getActiveCreditPack, getCreditPackInventory, getCreditPackProgram } from "../core/passes.js";
12
12
  const POLL_INTERVAL_MS = 3000;
13
13
  const POLL_MAX_MS = 120000;
14
- async function pollSolveJob(jobId) {
14
+ async function pollSolveJob(jobId, paymentMethod) {
15
15
  const deadline = Date.now() + POLL_MAX_MS;
16
- const walletAddress = await getWalletAddress();
16
+ const walletAddress = await getWalletAddress(paymentMethod);
17
17
  const walletParam = walletAddress ? `?wallet=${walletAddress}` : "";
18
18
  while (Date.now() < deadline) {
19
19
  await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS));
@@ -106,7 +106,8 @@ export function registerSolveTools(server) {
106
106
  .string()
107
107
  .trim()
108
108
  .min(1)
109
- .describe("Required payment method — wallet ID, chain name (tempo, base, etc.), or 'card'. Use wallet_status to see configured options."),
109
+ .optional()
110
+ .describe("Payment method — wallet ID, chain name (tempo, base, etc.), or 'card'. Auto-detected if omitted."),
110
111
  confirmed: z
111
112
  .boolean()
112
113
  .optional()
@@ -126,9 +127,9 @@ export function registerSolveTools(server) {
126
127
  const pendingKey = makeSolvePendingKey(intent, input, budget);
127
128
  const pending = pendingSolves.get(pendingKey);
128
129
  const configuredMethods = getConfiguredMethods();
129
- const requestedMethod = pay_with;
130
- const normalizedRequestedMethod = normalizePaymentMethod(requestedMethod);
131
- if (!normalizedRequestedMethod) {
130
+ const requestedMethod = pay_with ?? pending?.method;
131
+ const normalizedRequestedMethod = requestedMethod ? normalizePaymentMethod(requestedMethod) : null;
132
+ if (requestedMethod && !normalizedRequestedMethod) {
132
133
  return text(`Payment method "${requestedMethod}" is not configured.\n\n` +
133
134
  "Use wallet_status to review your current payment methods.");
134
135
  }
@@ -159,10 +160,12 @@ export function registerSolveTools(server) {
159
160
  return multiText(formatRunResult(result), feedbackAsk(jobId, agentId, usedCreditPack ? undefined : cost, tipMsg));
160
161
  }
161
162
  catch (err) {
162
- const isAuthError = err instanceof Error &&
163
- "status" in err &&
164
- err.status === 401;
165
- 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())
166
169
  throw err;
167
170
  }
168
171
  const params = new URLSearchParams({ q: intent, limit: "5" });
@@ -182,15 +185,19 @@ export function registerSolveTools(server) {
182
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 (!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
- const method = resolveConfirmationMethod(pay_with, pending?.method, compatibleMethods);
193
- const spendCheckMethod = method ?? normalizedRequestedMethod;
195
+ if (!activeCreditPack && compatibleMethods.length === 0) {
196
+ return text(`No compatible payment methods are configured for ${selected.name}.\n\n` +
197
+ "Use wallet_status to review your current payment methods.");
198
+ }
199
+ const method = resolveConfirmationMethod(requestedMethod, pending?.method, compatibleMethods);
200
+ const spendCheckMethod = method ?? normalizedRequestedMethod ?? compatibleMethods[0];
194
201
  const selectedPrice = parseFloat(selected.pricePerRunUsd ?? "0.01");
195
202
  const estimatedCost = selectedPrice;
196
203
  const needsPolicyConfirmation = !activeCreditPack && requiresPolicyConfirmation(spendCheckMethod, estimatedCost);
@@ -268,7 +275,7 @@ export function registerSolveTools(server) {
268
275
  storeFeedbackToken(jobId, result.feedback_token, agentId);
269
276
  }
270
277
  if (status === "processing") {
271
- const pollResult = await pollSolveJob(jobId);
278
+ const pollResult = await pollSolveJob(jobId, method);
272
279
  if (pollResult.status === "completed") {
273
280
  const asyncFormatted = formatRunResult({
274
281
  ...result,
@@ -1,6 +1,8 @@
1
1
  import { z } from "zod";
2
2
  import { getWallets, getCardConfig, setCardConfig, addWallet, getPendingCardSetupToken, getSpendPolicy, setSpendPolicy, } from "../core/config.js";
3
- import { getWalletAddress } from "../core/payments.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()
@@ -306,10 +326,10 @@ export function registerWalletTools(server) {
306
326
  policies.push(`Require confirmation above: $${nextPolicy.requireConfirmationAboveUsd.toFixed(2)}`);
307
327
  }
308
328
  return text([
309
- `Spending policy set for wallet "${wallet_id}":`,
329
+ `Local spending policy set for wallet "${wallet_id}":`,
310
330
  ...policies.map((p) => ` ${p}`),
311
331
  "",
312
- "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.",
313
333
  ].join("\n"));
314
334
  });
315
335
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentwonderland/mcp",
3
- "version": "0.1.25",
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,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
+ });