@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,289 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { beforeEach, describe, expect, it, vi } from "vitest";
3
+
4
+ type WalletEntry = {
5
+ id: string;
6
+ keyType: "evm" | "ows";
7
+ key?: string;
8
+ owsWalletId?: string;
9
+ chains: string[];
10
+ defaultChain?: string;
11
+ label?: string;
12
+ };
13
+
14
+ type CardConfig = {
15
+ consumerToken: string;
16
+ paymentMethodId?: string;
17
+ last4: string;
18
+ brand: string;
19
+ } | null;
20
+
21
+ type WalletToolResult = {
22
+ content: Array<{ type: "text"; text: string }>;
23
+ };
24
+
25
+ const state = vi.hoisted(() => ({
26
+ wallets: [] as WalletEntry[],
27
+ addedWallets: [] as WalletEntry[],
28
+ card: null as CardConfig,
29
+ pendingCardSetupToken: null as string | null,
30
+ spendPolicies: {} as Record<string, unknown>,
31
+ consumerPrincipal: "did:pkh:eip155:8453:0xabc",
32
+ owsAvailable: true,
33
+ owsWallets: [] as Array<{ id: string; name: string; address: string }>,
34
+ owsWalletsByChain: [] as Array<{ id: string; name: string; address: string }>,
35
+ createdWalletCalls: [] as Array<{ name: string; chain: "evm" | "solana" }>,
36
+ importWalletCalls: [] as Array<{ privateKey: string; name: string; chain: "evm" | "solana" }>,
37
+ createdWalletResult: {
38
+ walletId: "ows-wallet-created",
39
+ address: "0x1111111111111111111111111111111111111111",
40
+ },
41
+ importWalletResult: {
42
+ walletId: "ows-wallet-imported",
43
+ address: "0x2222222222222222222222222222222222222222",
44
+ },
45
+ cardSetup: {
46
+ url: "https://api.agentwonderland.com/card/handoff/setup-token",
47
+ token: "setup-token",
48
+ isNew: true,
49
+ },
50
+ cardSetupBlocks: [
51
+ "Open this setup page to connect your card:\n\nhttps://api.agentwonderland.com/card/handoff/setup-token",
52
+ ],
53
+ pollCardSetupCalls: [] as Array<{ token: string; timeoutMs: number }>,
54
+ pollCardSetupResult: null as
55
+ | {
56
+ brand: string;
57
+ last4: string;
58
+ consumerToken: string;
59
+ }
60
+ | null,
61
+ setCardConfigCalls: [] as CardConfig[],
62
+ }));
63
+
64
+ vi.mock("../../core/config.js", () => ({
65
+ addWallet: (wallet: WalletEntry) => {
66
+ state.addedWallets.push(wallet);
67
+ const existing = state.wallets.findIndex((entry) => entry.id === wallet.id);
68
+ if (existing >= 0) {
69
+ state.wallets[existing] = wallet;
70
+ } else {
71
+ state.wallets.push(wallet);
72
+ }
73
+ },
74
+ getCardConfig: () => state.card,
75
+ getPendingCardSetupToken: () => state.pendingCardSetupToken,
76
+ getSpendPolicy: (walletId: string) => state.spendPolicies[walletId] ?? null,
77
+ getWallets: () => state.wallets,
78
+ setCardConfig: (card: CardConfig) => {
79
+ state.card = card;
80
+ state.setCardConfigCalls.push(card);
81
+ },
82
+ setSpendPolicy: (walletId: string, policy: unknown) => {
83
+ state.spendPolicies[walletId] = policy;
84
+ },
85
+ }));
86
+
87
+ vi.mock("../../core/payments.js", () => ({
88
+ getWalletAddress: async () => null,
89
+ }));
90
+
91
+ vi.mock("../../core/card-setup.js", () => ({
92
+ formatCardSetupBlocks: () => state.cardSetupBlocks,
93
+ getCardCapabilities: async () => ({
94
+ spt_status: "enabled",
95
+ }),
96
+ getOrCreatePendingCardSetup: async () => state.cardSetup,
97
+ pollCardSetup: async (token: string, timeoutMs: number) => {
98
+ state.pollCardSetupCalls.push({ token, timeoutMs });
99
+ return state.pollCardSetupResult;
100
+ },
101
+ }));
102
+
103
+ vi.mock("../../core/ows-adapter.js", () => ({
104
+ createOwsWallet: async (name: string, chain: "evm" | "solana") => {
105
+ state.createdWalletCalls.push({ name, chain });
106
+ return state.createdWalletResult;
107
+ },
108
+ importKeyToOws: async (privateKey: string, name: string, chain: "evm" | "solana") => {
109
+ state.importWalletCalls.push({ privateKey, name, chain });
110
+ return state.importWalletResult;
111
+ },
112
+ isOwsAvailable: async () => state.owsAvailable,
113
+ listOwsWallets: async () => state.owsWallets,
114
+ listOwsWalletsByChain: async () => state.owsWalletsByChain,
115
+ }));
116
+
117
+ vi.mock("../../core/principal.js", () => ({
118
+ ensureConsumerPrincipal: async () => state.consumerPrincipal,
119
+ getConsumerPrincipal: async () => state.consumerPrincipal,
120
+ }));
121
+
122
+ function resetState(): void {
123
+ state.wallets = [];
124
+ state.addedWallets = [];
125
+ state.card = null;
126
+ state.pendingCardSetupToken = null;
127
+ state.spendPolicies = {};
128
+ state.consumerPrincipal = "did:pkh:eip155:8453:0xabc";
129
+ state.owsAvailable = true;
130
+ state.owsWallets = [];
131
+ state.owsWalletsByChain = [];
132
+ state.createdWalletCalls = [];
133
+ state.importWalletCalls = [];
134
+ state.createdWalletResult = {
135
+ walletId: "ows-wallet-created",
136
+ address: "0x1111111111111111111111111111111111111111",
137
+ };
138
+ state.importWalletResult = {
139
+ walletId: "ows-wallet-imported",
140
+ address: "0x2222222222222222222222222222222222222222",
141
+ };
142
+ state.cardSetup = {
143
+ url: "https://api.agentwonderland.com/card/handoff/setup-token",
144
+ token: "setup-token",
145
+ isNew: true,
146
+ };
147
+ state.cardSetupBlocks = [
148
+ "Open this setup page to connect your card:\n\nhttps://api.agentwonderland.com/card/handoff/setup-token",
149
+ ];
150
+ state.pollCardSetupCalls = [];
151
+ state.pollCardSetupResult = null;
152
+ state.setCardConfigCalls = [];
153
+ }
154
+
155
+ async function getWalletSetupTool(): Promise<(args: Record<string, unknown>) => Promise<WalletToolResult>> {
156
+ const tools = new Map<string, (args: Record<string, unknown>) => Promise<WalletToolResult>>();
157
+ const server = {
158
+ tool: (
159
+ name: string,
160
+ _description: string,
161
+ _schema: unknown,
162
+ handler: (args: Record<string, unknown>) => Promise<WalletToolResult>,
163
+ ) => {
164
+ tools.set(name, handler);
165
+ },
166
+ } as unknown as McpServer;
167
+
168
+ const { registerWalletTools } = await import("../wallet.js");
169
+ registerWalletTools(server);
170
+
171
+ const walletSetup = tools.get("wallet_setup");
172
+ if (!walletSetup) {
173
+ throw new Error("wallet_setup tool was not registered");
174
+ }
175
+ return walletSetup;
176
+ }
177
+
178
+ function flattenText(result: WalletToolResult): string {
179
+ return result.content.map((item) => item.text).join("\n");
180
+ }
181
+
182
+ describe("wallet_setup tool", () => {
183
+ beforeEach(() => {
184
+ vi.resetModules();
185
+ resetState();
186
+ });
187
+
188
+ it("creates an encrypted OWS wallet for Tempo/Base with Base as the default chain", async () => {
189
+ const walletSetup = await getWalletSetupTool();
190
+
191
+ const result = await walletSetup({ action: "create", name: "launch-wallet", chain: "base" });
192
+ const text = flattenText(result);
193
+
194
+ expect(state.createdWalletCalls).toEqual([
195
+ { name: "launch-wallet", chain: "evm" },
196
+ ]);
197
+ expect(state.addedWallets).toEqual([
198
+ {
199
+ id: "launch-wallet",
200
+ keyType: "ows",
201
+ owsWalletId: "ows-wallet-created",
202
+ chains: ["tempo", "base"],
203
+ defaultChain: "base",
204
+ label: "launch-wallet",
205
+ },
206
+ ]);
207
+ expect(text).toContain("Wallet created [encrypted]:");
208
+ expect(text).toContain("Address: 0x1111111111111111111111111111111111111111");
209
+ expect(text).toContain("Chains: tempo, base");
210
+ expect(text).toContain("Consumer principal: did:pkh:eip155:8453:0xabc");
211
+ });
212
+
213
+ it("imports a wallet into OWS encrypted storage with Base as the default chain", async () => {
214
+ const walletSetup = await getWalletSetupTool();
215
+
216
+ const result = await walletSetup({
217
+ action: "import",
218
+ key: "0x1234",
219
+ name: "imported-wallet",
220
+ chain: "base",
221
+ });
222
+ const text = flattenText(result);
223
+
224
+ expect(state.importWalletCalls).toEqual([
225
+ { privateKey: "0x1234", name: "imported-wallet", chain: "evm" },
226
+ ]);
227
+ expect(state.addedWallets).toEqual([
228
+ {
229
+ id: "imported-wallet",
230
+ keyType: "ows",
231
+ owsWalletId: "ows-wallet-imported",
232
+ chains: ["tempo", "base"],
233
+ defaultChain: "base",
234
+ label: "imported-wallet",
235
+ },
236
+ ]);
237
+ expect(text).toContain("Key imported to OWS [encrypted]:");
238
+ expect(text).toContain("Address: 0x2222222222222222222222222222222222222222");
239
+ expect(text).toContain("Consumer principal: did:pkh:eip155:8453:0xabc");
240
+ });
241
+
242
+ it("starts card setup when no card is connected", async () => {
243
+ const walletSetup = await getWalletSetupTool();
244
+
245
+ const result = await walletSetup({ action: "add-card" });
246
+ const text = flattenText(result);
247
+
248
+ expect(text).toContain("Open this setup page to connect your card:");
249
+ expect(text).toContain("https://api.agentwonderland.com/card/handoff/setup-token");
250
+ expect(state.pollCardSetupCalls).toEqual([]);
251
+ });
252
+
253
+ it("completes a pending card setup when Stripe handoff has finished", async () => {
254
+ state.pendingCardSetupToken = "setup-token";
255
+ state.pollCardSetupResult = {
256
+ brand: "Visa",
257
+ last4: "4242",
258
+ consumerToken: "consumer-token",
259
+ };
260
+
261
+ const walletSetup = await getWalletSetupTool();
262
+ const result = await walletSetup({ action: "add-card" });
263
+ const text = flattenText(result);
264
+
265
+ expect(state.pollCardSetupCalls).toEqual([
266
+ { token: "setup-token", timeoutMs: 250 },
267
+ ]);
268
+ expect(text).toContain("Connected! Visa ****4242 is ready for payments.");
269
+ expect(text).toContain("Consumer principal: did:pkh:eip155:8453:0xabc");
270
+ });
271
+
272
+ it("removes the connected card", async () => {
273
+ state.card = {
274
+ consumerToken: "consumer-token",
275
+ paymentMethodId: "pm_123",
276
+ last4: "4242",
277
+ brand: "Visa",
278
+ };
279
+
280
+ const walletSetup = await getWalletSetupTool();
281
+ const result = await walletSetup({ action: "remove-card" });
282
+ const text = flattenText(result);
283
+
284
+ expect(state.setCardConfigCalls).toEqual([null]);
285
+ expect(state.card).toBeNull();
286
+ expect(text).toContain("Removed Visa ****4242.");
287
+ expect(text).toContain("Card disconnected from Agent Wonderland.");
288
+ });
289
+ });
@@ -47,6 +47,6 @@ export function formatPaymentChoicePrompt(
47
47
  ...commands,
48
48
  "",
49
49
  `Available methods: ${methods.map((method) => `"${method}"`).join(", ")}`,
50
- "For fully agentic execution, include pay_with explicitly.",
50
+ "You can omit pay_with to use the default compatible method.",
51
51
  ].join("\n");
52
52
  }
package/src/tools/jobs.ts CHANGED
@@ -18,7 +18,16 @@ export function registerJobTools(server: McpServer): void {
18
18
  job_id: z.string().describe("Job ID (UUID)"),
19
19
  },
20
20
  async ({ job_id }) => {
21
- const result = await apiGet<Record<string, unknown>>(`/jobs/${job_id}`);
21
+ let url = `/jobs/${job_id}`;
22
+
23
+ if (!isAuthenticated() && hasWalletConfigured()) {
24
+ const address = await getWalletAddress();
25
+ if (address) {
26
+ url += `?wallet=${encodeURIComponent(address)}`;
27
+ }
28
+ }
29
+
30
+ const result = await apiGet<Record<string, unknown>>(url);
22
31
  if (result.status === "processing") {
23
32
  return text(`Job ${job_id} is still processing...`);
24
33
  }
@@ -4,6 +4,7 @@ import { apiGet, apiPostWithPayment } from "../core/api-client.js";
4
4
  import {
5
5
  formatCreditPack,
6
6
  formatCreditPackOffer,
7
+ getCreditPackInventory,
7
8
  getCreditPackProgram,
8
9
  } from "../core/passes.js";
9
10
  import {
@@ -13,7 +14,7 @@ import {
13
14
  normalizePaymentMethod,
14
15
  } from "../core/payments.js";
15
16
  import { requiresSpendConfirmation } from "../core/config.js";
16
- import { ensureConsumerPrincipal } from "../core/principal.js";
17
+ import { ensureConsumerPrincipalForMethod } from "../core/principal.js";
17
18
  import { getOrCreatePendingCardSetup, formatCardSetupBlocks } from "../core/card-setup.js";
18
19
  import {
19
20
  formatPaymentChoicePrompt,
@@ -84,7 +85,6 @@ export function registerPassTools(server: McpServer): void {
84
85
 
85
86
  const agent = await getAgent(agent_id);
86
87
  const agentName = agent.name ?? agent_id;
87
- const principal = await ensureConsumerPrincipal();
88
88
  const program = getCreditPackProgram(agent);
89
89
  const offers = (program?.packs ?? [])
90
90
  .map((pack) => findOffer(agent, pack.key ?? ""))
@@ -155,6 +155,7 @@ export function registerPassTools(server: McpServer): void {
155
155
  }
156
156
 
157
157
  const method = resolveConfirmationMethod(pay_with, pending?.method, compatibleMethods);
158
+ const principal = await ensureConsumerPrincipalForMethod(method);
158
159
  if (requiresSpendConfirmation() && !confirmed) {
159
160
  pendingCreditPackPurchases.set(agent.id, {
160
161
  agentId: agent.id,
@@ -192,7 +193,8 @@ export function registerPassTools(server: McpServer): void {
192
193
  formatCreditPack(result.credit_pack),
193
194
  `Consumer principal: ${result.consumer_principal}`,
194
195
  "",
195
- "Future runs through run_agent will automatically use this credit pack while units remain.",
196
+ "Future runs for this agent will automatically use this credit pack while units remain.",
197
+ "That includes run_agent, and solve whenever it selects this same agent.",
196
198
  ].join("\n"));
197
199
  },
198
200
  );
@@ -202,10 +204,14 @@ export function registerPassTools(server: McpServer): void {
202
204
  "Show discounted credit-pack offers for an agent plus any balances available under the current consumer principal.",
203
205
  {
204
206
  agent_id: z.string().describe("Agent ID (UUID, slug, or name)"),
207
+ pay_with: z.string().optional().describe("Optional payment method context used to inspect the matching consumer principal."),
205
208
  },
206
- async ({ agent_id }) => {
209
+ async ({ agent_id, pay_with }) => {
207
210
  const agent = await getAgent(agent_id);
208
- const result = await apiGet<CreditPackInventory>(`/agents/${agent.id}/credit-packs`, { ensureConsumerPrincipal: true });
211
+ const result = await getCreditPackInventory(agent.id, pay_with);
212
+ if (!result) {
213
+ return text(`Could not load credit-pack inventory for ${agent.name}.`);
214
+ }
209
215
 
210
216
  const lines = [
211
217
  `Credit packs for ${agent.name}`,
package/src/tools/run.ts CHANGED
@@ -32,9 +32,10 @@ const POLL_MAX_MS = 120000;
32
32
 
33
33
  async function pollJobUntilDone(
34
34
  jobId: string,
35
+ paymentMethod?: string,
35
36
  ): Promise<{ status: string; output?: unknown; error_code?: string }> {
36
37
  const deadline = Date.now() + POLL_MAX_MS;
37
- const walletAddress = await getWalletAddress();
38
+ const walletAddress = await getWalletAddress(paymentMethod);
38
39
  const walletParam = walletAddress ? `?wallet=${walletAddress}` : "";
39
40
 
40
41
  while (Date.now() < deadline) {
@@ -100,7 +101,7 @@ export function registerRunTools(server: McpServer): void {
100
101
  {
101
102
  agent_id: z.string().describe("Agent ID (UUID, slug, or name)"),
102
103
  input: z.record(z.unknown()).describe("Input payload for the agent"),
103
- 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."),
104
+ pay_with: z.string().trim().min(1).optional().describe("Payment method — wallet ID, chain name (tempo, base, etc.), or 'card'. Auto-detected if omitted."),
104
105
  confirmed: z.boolean().optional().describe("Set to true to confirm spending after seeing the price quote."),
105
106
  },
106
107
  async ({ agent_id, input, pay_with, confirmed }) => {
@@ -126,21 +127,21 @@ export function registerRunTools(server: McpServer): void {
126
127
 
127
128
  const price = parseFloat(agent.pricePerRunUsd ?? "0.01");
128
129
  const agentName = agent.name ?? agent_id;
129
- const creditPackInventory = await getCreditPackInventory(agent.id);
130
- const activeCreditPack = getActiveCreditPack(creditPackInventory);
131
130
  const compatibleMethods = getCompatiblePaymentMethods(agent, getConfiguredMethods());
132
131
  const pending = pendingRuns.get(agent.id);
133
- const requestedMethod = pay_with;
134
- const normalizedRequestedMethod = normalizePaymentMethod(requestedMethod);
132
+ const requestedMethod = pay_with ?? pending?.method;
133
+ const normalizedRequestedMethod = requestedMethod ? normalizePaymentMethod(requestedMethod) : null;
134
+ const creditPackInventory = await getCreditPackInventory(agent.id, requestedMethod);
135
+ const activeCreditPack = getActiveCreditPack(creditPackInventory);
135
136
 
136
- if (!normalizedRequestedMethod) {
137
+ if (requestedMethod && !normalizedRequestedMethod) {
137
138
  return text(
138
139
  `Payment method "${requestedMethod}" is not configured.\n\n` +
139
140
  "Use wallet_status to review your current payment methods.",
140
141
  );
141
142
  }
142
143
 
143
- if (!compatibleMethods.includes(normalizedRequestedMethod)) {
144
+ if (normalizedRequestedMethod && !compatibleMethods.includes(normalizedRequestedMethod)) {
144
145
  return text(
145
146
  `This agent cannot be paid with "${requestedMethod}".\n\n` +
146
147
  `Available payment methods for this agent: ${compatibleMethods.join(", ") || "none"}.\n` +
@@ -148,8 +149,15 @@ export function registerRunTools(server: McpServer): void {
148
149
  );
149
150
  }
150
151
 
151
- const method = resolveConfirmationMethod(pay_with, pending?.method, compatibleMethods);
152
- const spendCheckMethod = method ?? normalizedRequestedMethod;
152
+ if (!activeCreditPack && compatibleMethods.length === 0) {
153
+ return text(
154
+ `No compatible payment methods are configured for ${agentName}.\n\n` +
155
+ "Use wallet_status to review your current payment methods.",
156
+ );
157
+ }
158
+
159
+ const method = resolveConfirmationMethod(requestedMethod, pending?.method, compatibleMethods);
160
+ const spendCheckMethod = method ?? normalizedRequestedMethod ?? compatibleMethods[0];
153
161
 
154
162
  if (!activeCreditPack) {
155
163
  const spendCheck = canSpend({
@@ -278,7 +286,7 @@ export function registerRunTools(server: McpServer): void {
278
286
  const usedCreditPack = result.consumption_mode === "credit_pack";
279
287
 
280
288
  if (status === "processing") {
281
- const pollResult = await pollJobUntilDone(jobId);
289
+ const pollResult = await pollJobUntilDone(jobId, method);
282
290
  if (pollResult.status === "completed") {
283
291
  const asyncFormatted = formatRunResult({
284
292
  ...result,
@@ -30,9 +30,10 @@ const POLL_MAX_MS = 120000;
30
30
 
31
31
  async function pollSolveJob(
32
32
  jobId: string,
33
+ paymentMethod?: string,
33
34
  ): Promise<{ status: string; output?: unknown; error_code?: string }> {
34
35
  const deadline = Date.now() + POLL_MAX_MS;
35
- const walletAddress = await getWalletAddress();
36
+ const walletAddress = await getWalletAddress(paymentMethod);
36
37
  const walletParam = walletAddress ? `?wallet=${walletAddress}` : "";
37
38
 
38
39
  while (Date.now() < deadline) {
@@ -139,7 +140,8 @@ export function registerSolveTools(server: McpServer): void {
139
140
  .string()
140
141
  .trim()
141
142
  .min(1)
142
- .describe("Required payment method — wallet ID, chain name (tempo, base, etc.), or 'card'. Use wallet_status to see configured options."),
143
+ .optional()
144
+ .describe("Payment method — wallet ID, chain name (tempo, base, etc.), or 'card'. Auto-detected if omitted."),
143
145
  confirmed: z
144
146
  .boolean()
145
147
  .optional()
@@ -162,10 +164,10 @@ export function registerSolveTools(server: McpServer): void {
162
164
  const pendingKey = makeSolvePendingKey(intent, input, budget);
163
165
  const pending = pendingSolves.get(pendingKey);
164
166
  const configuredMethods = getConfiguredMethods();
165
- const requestedMethod = pay_with;
166
- const normalizedRequestedMethod = normalizePaymentMethod(requestedMethod);
167
+ const requestedMethod = pay_with ?? pending?.method;
168
+ const normalizedRequestedMethod = requestedMethod ? normalizePaymentMethod(requestedMethod) : null;
167
169
 
168
- if (!normalizedRequestedMethod) {
170
+ if (requestedMethod && !normalizedRequestedMethod) {
169
171
  return text(
170
172
  `Payment method "${requestedMethod}" is not configured.\n\n` +
171
173
  "Use wallet_status to review your current payment methods.",
@@ -200,11 +202,13 @@ export function registerSolveTools(server: McpServer): void {
200
202
  const tipMsg = result.feedback_token ? await autoTip(jobId, agentId, agentName, result.feedback_token as string) : "";
201
203
  return multiText(formatRunResult(result), feedbackAsk(jobId, agentId, usedCreditPack ? undefined : cost, tipMsg));
202
204
  } catch (err: unknown) {
203
- const isAuthError =
205
+ const status =
204
206
  err instanceof Error &&
205
- "status" in err &&
206
- (err as { status: number }).status === 401;
207
- if (!isAuthError || !hasWalletConfigured()) throw err;
207
+ "status" in err
208
+ ? (err as { status: number }).status
209
+ : undefined;
210
+ const isRecoverableDirectSolveError = status === 401 || status === 402;
211
+ if (!isRecoverableDirectSolveError || !hasWalletConfigured()) throw err;
208
212
  }
209
213
 
210
214
  const params = new URLSearchParams({ q: intent, limit: "5" });
@@ -226,10 +230,10 @@ export function registerSolveTools(server: McpServer): void {
226
230
  return price <= budget;
227
231
  });
228
232
  const selected = affordable[0] ?? agents[0];
229
- const activeCreditPack = await getCreditPackInventory(selected.id).then(getActiveCreditPack);
233
+ const activeCreditPack = await getCreditPackInventory(selected.id, requestedMethod).then(getActiveCreditPack);
230
234
  const compatibleMethods = getCompatiblePaymentMethods(selected, configuredMethods);
231
235
 
232
- if (!compatibleMethods.includes(normalizedRequestedMethod)) {
236
+ if (normalizedRequestedMethod && !compatibleMethods.includes(normalizedRequestedMethod)) {
233
237
  return text(
234
238
  `The best matching agent cannot be paid with "${requestedMethod}".\n\n` +
235
239
  `Available payment methods for ${selected.name}: ${compatibleMethods.join(", ") || "none"}.\n` +
@@ -237,8 +241,15 @@ export function registerSolveTools(server: McpServer): void {
237
241
  );
238
242
  }
239
243
 
240
- const method = resolveConfirmationMethod(pay_with, pending?.method, compatibleMethods);
241
- const spendCheckMethod = method ?? normalizedRequestedMethod;
244
+ if (!activeCreditPack && compatibleMethods.length === 0) {
245
+ return text(
246
+ `No compatible payment methods are configured for ${selected.name}.\n\n` +
247
+ "Use wallet_status to review your current payment methods.",
248
+ );
249
+ }
250
+
251
+ const method = resolveConfirmationMethod(requestedMethod, pending?.method, compatibleMethods);
252
+ const spendCheckMethod = method ?? normalizedRequestedMethod ?? compatibleMethods[0];
242
253
 
243
254
  const selectedPrice = parseFloat(selected.pricePerRunUsd ?? "0.01");
244
255
  const estimatedCost = selectedPrice;
@@ -334,7 +345,7 @@ export function registerSolveTools(server: McpServer): void {
334
345
  }
335
346
 
336
347
  if (status === "processing") {
337
- const pollResult = await pollSolveJob(jobId);
348
+ const pollResult = await pollSolveJob(jobId, method);
338
349
  if (pollResult.status === "completed") {
339
350
  const asyncFormatted = formatRunResult({
340
351
  ...result,
@@ -9,7 +9,9 @@ import {
9
9
  getSpendPolicy,
10
10
  setSpendPolicy,
11
11
  } from "../core/config.js";
12
- import { getWalletAddress } from "../core/payments.js";
12
+ import { getWalletAddress, isCardPaymentEnabled } from "../core/payments.js";
13
+ import { fetchUsdcBalance } from "../core/balances.js";
14
+ import { getSettings } from "../core/settings.js";
13
15
  import {
14
16
  getOrCreatePendingCardSetup,
15
17
  formatCardSetupBlocks,
@@ -47,25 +49,36 @@ export function registerWalletTools(server: McpServer): void {
47
49
  );
48
50
  }
49
51
 
50
- const lines = ["Payment methods:"];
52
+ const settings = await getSettings();
53
+ const networkLabel = settings ? ` (${settings.network})` : "";
54
+ const lines = [`Payment methods${networkLabel}:`];
51
55
 
52
56
  for (const w of wallets) {
53
57
  const label = w.label ? ` (${w.label})` : "";
54
58
  const storage =
55
59
  w.keyType === "ows" ? " [encrypted]" : " [plaintext]";
56
- const chainAddresses = await Promise.all(
60
+ const chainLines = await Promise.all(
57
61
  w.chains.map(async (chainName) => {
58
62
  const addr = await getWalletAddress(chainName);
59
- return `${chainName}: ${addr ?? "unknown"}`;
63
+ if (!addr) return `${chainName}: unknown`;
64
+ let balanceStr = "";
65
+ if (chainName === "tempo" || chainName === "base" || chainName === "solana") {
66
+ const balance = await fetchUsdcBalance(chainName, addr);
67
+ if (balance !== null) {
68
+ const num = Number(balance);
69
+ balanceStr = ` ${Number.isFinite(num) ? num.toFixed(4).replace(/\.?0+$/, "") : balance} USDC`;
70
+ }
71
+ }
72
+ return `${chainName}: ${addr}${balanceStr}`;
60
73
  }),
61
74
  );
62
- lines.push(
63
- ` ${w.id}${label}${storage}: ${chainAddresses.join(" | ")}`,
64
- );
75
+ lines.push(` ${w.id}${label}${storage}:`);
76
+ for (const line of chainLines) lines.push(` ${line}`);
65
77
  }
66
78
 
67
- if (card) {
68
- lines.push(` Card: ${card.brand} ****${card.last4}`);
79
+ if (card && isCardPaymentEnabled()) {
80
+ const stripeMode = settings?.stripe.mode === "test" ? " [Stripe test mode]" : "";
81
+ lines.push(` Card: ${card.brand} ****${card.last4}${stripeMode}`);
69
82
  const capabilities = await getCardCapabilities();
70
83
  if (capabilities.spt_status === "enabled") {
71
84
  lines.push(" Card MPP: ready");
@@ -91,7 +104,7 @@ export function registerWalletTools(server: McpServer): void {
91
104
  // ── wallet_setup (NEW) ──────────────────────────────────────────
92
105
  server.tool(
93
106
  "wallet_setup",
94
- "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.",
107
+ "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.",
95
108
  {
96
109
  action: z
97
110
  .enum(["create", "import", "add-card", "remove-card"])
@@ -112,6 +125,9 @@ export function registerWalletTools(server: McpServer): void {
112
125
  async ({ action, name, key, chain }) => {
113
126
  // ── Card setup flow ──────────────────────────────────────
114
127
  if (action === "add-card") {
128
+ if (!isCardPaymentEnabled()) {
129
+ return text("Card payments are temporarily unavailable. Use wallet_setup({ action: \"create\" }) for a crypto wallet instead.");
130
+ }
115
131
  const existing = getCardConfig();
116
132
  if (existing) {
117
133
  return text(`Card already connected: ${existing.brand} ****${existing.last4}\n\nTo replace it, remove the current card first.`);
@@ -341,9 +357,9 @@ export function registerWalletTools(server: McpServer): void {
341
357
  // ── wallet_set_policy (NEW) ─────────────────────────────────────
342
358
  server.tool(
343
359
  "wallet_set_policy",
344
- "Set spending limits on a wallet to control agent costs. Limits reset daily.",
360
+ "Set client-side spending limits on a wallet to control agent costs in this MCP client. Limits reset daily.",
345
361
  {
346
- wallet_id: z.string().describe("Wallet ID to set policy on"),
362
+ wallet_id: z.string().describe("Wallet ID to set local policy on"),
347
363
  max_per_tx: z
348
364
  .number()
349
365
  .positive()
@@ -403,10 +419,10 @@ export function registerWalletTools(server: McpServer): void {
403
419
  }
404
420
  return text(
405
421
  [
406
- `Spending policy set for wallet "${wallet_id}":`,
422
+ `Local spending policy set for wallet "${wallet_id}":`,
407
423
  ...policies.map((p) => ` ${p}`),
408
424
  "",
409
- "Policy will be enforced on all future transactions from this wallet.",
425
+ "Policy is stored in this MCP client's local config and enforced on future transactions from this wallet on this client.",
410
426
  ].join("\n"),
411
427
  );
412
428
  },