@agentwonderland/mcp 0.1.23 → 0.1.25
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__/amount-utils.test.d.ts +1 -0
- package/dist/core/__tests__/amount-utils.test.js +11 -0
- package/dist/core/__tests__/card-setup.test.d.ts +1 -0
- package/dist/core/__tests__/card-setup.test.js +99 -0
- package/dist/core/__tests__/formatters.test.d.ts +1 -0
- package/dist/core/__tests__/formatters.test.js +15 -0
- package/dist/core/__tests__/passes.test.d.ts +1 -0
- package/dist/core/__tests__/passes.test.js +82 -0
- package/dist/core/__tests__/payments.test.d.ts +1 -0
- package/dist/core/__tests__/payments.test.js +101 -0
- package/dist/core/__tests__/principal.test.d.ts +1 -0
- package/dist/core/__tests__/principal.test.js +67 -0
- package/dist/core/__tests__/spend-policy.test.d.ts +1 -0
- package/dist/core/__tests__/spend-policy.test.js +40 -0
- package/dist/core/amount-utils.d.ts +1 -0
- package/dist/core/amount-utils.js +4 -0
- package/dist/core/api-client.d.ts +9 -4
- package/dist/core/api-client.js +52 -22
- package/dist/core/base-charge.js +3 -2
- package/dist/core/card-setup.d.ts +20 -13
- package/dist/core/card-setup.js +85 -29
- package/dist/core/config.d.ts +22 -0
- package/dist/core/config.js +46 -2
- package/dist/core/formatters.d.ts +4 -3
- package/dist/core/formatters.js +10 -8
- package/dist/core/index.d.ts +2 -0
- package/dist/core/index.js +2 -0
- package/dist/core/ows-adapter.d.ts +10 -2
- package/dist/core/ows-adapter.js +54 -10
- package/dist/core/passes.d.ts +40 -0
- package/dist/core/passes.js +32 -0
- package/dist/core/payments.d.ts +8 -0
- package/dist/core/payments.js +111 -17
- package/dist/core/principal.d.ts +2 -0
- package/dist/core/principal.js +109 -0
- package/dist/core/solana-charge.d.ts +9 -0
- package/dist/core/solana-charge.js +96 -0
- package/dist/core/spend-policy.d.ts +12 -0
- package/dist/core/spend-policy.js +53 -0
- package/dist/core/types.d.ts +11 -2
- package/dist/index.js +11 -3
- package/dist/prompts/index.js +4 -2
- package/dist/resources/agents.js +1 -1
- package/dist/resources/wallet.js +8 -1
- package/dist/tools/__tests__/_payment-confirmation.test.d.ts +1 -0
- package/dist/tools/__tests__/_payment-confirmation.test.js +30 -0
- package/dist/tools/_payment-confirmation.d.ts +6 -0
- package/dist/tools/_payment-confirmation.js +28 -0
- package/dist/tools/agent-info.js +16 -2
- package/dist/tools/favorites.js +1 -1
- package/dist/tools/index.d.ts +1 -0
- package/dist/tools/index.js +1 -0
- package/dist/tools/observability.d.ts +2 -0
- package/dist/tools/observability.js +20 -0
- package/dist/tools/passes.d.ts +2 -0
- package/dist/tools/passes.js +157 -0
- package/dist/tools/run.js +127 -53
- package/dist/tools/solve.js +115 -51
- package/dist/tools/wallet.js +110 -59
- package/package.json +3 -1
- package/src/core/__tests__/amount-utils.test.ts +13 -0
- package/src/core/__tests__/card-setup.test.ts +118 -0
- package/src/core/__tests__/formatters.test.ts +17 -0
- package/src/core/__tests__/passes.test.ts +94 -0
- package/src/core/__tests__/payments.test.ts +122 -0
- package/src/core/__tests__/principal.test.ts +87 -0
- package/src/core/__tests__/spend-policy.test.ts +58 -0
- package/src/core/amount-utils.ts +5 -0
- package/src/core/api-client.ts +70 -23
- package/src/core/base-charge.ts +3 -2
- package/src/core/card-setup.ts +109 -34
- package/src/core/config.ts +74 -3
- package/src/core/formatters.ts +13 -9
- package/src/core/index.ts +2 -0
- package/src/core/ows-adapter.ts +74 -8
- package/src/core/passes.ts +74 -0
- package/src/core/payments.ts +130 -17
- package/src/core/principal.ts +128 -0
- package/src/core/solana-charge.ts +150 -0
- package/src/core/spend-policy.ts +69 -0
- package/src/core/types.ts +11 -2
- package/src/index.ts +11 -3
- package/src/prompts/index.ts +4 -2
- package/src/resources/agents.ts +1 -1
- package/src/resources/wallet.ts +8 -1
- package/src/tools/__tests__/_payment-confirmation.test.ts +45 -0
- package/src/tools/_payment-confirmation.ts +52 -0
- package/src/tools/agent-info.ts +25 -2
- package/src/tools/favorites.ts +1 -4
- package/src/tools/index.ts +1 -0
- package/src/tools/observability.ts +43 -0
- package/src/tools/passes.ts +228 -0
- package/src/tools/run.ts +174 -57
- package/src/tools/solve.ts +147 -59
- package/src/tools/wallet.ts +132 -62
package/dist/tools/solve.js
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { apiGet, apiPost, apiPostWithPayment } from "../core/api-client.js";
|
|
3
|
-
import {
|
|
4
|
-
import { hasWalletConfigured, getConfiguredMethods, getAcceptedPaymentMethods, getWalletAddress, } from "../core/payments.js";
|
|
3
|
+
import { getOrCreatePendingCardSetup, formatCardSetupBlocks } from "../core/card-setup.js";
|
|
4
|
+
import { getCompatiblePaymentMethods, hasWalletConfigured, getConfiguredMethods, getAcceptedPaymentMethods, getWalletAddress, normalizePaymentMethod, toRegistryPaymentMethod, } from "../core/payments.js";
|
|
5
5
|
import { requiresSpendConfirmation, getDefaultTipAmount } from "../core/config.js";
|
|
6
6
|
import { agentList, formatRunResult } from "../core/formatters.js";
|
|
7
|
+
import { canSpend, recordSpend, requiresPolicyConfirmation } from "../core/spend-policy.js";
|
|
7
8
|
import { uploadLocalFiles } from "../core/file-upload.js";
|
|
8
9
|
import { storeFeedbackToken } from "./_token-cache.js";
|
|
10
|
+
import { formatPaymentLabel, formatSolveConfirmationCommand, makeSolvePendingKey, resolveConfirmationMethod, } from "./_payment-confirmation.js";
|
|
11
|
+
import { getActiveCreditPack, getCreditPackInventory, getCreditPackProgram } from "../core/passes.js";
|
|
9
12
|
const POLL_INTERVAL_MS = 3000;
|
|
10
13
|
const POLL_MAX_MS = 120000;
|
|
11
14
|
async function pollSolveJob(jobId) {
|
|
@@ -24,7 +27,7 @@ async function pollSolveJob(jobId) {
|
|
|
24
27
|
}
|
|
25
28
|
}
|
|
26
29
|
catch {
|
|
27
|
-
// Ignore poll errors
|
|
30
|
+
// Ignore poll errors and keep trying.
|
|
28
31
|
}
|
|
29
32
|
}
|
|
30
33
|
return { status: "failed", error_code: "POLL_TIMEOUT" };
|
|
@@ -35,11 +38,12 @@ function text(t) {
|
|
|
35
38
|
function multiText(...blocks) {
|
|
36
39
|
return { content: blocks.map((t) => ({ type: "text", text: t })) };
|
|
37
40
|
}
|
|
41
|
+
const pendingSolves = new Map();
|
|
38
42
|
function feedbackAsk(jobId, agentId, cost, tipSent) {
|
|
39
43
|
const defaultTip = getDefaultTipAmount();
|
|
40
|
-
const costLine = cost != null ? `Paid $${cost.toFixed(cost < 0.01 ? 4 : 2)}.` : "";
|
|
44
|
+
const costLine = cost != null ? `Paid $${cost.toFixed(cost < 0.01 ? 4 : 2)}.` : "This run used your available credit pack.";
|
|
41
45
|
return [
|
|
42
|
-
|
|
46
|
+
"IMPORTANT: After presenting the result above, you MUST ask the user:",
|
|
43
47
|
`"${costLine} Was this result helpful? I can rate it (1-5 stars) or tip the builder for you."`,
|
|
44
48
|
tipSent ?? "",
|
|
45
49
|
"",
|
|
@@ -67,8 +71,22 @@ async function autoTip(jobId, agentId, agentName, feedbackToken) {
|
|
|
67
71
|
return `Auto-tip of $${defaultTip.toFixed(2)} failed.`;
|
|
68
72
|
}
|
|
69
73
|
}
|
|
74
|
+
function buildCreditPackSummary(agent) {
|
|
75
|
+
const creditPacks = getCreditPackProgram(agent);
|
|
76
|
+
if (!creditPacks?.packs?.length)
|
|
77
|
+
return [];
|
|
78
|
+
return [
|
|
79
|
+
"Discounted credit packs available:",
|
|
80
|
+
...creditPacks.packs.map((pack) => {
|
|
81
|
+
const price = Number(pack.price_usd ?? "0");
|
|
82
|
+
const units = pack.included_units ?? 0;
|
|
83
|
+
const unitPrice = units > 0 ? ` ($${(price / units).toFixed(2)}/unit)` : "";
|
|
84
|
+
return ` ${pack.name ?? pack.key}: ${units} units for $${(pack.price_usd ?? "0.00")}${unitPrice}`;
|
|
85
|
+
}),
|
|
86
|
+
];
|
|
87
|
+
}
|
|
70
88
|
export function registerSolveTools(server) {
|
|
71
|
-
server.tool("solve", "Solve a task by finding the best agent, paying, and executing. The primary way to use the marketplace. If spending confirmation is enabled, returns a price quote first — call again with confirmed: true to execute. Local file paths in the input
|
|
89
|
+
server.tool("solve", "Solve a task by finding the best agent, paying, and executing. The primary way to use the marketplace. If spending confirmation is enabled, returns a price quote first — call again with confirmed: true to execute. Local file paths in the input are auto-uploaded before execution.", {
|
|
72
90
|
intent: z
|
|
73
91
|
.string()
|
|
74
92
|
.describe("What you want to accomplish (natural language)"),
|
|
@@ -86,8 +104,9 @@ export function registerSolveTools(server) {
|
|
|
86
104
|
.describe("Maximum budget in USD"),
|
|
87
105
|
pay_with: z
|
|
88
106
|
.string()
|
|
89
|
-
.
|
|
90
|
-
.
|
|
107
|
+
.trim()
|
|
108
|
+
.min(1)
|
|
109
|
+
.describe("Required payment method — wallet ID, chain name (tempo, base, etc.), or 'card'. Use wallet_status to see configured options."),
|
|
91
110
|
confirmed: z
|
|
92
111
|
.boolean()
|
|
93
112
|
.optional()
|
|
@@ -95,9 +114,8 @@ export function registerSolveTools(server) {
|
|
|
95
114
|
}, async ({ intent, input, budget, pay_with, confirmed }) => {
|
|
96
115
|
if (!hasWalletConfigured()) {
|
|
97
116
|
try {
|
|
98
|
-
const {
|
|
99
|
-
|
|
100
|
-
return { content: blocks.map((t) => ({ type: "text", text: t })) };
|
|
117
|
+
const { url } = await getOrCreatePendingCardSetup();
|
|
118
|
+
return multiText(...formatCardSetupBlocks(url));
|
|
101
119
|
}
|
|
102
120
|
catch {
|
|
103
121
|
return text("No payment method configured.\n\n" +
|
|
@@ -105,8 +123,15 @@ export function registerSolveTools(server) {
|
|
|
105
123
|
"To use crypto: wallet_setup({ action: \"create\" })");
|
|
106
124
|
}
|
|
107
125
|
}
|
|
108
|
-
const
|
|
109
|
-
|
|
126
|
+
const pendingKey = makeSolvePendingKey(intent, input, budget);
|
|
127
|
+
const pending = pendingSolves.get(pendingKey);
|
|
128
|
+
const configuredMethods = getConfiguredMethods();
|
|
129
|
+
const requestedMethod = pay_with;
|
|
130
|
+
const normalizedRequestedMethod = normalizePaymentMethod(requestedMethod);
|
|
131
|
+
if (!normalizedRequestedMethod) {
|
|
132
|
+
return text(`Payment method "${requestedMethod}" is not configured.\n\n` +
|
|
133
|
+
"Use wallet_status to review your current payment methods.");
|
|
134
|
+
}
|
|
110
135
|
let processedInput;
|
|
111
136
|
try {
|
|
112
137
|
const uploadResult = await uploadLocalFiles(input);
|
|
@@ -129,8 +154,9 @@ export function registerSolveTools(server) {
|
|
|
129
154
|
storeFeedbackToken(jobId, result.feedback_token, agentId);
|
|
130
155
|
}
|
|
131
156
|
const cost = result.cost;
|
|
157
|
+
const usedCreditPack = result.consumption_mode === "credit_pack";
|
|
132
158
|
const tipMsg = result.feedback_token ? await autoTip(jobId, agentId, agentName, result.feedback_token) : "";
|
|
133
|
-
return multiText(formatRunResult(result), feedbackAsk(jobId, agentId, cost, tipMsg));
|
|
159
|
+
return multiText(formatRunResult(result), feedbackAsk(jobId, agentId, usedCreditPack ? undefined : cost, tipMsg));
|
|
134
160
|
}
|
|
135
161
|
catch (err) {
|
|
136
162
|
const isAuthError = err instanceof Error &&
|
|
@@ -139,9 +165,10 @@ export function registerSolveTools(server) {
|
|
|
139
165
|
if (!isAuthError || !hasWalletConfigured())
|
|
140
166
|
throw err;
|
|
141
167
|
}
|
|
142
|
-
// Path 2: Wallet-only discovery — find agents, pick best, pay via MPP
|
|
143
168
|
const params = new URLSearchParams({ q: intent, limit: "5" });
|
|
144
|
-
const acceptedMethods =
|
|
169
|
+
const acceptedMethods = requestedMethod
|
|
170
|
+
? [toRegistryPaymentMethod(requestedMethod)].filter((value) => !!value)
|
|
171
|
+
: getAcceptedPaymentMethods();
|
|
145
172
|
if (acceptedMethods.length > 0) {
|
|
146
173
|
params.set("accepted_payment_methods", acceptedMethods.join(","));
|
|
147
174
|
}
|
|
@@ -149,64 +176,97 @@ export function registerSolveTools(server) {
|
|
|
149
176
|
if (!agents || agents.length === 0) {
|
|
150
177
|
return text(`No agents found matching "${intent}".`);
|
|
151
178
|
}
|
|
152
|
-
// Show discovery results
|
|
153
179
|
const discovery = agentList(agents, intent);
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
const price = parseFloat(a.pricePer1kTokens ?? "0.01");
|
|
158
|
-
const cost = a.pricingModel === "fixed" ? price : (inputTokens / 1000) * price;
|
|
159
|
-
return cost <= budget;
|
|
180
|
+
const affordable = agents.filter((agent) => {
|
|
181
|
+
const price = parseFloat(agent.pricePerRunUsd ?? "0.01");
|
|
182
|
+
return price <= budget;
|
|
160
183
|
});
|
|
161
184
|
const selected = affordable[0] ?? agents[0];
|
|
162
|
-
const
|
|
163
|
-
const
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
185
|
+
const activeCreditPack = await getCreditPackInventory(selected.id).then(getActiveCreditPack);
|
|
186
|
+
const compatibleMethods = getCompatiblePaymentMethods(selected, configuredMethods);
|
|
187
|
+
if (!compatibleMethods.includes(normalizedRequestedMethod)) {
|
|
188
|
+
return text(`The best matching agent cannot be paid with "${requestedMethod}".\n\n` +
|
|
189
|
+
`Available payment methods for ${selected.name}: ${compatibleMethods.join(", ") || "none"}.\n` +
|
|
190
|
+
"Choose another payment method or refine your search.");
|
|
191
|
+
}
|
|
192
|
+
const method = resolveConfirmationMethod(pay_with, pending?.method, compatibleMethods);
|
|
193
|
+
const spendCheckMethod = method ?? normalizedRequestedMethod;
|
|
194
|
+
const selectedPrice = parseFloat(selected.pricePerRunUsd ?? "0.01");
|
|
195
|
+
const estimatedCost = selectedPrice;
|
|
196
|
+
const needsPolicyConfirmation = !activeCreditPack && requiresPolicyConfirmation(spendCheckMethod, estimatedCost);
|
|
197
|
+
if (!activeCreditPack && (requiresSpendConfirmation() || needsPolicyConfirmation) && !confirmed) {
|
|
198
|
+
pendingSolves.set(pendingKey, { method });
|
|
168
199
|
return text([
|
|
169
200
|
discovery,
|
|
170
201
|
"",
|
|
171
202
|
`Best match: ${selected.name}`,
|
|
172
203
|
`Cost: $${estimatedCost.toFixed(2)}`,
|
|
173
|
-
`Payment: ${method}`,
|
|
204
|
+
`Payment: ${formatPaymentLabel(method)}`,
|
|
205
|
+
...(() => {
|
|
206
|
+
const summary = buildCreditPackSummary(selected);
|
|
207
|
+
return summary.length > 0 ? ["", ...summary] : [];
|
|
208
|
+
})(),
|
|
174
209
|
"",
|
|
175
210
|
"To proceed, call:",
|
|
176
|
-
|
|
211
|
+
formatSolveConfirmationCommand(intent, budget, method),
|
|
177
212
|
"",
|
|
178
213
|
"To cancel, do nothing.",
|
|
179
214
|
].join("\n"));
|
|
180
215
|
}
|
|
181
216
|
let result;
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
217
|
+
if (!activeCreditPack) {
|
|
218
|
+
const spendCheck = canSpend({
|
|
219
|
+
method: spendCheckMethod,
|
|
220
|
+
amountUsd: estimatedCost,
|
|
221
|
+
});
|
|
222
|
+
if (!spendCheck.ok) {
|
|
223
|
+
return text(spendCheck.message);
|
|
224
|
+
}
|
|
190
225
|
}
|
|
191
226
|
try {
|
|
192
|
-
|
|
227
|
+
let usedPaidMethod = !activeCreditPack;
|
|
228
|
+
if (activeCreditPack) {
|
|
229
|
+
try {
|
|
230
|
+
result = await apiPost(`/agents/${selected.id}/run`, { input: processedInput }, { ensureConsumerPrincipal: true });
|
|
231
|
+
}
|
|
232
|
+
catch (packErr) {
|
|
233
|
+
const packApiErr = packErr;
|
|
234
|
+
if (packApiErr?.status !== 402)
|
|
235
|
+
throw packErr;
|
|
236
|
+
result = await apiPostWithPayment(`/agents/${selected.id}/run`, { input: processedInput }, method);
|
|
237
|
+
usedPaidMethod = true;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
else {
|
|
241
|
+
result = await apiPostWithPayment(`/agents/${selected.id}/run`, { input: processedInput }, method);
|
|
242
|
+
}
|
|
243
|
+
if (usedPaidMethod) {
|
|
244
|
+
recordSpend(spendCheckMethod, estimatedCost);
|
|
245
|
+
}
|
|
193
246
|
}
|
|
194
247
|
catch (err) {
|
|
195
248
|
const apiErr = err;
|
|
196
249
|
if (apiErr?.status === 402) {
|
|
197
|
-
return text(
|
|
198
|
-
"
|
|
199
|
-
"
|
|
250
|
+
return text([
|
|
251
|
+
"Payment failed — your wallet may not have enough funds or the selected method was rejected.",
|
|
252
|
+
"",
|
|
253
|
+
"Check your balance and try again.",
|
|
254
|
+
"Use wallet_status to check your current payment methods.",
|
|
255
|
+
...(() => {
|
|
256
|
+
const summary = buildCreditPackSummary(selected);
|
|
257
|
+
return summary.length > 0 ? ["", ...summary] : [];
|
|
258
|
+
})(),
|
|
259
|
+
].join("\n"));
|
|
200
260
|
}
|
|
201
261
|
return text(`Error: ${apiErr?.message ?? "Failed to run agent"}`);
|
|
202
262
|
}
|
|
263
|
+
pendingSolves.delete(pendingKey);
|
|
203
264
|
const jobId = result.job_id ?? "";
|
|
204
|
-
const
|
|
265
|
+
const agentId = result.agent_id ?? selected.id;
|
|
205
266
|
const status = result.status;
|
|
206
267
|
if (result.feedback_token) {
|
|
207
|
-
storeFeedbackToken(jobId, result.feedback_token,
|
|
268
|
+
storeFeedbackToken(jobId, result.feedback_token, agentId);
|
|
208
269
|
}
|
|
209
|
-
// Async agent — poll until complete
|
|
210
270
|
if (status === "processing") {
|
|
211
271
|
const pollResult = await pollSolveJob(jobId);
|
|
212
272
|
if (pollResult.status === "completed") {
|
|
@@ -224,9 +284,9 @@ export function registerSolveTools(server) {
|
|
|
224
284
|
asyncFormatted,
|
|
225
285
|
].join("\n");
|
|
226
286
|
const asyncCost = result.cost;
|
|
227
|
-
|
|
287
|
+
const usedCreditPack = result.consumption_mode === "credit_pack";
|
|
288
|
+
return multiText(asyncOutput, feedbackAsk(jobId, agentId, usedCreditPack ? undefined : asyncCost, ""));
|
|
228
289
|
}
|
|
229
|
-
// Async agent failed
|
|
230
290
|
const failedFormatted = formatRunResult({
|
|
231
291
|
...result,
|
|
232
292
|
status: "failed",
|
|
@@ -240,10 +300,14 @@ export function registerSolveTools(server) {
|
|
|
240
300
|
"",
|
|
241
301
|
failedFormatted,
|
|
242
302
|
].join("\n");
|
|
243
|
-
|
|
303
|
+
const usedCreditPack = result.consumption_mode === "credit_pack";
|
|
304
|
+
return multiText(failedOutput, usedCreditPack
|
|
305
|
+
? "The agent execution failed and the reserved credit-pack unit was released automatically."
|
|
306
|
+
: "The agent execution failed. A refund has been initiated automatically.");
|
|
244
307
|
}
|
|
245
308
|
const actualCost = result.cost;
|
|
246
|
-
const
|
|
309
|
+
const usedCreditPack = result.consumption_mode === "credit_pack";
|
|
310
|
+
const tipMsg = result.feedback_token ? await autoTip(jobId, agentId, selected.name ?? "", result.feedback_token) : "";
|
|
247
311
|
const output = [
|
|
248
312
|
discovery,
|
|
249
313
|
"",
|
|
@@ -252,6 +316,6 @@ export function registerSolveTools(server) {
|
|
|
252
316
|
"",
|
|
253
317
|
formatRunResult(result, { paymentMethod: method }),
|
|
254
318
|
].join("\n");
|
|
255
|
-
return multiText(output, feedbackAsk(jobId,
|
|
319
|
+
return multiText(output, feedbackAsk(jobId, agentId, usedCreditPack ? undefined : actualCost, tipMsg));
|
|
256
320
|
});
|
|
257
321
|
}
|
package/dist/tools/wallet.js
CHANGED
|
@@ -1,30 +1,50 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import { getWallets, getCardConfig, setCardConfig, addWallet } from "../core/config.js";
|
|
2
|
+
import { getWallets, getCardConfig, setCardConfig, addWallet, getPendingCardSetupToken, getSpendPolicy, setSpendPolicy, } from "../core/config.js";
|
|
3
3
|
import { getWalletAddress } from "../core/payments.js";
|
|
4
|
-
import {
|
|
5
|
-
import { isOwsAvailable, createOwsWallet, importKeyToOws, listOwsWallets, } from "../core/ows-adapter.js";
|
|
4
|
+
import { getOrCreatePendingCardSetup, formatCardSetupBlocks, getCardCapabilities, pollCardSetup, } from "../core/card-setup.js";
|
|
5
|
+
import { isOwsAvailable, createOwsWallet, importKeyToOws, listOwsWallets, listOwsWalletsByChain, } from "../core/ows-adapter.js";
|
|
6
|
+
import { ensureConsumerPrincipal, getConsumerPrincipal } from "../core/principal.js";
|
|
6
7
|
function text(t) {
|
|
7
8
|
return { content: [{ type: "text", text: t }] };
|
|
8
9
|
}
|
|
9
|
-
// Pending card setup — stored between the QR display call and the confirmation poll
|
|
10
|
-
let pendingCardSetup = null;
|
|
11
10
|
export function registerWalletTools(server) {
|
|
12
11
|
// ── wallet_status (extracted from check_wallet) ─────────────────
|
|
13
12
|
server.tool("wallet_status", "Check payment readiness. Shows all configured wallets, their chains, addresses, and card status.", {}, async () => {
|
|
14
13
|
const wallets = getWallets();
|
|
15
14
|
const card = getCardConfig();
|
|
16
|
-
|
|
15
|
+
const pendingCardSetupToken = getPendingCardSetupToken();
|
|
16
|
+
const consumerPrincipal = await getConsumerPrincipal();
|
|
17
|
+
if (wallets.length === 0 && !card && !pendingCardSetupToken) {
|
|
17
18
|
return text("No payment methods configured.\nUse wallet_setup to create or import a wallet.");
|
|
18
19
|
}
|
|
19
20
|
const lines = ["Payment methods:"];
|
|
20
21
|
for (const w of wallets) {
|
|
21
|
-
const addr = await getWalletAddress(w.id);
|
|
22
22
|
const label = w.label ? ` (${w.label})` : "";
|
|
23
23
|
const storage = w.keyType === "ows" ? " [encrypted]" : " [plaintext]";
|
|
24
|
-
|
|
24
|
+
const chainAddresses = await Promise.all(w.chains.map(async (chainName) => {
|
|
25
|
+
const addr = await getWalletAddress(chainName);
|
|
26
|
+
return `${chainName}: ${addr ?? "unknown"}`;
|
|
27
|
+
}));
|
|
28
|
+
lines.push(` ${w.id}${label}${storage}: ${chainAddresses.join(" | ")}`);
|
|
25
29
|
}
|
|
26
30
|
if (card) {
|
|
27
31
|
lines.push(` Card: ${card.brand} ****${card.last4}`);
|
|
32
|
+
const capabilities = await getCardCapabilities();
|
|
33
|
+
if (capabilities.spt_status === "enabled") {
|
|
34
|
+
lines.push(" Card MPP: ready");
|
|
35
|
+
}
|
|
36
|
+
else if (capabilities.spt_status === "unavailable") {
|
|
37
|
+
lines.push(` Card MPP: unavailable — ${capabilities.message ?? "Stripe Shared Payment Token access is not enabled."}`);
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
lines.push(` Card MPP: unknown — ${capabilities.message ?? "Could not determine card payment readiness."}`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
if (pendingCardSetupToken) {
|
|
44
|
+
lines.push(" Card setup: pending confirmation");
|
|
45
|
+
}
|
|
46
|
+
if (consumerPrincipal) {
|
|
47
|
+
lines.push("", `Consumer principal: ${consumerPrincipal}`);
|
|
28
48
|
}
|
|
29
49
|
return text(lines.join("\n"));
|
|
30
50
|
});
|
|
@@ -42,7 +62,7 @@ export function registerWalletTools(server) {
|
|
|
42
62
|
.optional()
|
|
43
63
|
.describe("Private key hex string (required for 'import', ignored for 'create')"),
|
|
44
64
|
chain: z.enum(["tempo", "base", "solana"]).optional()
|
|
45
|
-
.describe("Primary chain (default: tempo). Solana
|
|
65
|
+
.describe("Primary chain (default: tempo). Solana uses an OWS wallet plus Stripe deposit-mode USDC."),
|
|
46
66
|
}, async ({ action, name, key, chain }) => {
|
|
47
67
|
// ── Card setup flow ──────────────────────────────────────
|
|
48
68
|
if (action === "add-card") {
|
|
@@ -50,48 +70,32 @@ export function registerWalletTools(server) {
|
|
|
50
70
|
if (existing) {
|
|
51
71
|
return text(`Card already connected: ${existing.brand} ****${existing.last4}\n\nTo replace it, remove the current card first.`);
|
|
52
72
|
}
|
|
73
|
+
const pendingToken = getPendingCardSetupToken();
|
|
53
74
|
// Check if there's a pending setup to complete (user said they're done)
|
|
54
|
-
if (
|
|
55
|
-
const
|
|
56
|
-
const result = await pollCardSetup(pendingToken, 120_000);
|
|
57
|
-
pendingCardSetup = null;
|
|
75
|
+
if (pendingToken) {
|
|
76
|
+
const result = await pollCardSetup(pendingToken, 250);
|
|
58
77
|
if (result) {
|
|
59
|
-
|
|
78
|
+
const principal = await ensureConsumerPrincipal();
|
|
79
|
+
return text(`Connected! ${result.brand} ****${result.last4} is ready for payments.\n\n` +
|
|
80
|
+
`Consumer principal: ${principal}`);
|
|
60
81
|
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
try {
|
|
65
|
-
const { qr, url, token } = await initiateCardSetup();
|
|
66
|
-
// Auto-poll for up to 90 seconds — if the user enters their card
|
|
67
|
-
// quickly, the tool returns success without a second call
|
|
68
|
-
const result = await pollCardSetup(token, 90_000);
|
|
69
|
-
if (result) {
|
|
82
|
+
try {
|
|
83
|
+
const { url } = await getOrCreatePendingCardSetup();
|
|
84
|
+
const blocks = formatCardSetupBlocks(url);
|
|
70
85
|
return {
|
|
71
|
-
content:
|
|
72
|
-
{ type: "text", text: "\n" + qr.trim() },
|
|
73
|
-
{ type: "text", text: [
|
|
74
|
-
`Open to connect a card: ${url}`,
|
|
75
|
-
"",
|
|
76
|
-
`Connected! ${result.brand} ****${result.last4} is ready for payments.`,
|
|
77
|
-
].join("\n") },
|
|
78
|
-
],
|
|
86
|
+
content: blocks.map((t) => ({ type: "text", text: t })),
|
|
79
87
|
};
|
|
80
88
|
}
|
|
81
|
-
|
|
82
|
-
|
|
89
|
+
catch {
|
|
90
|
+
return text("Card setup timed out. Run wallet_setup({ action: \"add-card\" }) to try again.");
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
// Create new card setup session and return immediately so clients
|
|
94
|
+
// don't appear hung while the user is still in the browser flow.
|
|
95
|
+
try {
|
|
96
|
+
const { url } = await getOrCreatePendingCardSetup();
|
|
83
97
|
return {
|
|
84
|
-
content:
|
|
85
|
-
{ type: "text", text: "\n" + qr.trim() },
|
|
86
|
-
{ type: "text", text: [
|
|
87
|
-
`IMPORTANT: Present this link to the user to connect a payment card:`,
|
|
88
|
-
"",
|
|
89
|
-
url,
|
|
90
|
-
"",
|
|
91
|
-
`Tell the user: "Scan the QR code above or open this link to connect your card. Let me know when you're done."`,
|
|
92
|
-
`After they confirm, call wallet_setup({ action: "add-card" }) to complete setup.`,
|
|
93
|
-
].join("\n") },
|
|
94
|
-
],
|
|
98
|
+
content: formatCardSetupBlocks(url).map((t) => ({ type: "text", text: t })),
|
|
95
99
|
};
|
|
96
100
|
}
|
|
97
101
|
catch {
|
|
@@ -113,16 +117,20 @@ export function registerWalletTools(server) {
|
|
|
113
117
|
}
|
|
114
118
|
const selectedChains = chain === "solana" ? ["solana"] : ["tempo", "base"];
|
|
115
119
|
const defaultCh = chain ?? "tempo";
|
|
120
|
+
const preferredOwsChain = defaultCh === "solana" ? "solana" : "evm";
|
|
116
121
|
const owsReady = await isOwsAvailable();
|
|
117
122
|
if (action === "create") {
|
|
118
123
|
// Check for existing OWS wallets first
|
|
119
124
|
if (owsReady) {
|
|
120
|
-
const existing =
|
|
125
|
+
const existing = preferredOwsChain === "solana"
|
|
126
|
+
? await listOwsWalletsByChain("solana")
|
|
127
|
+
: await listOwsWallets();
|
|
121
128
|
if (existing.length > 0) {
|
|
122
129
|
const w = existing[0];
|
|
123
130
|
// Check if already in config
|
|
124
131
|
const wallets = getWallets();
|
|
125
|
-
const alreadyLinked = wallets.find(wl => wl.owsWalletId === w.id);
|
|
132
|
+
const alreadyLinked = wallets.find((wl) => wl.owsWalletId === w.id);
|
|
133
|
+
let chainStatus = "Linked to Agent Wonderland.";
|
|
126
134
|
if (!alreadyLinked) {
|
|
127
135
|
addWallet({
|
|
128
136
|
id: w.name,
|
|
@@ -133,14 +141,29 @@ export function registerWalletTools(server) {
|
|
|
133
141
|
label: w.name,
|
|
134
142
|
});
|
|
135
143
|
}
|
|
144
|
+
else {
|
|
145
|
+
const mergedChains = [...new Set([...alreadyLinked.chains, ...selectedChains])];
|
|
146
|
+
if (mergedChains.length !== alreadyLinked.chains.length) {
|
|
147
|
+
addWallet({
|
|
148
|
+
...alreadyLinked,
|
|
149
|
+
chains: mergedChains,
|
|
150
|
+
});
|
|
151
|
+
chainStatus = `Updated linked chains: ${mergedChains.join(", ")}.`;
|
|
152
|
+
}
|
|
153
|
+
else {
|
|
154
|
+
chainStatus = "Already linked to Agent Wonderland.";
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
const principal = await ensureConsumerPrincipal();
|
|
136
158
|
return text([
|
|
137
159
|
"Found existing OWS wallet:",
|
|
138
160
|
` Name: ${w.name}`,
|
|
139
161
|
` Address: ${w.address}`,
|
|
140
162
|
` Chains: ${selectedChains.join(", ")}`,
|
|
141
163
|
` Storage: ~/.ows/ (encrypted)`,
|
|
164
|
+
` Consumer principal: ${principal}`,
|
|
142
165
|
"",
|
|
143
|
-
|
|
166
|
+
chainStatus,
|
|
144
167
|
"",
|
|
145
168
|
`Fund this address with USDC on ${defaultCh === "solana" ? "Solana" : "Tempo"} to start using agents.`,
|
|
146
169
|
].join("\n"));
|
|
@@ -149,10 +172,12 @@ export function registerWalletTools(server) {
|
|
|
149
172
|
if (!owsReady) {
|
|
150
173
|
return text("Cannot create wallet: OWS (Open Wallet Standard) is not installed.\n" +
|
|
151
174
|
"Install with: npm install -g @open-wallet-standard/core\n\n" +
|
|
152
|
-
|
|
175
|
+
(defaultCh === "solana"
|
|
176
|
+
? "Solana wallets currently require OWS-managed encrypted storage."
|
|
177
|
+
: "Alternatively, use action 'import' with an existing private key."));
|
|
153
178
|
}
|
|
154
179
|
const walletName = name ?? `aw-${Date.now()}`;
|
|
155
|
-
const result = await createOwsWallet(walletName);
|
|
180
|
+
const result = await createOwsWallet(walletName, preferredOwsChain);
|
|
156
181
|
// Persist to config so payment flows can find it
|
|
157
182
|
addWallet({
|
|
158
183
|
id: walletName,
|
|
@@ -162,6 +187,7 @@ export function registerWalletTools(server) {
|
|
|
162
187
|
defaultChain: defaultCh,
|
|
163
188
|
label: walletName,
|
|
164
189
|
});
|
|
190
|
+
const principal = await ensureConsumerPrincipal();
|
|
165
191
|
return text([
|
|
166
192
|
`Wallet created [encrypted]:`,
|
|
167
193
|
` ID: ${result.walletId}`,
|
|
@@ -169,6 +195,7 @@ export function registerWalletTools(server) {
|
|
|
169
195
|
` Name: ${walletName}`,
|
|
170
196
|
` Chains: ${selectedChains.join(", ")}`,
|
|
171
197
|
` Storage: ~/.ows/ (AES-256-GCM encrypted)`,
|
|
198
|
+
` Consumer principal: ${principal}`,
|
|
172
199
|
"",
|
|
173
200
|
`Fund this address with USDC on ${defaultCh === "solana" ? "Solana" : "Tempo"} to start using agents.`,
|
|
174
201
|
"For testnet: npx mppx account fund",
|
|
@@ -177,7 +204,7 @@ export function registerWalletTools(server) {
|
|
|
177
204
|
// action === "import"
|
|
178
205
|
if (owsReady) {
|
|
179
206
|
const walletName = name ?? `imported-${Date.now()}`;
|
|
180
|
-
const result = await importKeyToOws(key, walletName);
|
|
207
|
+
const result = await importKeyToOws(key, walletName, preferredOwsChain);
|
|
181
208
|
addWallet({
|
|
182
209
|
id: walletName,
|
|
183
210
|
keyType: "ows",
|
|
@@ -186,14 +213,20 @@ export function registerWalletTools(server) {
|
|
|
186
213
|
defaultChain: defaultCh,
|
|
187
214
|
label: walletName,
|
|
188
215
|
});
|
|
216
|
+
const principal = await ensureConsumerPrincipal();
|
|
189
217
|
return text([
|
|
190
218
|
`Key imported to OWS [encrypted]:`,
|
|
191
219
|
` ID: ${result.walletId}`,
|
|
192
220
|
` Address: ${result.address}`,
|
|
193
221
|
` Name: ${walletName}`,
|
|
194
222
|
` Chains: ${selectedChains.join(", ")}`,
|
|
223
|
+
` Consumer principal: ${principal}`,
|
|
195
224
|
].join("\n"));
|
|
196
225
|
}
|
|
226
|
+
if (defaultCh === "solana") {
|
|
227
|
+
return text("Solana key import currently requires OWS (Open Wallet Standard) so the ed25519 key can be stored safely.\n" +
|
|
228
|
+
"Install with: npm install -g @open-wallet-standard/core");
|
|
229
|
+
}
|
|
197
230
|
// No OWS — store plaintext
|
|
198
231
|
try {
|
|
199
232
|
const { privateKeyToAccount } = await import("viem/accounts");
|
|
@@ -208,11 +241,13 @@ export function registerWalletTools(server) {
|
|
|
208
241
|
defaultChain: defaultCh,
|
|
209
242
|
label: walletName,
|
|
210
243
|
});
|
|
244
|
+
const principal = await ensureConsumerPrincipal();
|
|
211
245
|
return text([
|
|
212
246
|
`Key imported [plaintext] \u2014 OWS not available for encrypted storage.`,
|
|
213
247
|
` Address: ${account.address}`,
|
|
214
248
|
` Name: ${walletName}`,
|
|
215
249
|
` Chains: ${selectedChains.join(", ")}`,
|
|
250
|
+
` Consumer principal: ${principal}`,
|
|
216
251
|
"",
|
|
217
252
|
"Install OWS for encrypted storage: npm install -g @open-wallet-standard/core",
|
|
218
253
|
].join("\n"));
|
|
@@ -234,26 +269,42 @@ export function registerWalletTools(server) {
|
|
|
234
269
|
.positive()
|
|
235
270
|
.optional()
|
|
236
271
|
.describe("Maximum USD per day across all transactions"),
|
|
237
|
-
|
|
272
|
+
require_confirmation_above: z
|
|
273
|
+
.number()
|
|
274
|
+
.positive()
|
|
275
|
+
.optional()
|
|
276
|
+
.describe("Require manual confirmation above this USD amount even when auto-spend is enabled"),
|
|
277
|
+
}, async ({ wallet_id, max_per_tx, max_per_day, require_confirmation_above }) => {
|
|
238
278
|
const wallets = getWallets();
|
|
239
279
|
const wallet = wallets.find((w) => w.id === wallet_id);
|
|
240
280
|
if (!wallet) {
|
|
241
281
|
const available = wallets.map((w) => w.id).join(", ") || "none";
|
|
242
282
|
return text(`Wallet "${wallet_id}" not found. Available wallets: ${available}`);
|
|
243
283
|
}
|
|
244
|
-
if (max_per_tx == null &&
|
|
245
|
-
|
|
284
|
+
if (max_per_tx == null &&
|
|
285
|
+
max_per_day == null &&
|
|
286
|
+
require_confirmation_above == null) {
|
|
287
|
+
return text("No policy changes specified. Provide at least one policy field.");
|
|
246
288
|
}
|
|
289
|
+
const existing = getSpendPolicy(wallet_id) ?? {};
|
|
290
|
+
const nextPolicy = {
|
|
291
|
+
...existing,
|
|
292
|
+
...(max_per_tx != null ? { maxPerTxUsd: max_per_tx } : {}),
|
|
293
|
+
...(max_per_day != null ? { maxPerDayUsd: max_per_day } : {}),
|
|
294
|
+
...(require_confirmation_above != null ? { requireConfirmationAboveUsd: require_confirmation_above } : {}),
|
|
295
|
+
};
|
|
296
|
+
setSpendPolicy(wallet_id, nextPolicy);
|
|
247
297
|
// Build policy summary
|
|
248
298
|
const policies = [];
|
|
249
|
-
if (
|
|
250
|
-
policies.push(`Max per transaction: $${
|
|
299
|
+
if (nextPolicy.maxPerTxUsd != null) {
|
|
300
|
+
policies.push(`Max per transaction: $${nextPolicy.maxPerTxUsd.toFixed(2)}`);
|
|
301
|
+
}
|
|
302
|
+
if (nextPolicy.maxPerDayUsd != null) {
|
|
303
|
+
policies.push(`Max per day: $${nextPolicy.maxPerDayUsd.toFixed(2)}`);
|
|
251
304
|
}
|
|
252
|
-
if (
|
|
253
|
-
policies.push(`
|
|
305
|
+
if (nextPolicy.requireConfirmationAboveUsd != null) {
|
|
306
|
+
policies.push(`Require confirmation above: $${nextPolicy.requireConfirmationAboveUsd.toFixed(2)}`);
|
|
254
307
|
}
|
|
255
|
-
// Note: actual persistence depends on the config module supporting policy fields.
|
|
256
|
-
// For now, we report what would be set. The core/config module will handle storage.
|
|
257
308
|
return text([
|
|
258
309
|
`Spending policy set for wallet "${wallet_id}":`,
|
|
259
310
|
...policies.map((p) => ` ${p}`),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agentwonderland/mcp",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.25",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "MCP server for the Agent Wonderland AI agent marketplace",
|
|
6
6
|
"bin": {
|
|
@@ -23,6 +23,8 @@
|
|
|
23
23
|
},
|
|
24
24
|
"dependencies": {
|
|
25
25
|
"@modelcontextprotocol/sdk": "^1.12.1",
|
|
26
|
+
"@solana/spl-token": "^0.4.14",
|
|
27
|
+
"@solana/web3.js": "^1.98.4",
|
|
26
28
|
"mppx": "^0.4.9",
|
|
27
29
|
"qrcode": "^1.5.4",
|
|
28
30
|
"viem": "^2.47.6",
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { toAtomicAmount } from "../amount-utils.js";
|
|
3
|
+
|
|
4
|
+
describe("toAtomicAmount", () => {
|
|
5
|
+
it("converts decimal USDC challenge amounts into atomic units", () => {
|
|
6
|
+
expect(toAtomicAmount("0.020000")).toBe(20000n);
|
|
7
|
+
expect(toAtomicAmount("1.000000")).toBe(1000000n);
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it("supports different decimal precisions", () => {
|
|
11
|
+
expect(toAtomicAmount("0.50", 2)).toBe(50n);
|
|
12
|
+
});
|
|
13
|
+
});
|