@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.
- package/dist/core/__tests__/api-client.test.d.ts +1 -0
- package/dist/core/__tests__/api-client.test.js +51 -0
- package/dist/core/__tests__/formatters.test.js +10 -0
- package/dist/core/__tests__/passes-api.test.d.ts +1 -0
- package/dist/core/__tests__/passes-api.test.js +27 -0
- package/dist/core/__tests__/payments.test.js +10 -6
- package/dist/core/__tests__/principal.test.js +41 -4
- package/dist/core/__tests__/solana-charge.test.d.ts +1 -0
- package/dist/core/__tests__/solana-charge.test.js +50 -0
- package/dist/core/api-client.d.ts +1 -0
- package/dist/core/api-client.js +8 -3
- package/dist/core/balances.d.ts +1 -0
- package/dist/core/balances.js +56 -0
- package/dist/core/base-charge.js +13 -6
- package/dist/core/formatters.d.ts +3 -2
- package/dist/core/formatters.js +7 -1
- package/dist/core/passes.d.ts +1 -1
- package/dist/core/passes.js +5 -2
- package/dist/core/payments.d.ts +1 -0
- package/dist/core/payments.js +20 -7
- package/dist/core/principal.d.ts +3 -0
- package/dist/core/principal.js +29 -1
- package/dist/core/settings.d.ts +20 -0
- package/dist/core/settings.js +19 -0
- package/dist/core/solana-charge.d.ts +5 -0
- package/dist/core/solana-charge.js +29 -7
- package/dist/core/tempo-charge.d.ts +7 -0
- package/dist/core/tempo-charge.js +84 -0
- package/dist/index.js +5 -4
- package/dist/tools/__tests__/jobs.test.d.ts +1 -0
- package/dist/tools/__tests__/jobs.test.js +71 -0
- package/dist/tools/__tests__/run.test.d.ts +1 -0
- package/dist/tools/__tests__/run.test.js +149 -0
- package/dist/tools/__tests__/solve.test.d.ts +1 -0
- package/dist/tools/__tests__/solve.test.js +158 -0
- package/dist/tools/__tests__/wallet.test.d.ts +1 -0
- package/dist/tools/__tests__/wallet.test.js +230 -0
- package/dist/tools/_payment-confirmation.js +1 -1
- package/dist/tools/jobs.js +8 -1
- package/dist/tools/passes.js +11 -6
- package/dist/tools/run.js +16 -12
- package/dist/tools/solve.js +22 -15
- package/dist/tools/wallet.js +32 -12
- package/package.json +2 -2
- package/src/core/__tests__/api-client.test.ts +78 -0
- package/src/core/__tests__/formatters.test.ts +12 -0
- package/src/core/__tests__/passes-api.test.ts +33 -0
- package/src/core/__tests__/payments.test.ts +17 -6
- package/src/core/__tests__/principal.test.ts +49 -4
- package/src/core/__tests__/solana-charge.test.ts +59 -0
- package/src/core/api-client.ts +16 -3
- package/src/core/balances.ts +63 -0
- package/src/core/base-charge.ts +13 -6
- package/src/core/formatters.ts +10 -3
- package/src/core/passes.ts +5 -2
- package/src/core/payments.ts +22 -7
- package/src/core/principal.ts +42 -1
- package/src/core/settings.ts +36 -0
- package/src/core/solana-charge.ts +43 -9
- package/src/core/tempo-charge.ts +104 -0
- package/src/index.ts +5 -4
- package/src/tools/__tests__/jobs.test.ts +89 -0
- package/src/tools/__tests__/run.test.ts +176 -0
- package/src/tools/__tests__/solve.test.ts +186 -0
- package/src/tools/__tests__/wallet.test.ts +289 -0
- package/src/tools/_payment-confirmation.ts +1 -1
- package/src/tools/jobs.ts +10 -1
- package/src/tools/passes.ts +11 -5
- package/src/tools/run.ts +19 -11
- package/src/tools/solve.ts +25 -14
- 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
|
-
"
|
|
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
|
-
|
|
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
|
}
|
package/src/tools/passes.ts
CHANGED
|
@@ -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 {
|
|
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
|
|
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
|
|
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("
|
|
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
|
-
|
|
152
|
-
|
|
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,
|
package/src/tools/solve.ts
CHANGED
|
@@ -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
|
-
.
|
|
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
|
|
205
|
+
const status =
|
|
204
206
|
err instanceof Error &&
|
|
205
|
-
"status" in err
|
|
206
|
-
|
|
207
|
-
|
|
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
|
-
|
|
241
|
-
|
|
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,
|
package/src/tools/wallet.ts
CHANGED
|
@@ -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
|
|
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
|
|
60
|
+
const chainLines = await Promise.all(
|
|
57
61
|
w.chains.map(async (chainName) => {
|
|
58
62
|
const addr = await getWalletAddress(chainName);
|
|
59
|
-
return `${chainName}:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
`
|
|
422
|
+
`Local spending policy set for wallet "${wallet_id}":`,
|
|
407
423
|
...policies.map((p) => ` ${p}`),
|
|
408
424
|
"",
|
|
409
|
-
"Policy
|
|
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
|
},
|