agent402-mcp 0.13.3 → 0.13.5
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/README.md +2 -2
- package/index.js +99 -9
- package/package.json +6 -6
package/README.md
CHANGED
|
@@ -10,7 +10,7 @@ Your agent gets browser rendering, screenshots, PDF text extraction, URL→markd
|
|
|
10
10
|
MCP server - e.g. claude.ai → Settings → Connectors → Add custom connector. The
|
|
11
11
|
pure-CPU tools run free there (rate-limited), and the paid tools are payable
|
|
12
12
|
right on the connector over [MPP](https://agent402.tools/what-is-mpp): a paid
|
|
13
|
-
call answers
|
|
13
|
+
call answers with a tool result carrying the challenges in `_meta["org.paymentauth/payment-required"]` (`-32043` if a presented credential was refused), and an MCP client
|
|
14
14
|
wrapped with `mppx`'s `McpClient.wrap()` (USDC on Base/Celo, or native Tempo)
|
|
15
15
|
pays and retries on its own, receipt in `_meta`. For no rate limit, or to pay
|
|
16
16
|
over x402 or by card instead, run this package locally:
|
|
@@ -43,7 +43,7 @@ With prepaid card credits (no wallet) - buy a pack at https://agent402.tools/cre
|
|
|
43
43
|
}
|
|
44
44
|
```
|
|
45
45
|
|
|
46
|
-
Without a wallet or credits key - the
|
|
46
|
+
Without a wallet or credits key - the 150+ pure-CPU tools work free via proof-of-work (the network/browser/memory tools will ask for a wallet or a credits key):
|
|
47
47
|
|
|
48
48
|
```json
|
|
49
49
|
{
|
package/index.js
CHANGED
|
@@ -201,6 +201,90 @@ function walletRequiredText(tool) {
|
|
|
201
201
|
].join(" ");
|
|
202
202
|
}
|
|
203
203
|
|
|
204
|
+
// ---------------------------------------------------------------------------
|
|
205
|
+
// What THIS call will actually be asked to pay.
|
|
206
|
+
//
|
|
207
|
+
// The catalog price is the ROUTE's list price, and it is not always the amount
|
|
208
|
+
// in the challenge: a route may quote per request from the body (a token-
|
|
209
|
+
// metered route, or a chat route priced by the model named in it). A cap
|
|
210
|
+
// enforced against the list price is therefore not a cap at all, so both paid
|
|
211
|
+
// paths read the amount out of the 402 before spending anything.
|
|
212
|
+
//
|
|
213
|
+
// The preflight carries no payment header and no credits key, so it can only
|
|
214
|
+
// ever draw an unpaid 402 - it never signs and never debits. Only run when a
|
|
215
|
+
// ceiling is configured: with none, the number changes nothing and the round
|
|
216
|
+
// trip is pure cost.
|
|
217
|
+
const CAPS_CONFIGURED = Number.isFinite(MAX_PER_CALL) || Number.isFinite(BUDGET);
|
|
218
|
+
|
|
219
|
+
/** The accepts array, at the top level or under the envelope keys sellers wrap it in. */
|
|
220
|
+
function acceptsOf(obj) {
|
|
221
|
+
if (!obj || typeof obj !== "object") return null;
|
|
222
|
+
if (Array.isArray(obj.accepts) && obj.accepts.length) return obj.accepts;
|
|
223
|
+
for (const k of ["payment", "x402", "paymentRequired", "payment_required"]) {
|
|
224
|
+
const nested = obj[k];
|
|
225
|
+
if (nested && typeof nested === "object" && Array.isArray(nested.accepts) && nested.accepts.length) return nested.accepts;
|
|
226
|
+
}
|
|
227
|
+
return null;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/** The MAXIMUM across accepts: the wallet picks which chain to settle on and
|
|
231
|
+
* this server cannot know which, so the only sound ceiling is the dearest
|
|
232
|
+
* offer. An entry we cannot price makes the whole quote unreadable (null),
|
|
233
|
+
* never a guess. */
|
|
234
|
+
function usdFromAccepts(accepts) {
|
|
235
|
+
if (!Array.isArray(accepts) || !accepts.length) return null;
|
|
236
|
+
let maxUsd = null;
|
|
237
|
+
for (const a of accepts) {
|
|
238
|
+
// v2 names the base-unit amount `amount`; x402 v1 called it `maxAmountRequired`.
|
|
239
|
+
const atomic = Number(a && (a.amount ?? a.maxAmountRequired));
|
|
240
|
+
if (!Number.isFinite(atomic) || atomic < 0) return null;
|
|
241
|
+
// USDC is 6 decimals on every chain these rails settle; a wrong exponent is
|
|
242
|
+
// a 1000x pricing error, so an out-of-range hint reads as unreadable.
|
|
243
|
+
const declared = (a && a.extra && a.extra.decimals) ?? (a && a.decimals);
|
|
244
|
+
const decimals = declared == null ? 6 : Number(declared);
|
|
245
|
+
if (!Number.isInteger(decimals) || decimals < 0 || decimals > 30) return null;
|
|
246
|
+
const usd = atomic / 10 ** decimals;
|
|
247
|
+
if (maxUsd == null || usd > maxUsd) maxUsd = usd;
|
|
248
|
+
}
|
|
249
|
+
return maxUsd;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/** The quoted price for this request, or `listUsd` when the 402 cannot be read.
|
|
253
|
+
* x402 v2 carries the challenge base64 in the PAYMENT-REQUIRED header and
|
|
254
|
+
* leaves the body `{}`; v1 sellers put it in the body. Header first. */
|
|
255
|
+
async function quotedUsd(url, init, listUsd) {
|
|
256
|
+
if (!CAPS_CONFIGURED) return listUsd;
|
|
257
|
+
try {
|
|
258
|
+
const pre = await fetch(url, init);
|
|
259
|
+
if (pre.status !== 402) return listUsd;
|
|
260
|
+
let quoted = null;
|
|
261
|
+
const header = pre.headers.get("payment-required");
|
|
262
|
+
if (header) {
|
|
263
|
+
try { quoted = usdFromAccepts(acceptsOf(JSON.parse(Buffer.from(header.trim(), "base64").toString("utf8")))); } catch { quoted = null; }
|
|
264
|
+
}
|
|
265
|
+
if (quoted == null) {
|
|
266
|
+
let body = null; try { body = await pre.json(); } catch { body = null; }
|
|
267
|
+
quoted = usdFromAccepts(acceptsOf(body));
|
|
268
|
+
}
|
|
269
|
+
// Larger of the two: an understated catalog must not lower the ceiling.
|
|
270
|
+
return quoted == null ? listUsd : Math.max(listUsd, quoted);
|
|
271
|
+
} catch { return listUsd; /* fail-open to the advertised price */ }
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// A refused call's tool text. A 402 body from an x402 v2 seller may also carry
|
|
275
|
+
// the whole PaymentRequired offer (this server copies its PAYMENT-REQUIRED
|
|
276
|
+
// header into the body), which is kilobytes of accepts and extension schemas
|
|
277
|
+
// an agent cannot act on in text. Keep the refusal's own fields (error,
|
|
278
|
+
// reason, hint, retry) and say where the offer is; any other body is returned
|
|
279
|
+
// as it came.
|
|
280
|
+
function failureText(status, text) {
|
|
281
|
+
let doc;
|
|
282
|
+
try { doc = JSON.parse(text); } catch { return text; }
|
|
283
|
+
if (!doc || typeof doc !== "object" || Array.isArray(doc) || typeof doc.x402Version !== "number") return text;
|
|
284
|
+
const { x402Version: _v, resource: _r, accepts, extensions: _e, ...rest } = doc;
|
|
285
|
+
return JSON.stringify({ ...rest, status, paymentOptions: Array.isArray(accepts) ? accepts.length : 0, note: "The full offer is in the PAYMENT-REQUIRED header of this 402." });
|
|
286
|
+
}
|
|
287
|
+
|
|
204
288
|
async function callEndpoint(tool, args = {}) {
|
|
205
289
|
const url = new URL(`${BASE}${tool.path}`);
|
|
206
290
|
const init = { method: tool.method, headers: { Accept: "application/json" } };
|
|
@@ -216,14 +300,16 @@ async function callEndpoint(tool, args = {}) {
|
|
|
216
300
|
|
|
217
301
|
let res;
|
|
218
302
|
if (!HAS_WALLET && HAS_CREDITS) {
|
|
219
|
-
// Card credits: same budget guards as the wallet path
|
|
220
|
-
// the
|
|
221
|
-
|
|
303
|
+
// Card credits: same budget guards as the wallet path, against the same
|
|
304
|
+
// number - the credits gate holds and debits what the 402 quotes for THIS
|
|
305
|
+
// body, not the route's list price.
|
|
306
|
+
const listUsd = parseFloat(String(tool.price).replace(/[^0-9.]/g, "")) || 0;
|
|
307
|
+
const price = await quotedUsd(url, init, listUsd);
|
|
222
308
|
if (price > MAX_PER_CALL) {
|
|
223
|
-
return { content: [{ type: "text", text: `Refused: "${tool.slug}"
|
|
309
|
+
return { content: [{ type: "text", text: `Refused: "${tool.slug}" is quoted $${price} for this request${price !== listUsd ? ` (list price ${tool.price})` : ""}, above the AGENT402_MAX_PER_CALL cap of $${MAX_PER_CALL}.` }], isError: true };
|
|
224
310
|
}
|
|
225
311
|
if (spentUsd + price > BUDGET) {
|
|
226
|
-
return { content: [{ type: "text", text: `Refused: session budget exhausted ($${spentUsd.toFixed(4)} of $${BUDGET} spent; "${tool.slug}"
|
|
312
|
+
return { content: [{ type: "text", text: `Refused: session budget exhausted ($${spentUsd.toFixed(4)} of $${BUDGET} spent; "${tool.slug}" is quoted $${price} for this request).` }], isError: true };
|
|
227
313
|
}
|
|
228
314
|
res = await fetch(url, { ...init, headers: { ...init.headers, Authorization: `Bearer ${CREDITS_KEY}` } });
|
|
229
315
|
if (res.ok) spentUsd += price;
|
|
@@ -232,16 +318,20 @@ async function callEndpoint(tool, args = {}) {
|
|
|
232
318
|
return { content: [{ type: "text", text: `Credits refused for "${tool.slug}": ${body.error || "payment required"}${body.balanceUsd != null ? ` (balance $${body.balanceUsd})` : ""}. Top up at ${body.topup || `${BASE}/credits`}.` }], isError: true };
|
|
233
319
|
}
|
|
234
320
|
} else if (HAS_WALLET) {
|
|
235
|
-
|
|
321
|
+
// The wallet signs whatever the challenge asks for (vendor spend controls
|
|
322
|
+
// are off - this package bounds spend itself), so the cap is checked
|
|
323
|
+
// against that amount, read from the 402 before anything is signed.
|
|
324
|
+
const listUsd = parseFloat(String(tool.price).replace(/[^0-9.]/g, "")) || 0;
|
|
325
|
+
const price = await quotedUsd(url, init, listUsd);
|
|
236
326
|
if (price > MAX_PER_CALL) {
|
|
237
327
|
return {
|
|
238
|
-
content: [{ type: "text", text: `Refused without paying: "${tool.slug}"
|
|
328
|
+
content: [{ type: "text", text: `Refused without paying: "${tool.slug}" is quoted $${price} for this request${price !== listUsd ? ` (list price ${tool.price})` : ""}, above the AGENT402_MAX_PER_CALL cap of $${MAX_PER_CALL}. Raise the cap on this MCP server to allow it.` }],
|
|
239
329
|
isError: true,
|
|
240
330
|
};
|
|
241
331
|
}
|
|
242
332
|
if (spentUsd + price > BUDGET) {
|
|
243
333
|
return {
|
|
244
|
-
content: [{ type: "text", text: `Refused without paying: session budget exhausted ($${spentUsd.toFixed(4)} of $${BUDGET} spent; "${tool.slug}"
|
|
334
|
+
content: [{ type: "text", text: `Refused without paying: session budget exhausted ($${spentUsd.toFixed(4)} of $${BUDGET} spent; "${tool.slug}" is quoted $${price} for this request). Restart the MCP server or raise AGENT402_BUDGET.` }],
|
|
245
335
|
isError: true,
|
|
246
336
|
};
|
|
247
337
|
}
|
|
@@ -273,7 +363,7 @@ async function callEndpoint(tool, args = {}) {
|
|
|
273
363
|
}
|
|
274
364
|
const text = await res.text();
|
|
275
365
|
if (res.status >= 400) {
|
|
276
|
-
return { content: [{ type: "text", text }], isError: true };
|
|
366
|
+
return { content: [{ type: "text", text: failureText(res.status, text) }], isError: true };
|
|
277
367
|
}
|
|
278
368
|
let parsed;
|
|
279
369
|
try { parsed = JSON.parse(text); } catch { parsed = { raw: text }; }
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent402-mcp",
|
|
3
|
-
"version": "0.13.
|
|
3
|
+
"version": "0.13.5",
|
|
4
4
|
"mcpName": "io.github.MikeyPetrillo/agent402",
|
|
5
5
|
"author": "Havok Holdings LLC <mike@agent402.tools> (https://agent402.tools)",
|
|
6
6
|
"homepage": "https://agent402.tools",
|
|
@@ -69,12 +69,12 @@
|
|
|
69
69
|
],
|
|
70
70
|
"license": "MIT",
|
|
71
71
|
"dependencies": {
|
|
72
|
-
"@modelcontextprotocol/sdk": "^1.30.
|
|
72
|
+
"@modelcontextprotocol/sdk": "^1.30.1",
|
|
73
73
|
"@solana/kit": "^5.1.0",
|
|
74
|
-
"@x402/core": "2.
|
|
75
|
-
"@x402/evm": "2.
|
|
76
|
-
"@x402/fetch": "2.
|
|
77
|
-
"@x402/svm": "2.
|
|
74
|
+
"@x402/core": "2.27.0",
|
|
75
|
+
"@x402/evm": "2.27.0",
|
|
76
|
+
"@x402/fetch": "2.27.0",
|
|
77
|
+
"@x402/svm": "2.27.0",
|
|
78
78
|
"viem": "^2.21.0"
|
|
79
79
|
},
|
|
80
80
|
"overrides": {
|