@agentwonderland/mcp 0.1.25 → 0.1.27
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 -7
- package/dist/prompts/index.js +1 -1
- 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/agent-info.js +14 -28
- package/dist/tools/jobs.js +82 -16
- package/dist/tools/passes.js +30 -14
- package/dist/tools/run.js +35 -20
- package/dist/tools/search.js +9 -8
- package/dist/tools/solve.js +45 -25
- package/dist/tools/wallet.js +35 -15
- 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 -7
- package/src/prompts/index.ts +1 -1
- 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/agent-info.ts +15 -38
- package/src/tools/jobs.ts +79 -17
- package/src/tools/passes.ts +30 -14
- package/src/tools/run.ts +38 -20
- package/src/tools/search.ts +10 -9
- package/src/tools/solve.ts +48 -25
- package/src/tools/wallet.ts +33 -17
- package/src/tools/observability.ts +0 -43
|
@@ -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/agent-info.ts
CHANGED
|
@@ -31,14 +31,27 @@ export function registerAgentInfoTools(server: McpServer): void {
|
|
|
31
31
|
}>;
|
|
32
32
|
} | null | undefined;
|
|
33
33
|
|
|
34
|
+
const totalJobs = (s.completedJobs ?? a.totalExecutions ?? 0) as number;
|
|
35
|
+
const acceptedPayments = (payment.accepted_payments as string[] | undefined) ?? [];
|
|
36
|
+
const paymentLabelMap: Record<string, string> = {
|
|
37
|
+
tempo_usdc: "tempo",
|
|
38
|
+
base_usdc: "base",
|
|
39
|
+
solana_usdc: "solana",
|
|
40
|
+
stripe_card: "card",
|
|
41
|
+
};
|
|
42
|
+
const paymentLabels = acceptedPayments.map((m) => paymentLabelMap[m] ?? m);
|
|
43
|
+
|
|
34
44
|
const lines = [
|
|
35
45
|
`${a.name}`,
|
|
36
|
-
`${stars(a.avgRating ?? (s.avgRating as number))} (${s.ratingCount ?? 0} reviews) • ${compactNumber(
|
|
46
|
+
`${stars(a.avgRating ?? (s.avgRating as number))} (${s.ratingCount ?? 0} reviews) • ${compactNumber(totalJobs)} jobs`,
|
|
37
47
|
"",
|
|
38
48
|
(a.description as string) ?? "",
|
|
39
49
|
"",
|
|
40
50
|
`Pricing: ${formatPrice(a.pricePerRunUsd)}`,
|
|
41
|
-
|
|
51
|
+
...(paymentLabels.length > 0 ? [`Accepted payments: ${paymentLabels.join(", ")}`] : []),
|
|
52
|
+
...(totalJobs > 0 && a.successRate != null
|
|
53
|
+
? [`Reliability: ${(Number(a.successRate) * 100).toFixed(0)}% (${compactNumber(totalJobs)} runs)`]
|
|
54
|
+
: []),
|
|
42
55
|
`Avg latency: ${(a.avgResponseTimeMs as number) != null ? a.avgResponseTimeMs + "ms" : "N/A"}`,
|
|
43
56
|
...(() => {
|
|
44
57
|
const lastActive = formatLastActive(a.lastActiveAt as string | null);
|
|
@@ -104,40 +117,4 @@ export function registerAgentInfoTools(server: McpServer): void {
|
|
|
104
117
|
return text(lines.join("\n"));
|
|
105
118
|
},
|
|
106
119
|
);
|
|
107
|
-
|
|
108
|
-
// ── compare_agents ──────────────────────────────────────────────
|
|
109
|
-
server.tool(
|
|
110
|
-
"compare_agents",
|
|
111
|
-
"Compare multiple agents side-by-side on rating, price, success rate, and job count.",
|
|
112
|
-
{
|
|
113
|
-
agent_ids: z
|
|
114
|
-
.array(z.string())
|
|
115
|
-
.min(2)
|
|
116
|
-
.max(5)
|
|
117
|
-
.describe("Agent IDs to compare (2-5)"),
|
|
118
|
-
},
|
|
119
|
-
async ({ agent_ids }) => {
|
|
120
|
-
const agents = await Promise.all(
|
|
121
|
-
agent_ids.map((id) => apiGet<AgentRecord>(`/agents/${id}`)),
|
|
122
|
-
);
|
|
123
|
-
|
|
124
|
-
const header = "Agent Comparison:\n";
|
|
125
|
-
const lines = agents.map((a) => {
|
|
126
|
-
const s = (a.stats ?? {}) as Record<string, unknown>;
|
|
127
|
-
const rating = a.avgRating ?? (s.avgRating as number);
|
|
128
|
-
const jobs = (s.completedJobs ?? a.totalExecutions ?? 0) as number;
|
|
129
|
-
const tipCount = (s.tipCount ?? 0) as number;
|
|
130
|
-
return [
|
|
131
|
-
` ${a.name}`,
|
|
132
|
-
` ${stars(rating)} (${s.ratingCount ?? 0} reviews)${tipCount > 0 ? ` • ${tipCount} tips` : ""}`,
|
|
133
|
-
` ${compactNumber(jobs)} jobs • ${formatPrice(a.pricePerRunUsd)}`,
|
|
134
|
-
` Success: ${a.successRate != null ? (Number(a.successRate) * 100).toFixed(0) + "%" : "N/A"}`,
|
|
135
|
-
` ${agentWebUrl(a.id)}`,
|
|
136
|
-
"",
|
|
137
|
-
].join("\n");
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
return text(header + lines.join("\n"));
|
|
141
|
-
},
|
|
142
|
-
);
|
|
143
120
|
}
|
package/src/tools/jobs.ts
CHANGED
|
@@ -9,6 +9,26 @@ function text(t: string) {
|
|
|
9
9
|
return { content: [{ type: "text" as const, text: t }] };
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
+
async function getConsumerWalletAddresses(): Promise<string[]> {
|
|
13
|
+
const addresses = new Set<string>();
|
|
14
|
+
for (const chain of ["tempo", "base", "solana"]) {
|
|
15
|
+
const addr = await getWalletAddress(chain);
|
|
16
|
+
if (addr) addresses.add(addr);
|
|
17
|
+
}
|
|
18
|
+
return [...addresses];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function formatRelativeTime(iso: string | null | undefined): string {
|
|
22
|
+
if (!iso) return "";
|
|
23
|
+
const t = new Date(iso).getTime();
|
|
24
|
+
if (!Number.isFinite(t)) return "";
|
|
25
|
+
const diffSec = Math.max(0, Math.floor((Date.now() - t) / 1000));
|
|
26
|
+
if (diffSec < 60) return `${diffSec}s ago`;
|
|
27
|
+
if (diffSec < 3600) return `${Math.floor(diffSec / 60)}m ago`;
|
|
28
|
+
if (diffSec < 86400) return `${Math.floor(diffSec / 3600)}h ago`;
|
|
29
|
+
return `${Math.floor(diffSec / 86400)}d ago`;
|
|
30
|
+
}
|
|
31
|
+
|
|
12
32
|
export function registerJobTools(server: McpServer): void {
|
|
13
33
|
// ── get_job ─────────────────────────────────────────────────────
|
|
14
34
|
server.tool(
|
|
@@ -18,7 +38,25 @@ export function registerJobTools(server: McpServer): void {
|
|
|
18
38
|
job_id: z.string().describe("Job ID (UUID)"),
|
|
19
39
|
},
|
|
20
40
|
async ({ job_id }) => {
|
|
21
|
-
const
|
|
41
|
+
const walletLookup = !isAuthenticated() && hasWalletConfigured();
|
|
42
|
+
const addresses = walletLookup ? await getConsumerWalletAddresses() : [""];
|
|
43
|
+
|
|
44
|
+
let result: Record<string, unknown> | null = null;
|
|
45
|
+
for (const addr of addresses) {
|
|
46
|
+
const url = addr ? `/jobs/${job_id}?wallet=${encodeURIComponent(addr)}` : `/jobs/${job_id}`;
|
|
47
|
+
try {
|
|
48
|
+
result = await apiGet<Record<string, unknown>>(url);
|
|
49
|
+
break;
|
|
50
|
+
} catch (err) {
|
|
51
|
+
const status = (err as { status?: number }).status;
|
|
52
|
+
if (status !== 404) throw err;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (!result) {
|
|
57
|
+
return text(`Job ${job_id} not found. Either the job ID is wrong or it was paid with a wallet not configured here.`);
|
|
58
|
+
}
|
|
59
|
+
|
|
22
60
|
if (result.status === "processing") {
|
|
23
61
|
return text(`Job ${job_id} is still processing...`);
|
|
24
62
|
}
|
|
@@ -26,26 +64,46 @@ export function registerJobTools(server: McpServer): void {
|
|
|
26
64
|
},
|
|
27
65
|
);
|
|
28
66
|
|
|
29
|
-
// ── list_jobs
|
|
67
|
+
// ── list_jobs ───────────────────────────────────────────────────
|
|
30
68
|
server.tool(
|
|
31
69
|
"list_jobs",
|
|
32
|
-
"List your recent jobs with status, cost, and
|
|
70
|
+
"List your recent jobs across all configured payment wallets, with status, cost, agent, method, and age.",
|
|
33
71
|
{
|
|
34
|
-
limit: z.coerce.number().optional().default(10).describe("Max results (1-50)"),
|
|
72
|
+
limit: z.coerce.number().int().min(1).max(50).optional().default(10).describe("Max results (1-50)"),
|
|
35
73
|
},
|
|
36
74
|
async ({ limit }) => {
|
|
37
|
-
|
|
75
|
+
const requestedLimit = limit ?? 10;
|
|
76
|
+
const walletLookup = !isAuthenticated() && hasWalletConfigured();
|
|
77
|
+
const addresses = walletLookup ? await getConsumerWalletAddresses() : [""];
|
|
38
78
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
79
|
+
const seen = new Set<string>();
|
|
80
|
+
const collected: Array<Record<string, unknown>> = [];
|
|
81
|
+
for (const addr of addresses) {
|
|
82
|
+
const url = addr
|
|
83
|
+
? `/jobs?wallet=${encodeURIComponent(addr)}&limit=${requestedLimit}`
|
|
84
|
+
: `/jobs?limit=${requestedLimit}`;
|
|
85
|
+
try {
|
|
86
|
+
const jobs = await apiGet<Array<Record<string, unknown>>>(url);
|
|
87
|
+
for (const j of jobs) {
|
|
88
|
+
const id = j.job_id as string;
|
|
89
|
+
if (!seen.has(id)) {
|
|
90
|
+
seen.add(id);
|
|
91
|
+
collected.push(j);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
} catch {
|
|
95
|
+
// Skip address that errored; continue with others.
|
|
44
96
|
}
|
|
45
97
|
}
|
|
46
98
|
|
|
47
|
-
|
|
48
|
-
|
|
99
|
+
if (collected.length === 0) return text("No jobs found.");
|
|
100
|
+
|
|
101
|
+
collected.sort((a, b) => {
|
|
102
|
+
const aTime = new Date((a.created_at as string) ?? 0).getTime();
|
|
103
|
+
const bTime = new Date((b.created_at as string) ?? 0).getTime();
|
|
104
|
+
return bTime - aTime;
|
|
105
|
+
});
|
|
106
|
+
const jobs = collected.slice(0, requestedLimit);
|
|
49
107
|
|
|
50
108
|
const lines = [`Recent jobs (${jobs.length}):`];
|
|
51
109
|
for (const j of jobs) {
|
|
@@ -55,12 +113,16 @@ export function registerJobTools(server: McpServer): void {
|
|
|
55
113
|
: j.status === "processing"
|
|
56
114
|
? "\u2026"
|
|
57
115
|
: "\u2717";
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
116
|
+
const amount = (j.settled_amount ?? j.estimated_cost) as string | number | undefined;
|
|
117
|
+
const cost = amount != null ? `$${Number(amount).toFixed(4)}` : "";
|
|
118
|
+
const method = ((j.settlement_trace as Record<string, unknown> | undefined)
|
|
119
|
+
?.payment_attempt as { payment_method?: string } | undefined)?.payment_method;
|
|
120
|
+
const methodLabel = method ? `[${method.replace(/_usdc$/, "").replace("stripe_", "")}]` : "";
|
|
121
|
+
const shortId = (j.job_id as string)?.slice(0, 8);
|
|
122
|
+
const shortAgent = j.agent_id ? String(j.agent_id).slice(0, 8) : "?";
|
|
123
|
+
const when = formatRelativeTime(j.created_at as string | null);
|
|
62
124
|
lines.push(
|
|
63
|
-
` ${status} ${
|
|
125
|
+
` ${status} ${shortId} agent=${shortAgent} ${cost} ${methodLabel} ${when}`.trimEnd(),
|
|
64
126
|
);
|
|
65
127
|
}
|
|
66
128
|
return text(lines.join("\n"));
|
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 {
|
|
@@ -11,9 +12,10 @@ import {
|
|
|
11
12
|
getConfiguredMethods,
|
|
12
13
|
hasWalletConfigured,
|
|
13
14
|
normalizePaymentMethod,
|
|
15
|
+
isCardPaymentEnabled,
|
|
14
16
|
} from "../core/payments.js";
|
|
15
17
|
import { requiresSpendConfirmation } from "../core/config.js";
|
|
16
|
-
import {
|
|
18
|
+
import { ensureConsumerPrincipalForMethod } from "../core/principal.js";
|
|
17
19
|
import { getOrCreatePendingCardSetup, formatCardSetupBlocks } from "../core/card-setup.js";
|
|
18
20
|
import {
|
|
19
21
|
formatPaymentChoicePrompt,
|
|
@@ -70,21 +72,29 @@ export function registerPassTools(server: McpServer): void {
|
|
|
70
72
|
},
|
|
71
73
|
async ({ agent_id, pack_id, pay_with, confirmed }) => {
|
|
72
74
|
if (!hasWalletConfigured()) {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
"To use crypto: wallet_setup({ action: \"create\" })",
|
|
81
|
-
);
|
|
75
|
+
if (isCardPaymentEnabled()) {
|
|
76
|
+
try {
|
|
77
|
+
const { url } = await getOrCreatePendingCardSetup();
|
|
78
|
+
return multiText(...formatCardSetupBlocks(url));
|
|
79
|
+
} catch {
|
|
80
|
+
// Fall through to the setup message below.
|
|
81
|
+
}
|
|
82
82
|
}
|
|
83
|
+
const setupLines = [
|
|
84
|
+
"No payment method configured.",
|
|
85
|
+
"",
|
|
86
|
+
"Supported rails: Tempo USDC, Base USDC, Solana USDC.",
|
|
87
|
+
"Run wallet_setup({ action: \"create\" }) to create a crypto wallet,",
|
|
88
|
+
"or wallet_setup({ action: \"import\" }) with an existing key.",
|
|
89
|
+
];
|
|
90
|
+
if (isCardPaymentEnabled()) {
|
|
91
|
+
setupLines.push("", "Or wallet_setup({ action: \"add-card\" }) to connect a credit card.");
|
|
92
|
+
}
|
|
93
|
+
return text(setupLines.join("\n"));
|
|
83
94
|
}
|
|
84
95
|
|
|
85
96
|
const agent = await getAgent(agent_id);
|
|
86
97
|
const agentName = agent.name ?? agent_id;
|
|
87
|
-
const principal = await ensureConsumerPrincipal();
|
|
88
98
|
const program = getCreditPackProgram(agent);
|
|
89
99
|
const offers = (program?.packs ?? [])
|
|
90
100
|
.map((pack) => findOffer(agent, pack.key ?? ""))
|
|
@@ -155,6 +165,7 @@ export function registerPassTools(server: McpServer): void {
|
|
|
155
165
|
}
|
|
156
166
|
|
|
157
167
|
const method = resolveConfirmationMethod(pay_with, pending?.method, compatibleMethods);
|
|
168
|
+
const principal = await ensureConsumerPrincipalForMethod(method);
|
|
158
169
|
if (requiresSpendConfirmation() && !confirmed) {
|
|
159
170
|
pendingCreditPackPurchases.set(agent.id, {
|
|
160
171
|
agentId: agent.id,
|
|
@@ -192,7 +203,8 @@ export function registerPassTools(server: McpServer): void {
|
|
|
192
203
|
formatCreditPack(result.credit_pack),
|
|
193
204
|
`Consumer principal: ${result.consumer_principal}`,
|
|
194
205
|
"",
|
|
195
|
-
"Future runs
|
|
206
|
+
"Future runs for this agent will automatically use this credit pack while units remain.",
|
|
207
|
+
"That includes run_agent, and solve whenever it selects this same agent.",
|
|
196
208
|
].join("\n"));
|
|
197
209
|
},
|
|
198
210
|
);
|
|
@@ -202,10 +214,14 @@ export function registerPassTools(server: McpServer): void {
|
|
|
202
214
|
"Show discounted credit-pack offers for an agent plus any balances available under the current consumer principal.",
|
|
203
215
|
{
|
|
204
216
|
agent_id: z.string().describe("Agent ID (UUID, slug, or name)"),
|
|
217
|
+
pay_with: z.string().optional().describe("Optional payment method context used to inspect the matching consumer principal."),
|
|
205
218
|
},
|
|
206
|
-
async ({ agent_id }) => {
|
|
219
|
+
async ({ agent_id, pay_with }) => {
|
|
207
220
|
const agent = await getAgent(agent_id);
|
|
208
|
-
const result = await
|
|
221
|
+
const result = await getCreditPackInventory(agent.id, pay_with);
|
|
222
|
+
if (!result) {
|
|
223
|
+
return text(`Could not load credit-pack inventory for ${agent.name}.`);
|
|
224
|
+
}
|
|
209
225
|
|
|
210
226
|
const lines = [
|
|
211
227
|
`Credit packs for ${agent.name}`,
|