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.
Files changed (3) hide show
  1. package/README.md +2 -2
  2. package/index.js +99 -9
  3. 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 JSON-RPC error `-32042` with the challenges (`-32043` if a presented credential was refused), and an MCP client
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 200+ pure-CPU tools work free via proof-of-work (the network/browser/memory tools will ask for a wallet or a credits key):
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 (the server enforces
220
- // the balance; these keep a runaway session from draining a pack).
221
- const price = parseFloat(String(tool.price).replace(/[^0-9.]/g, "")) || 0;
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}" costs ${tool.price}/call, above the AGENT402_MAX_PER_CALL cap of $${MAX_PER_CALL}.` }], isError: true };
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}" costs ${tool.price}).` }], isError: true };
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
- const price = parseFloat(String(tool.price).replace(/[^0-9.]/g, "")) || 0;
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}" costs ${tool.price}/call, above the AGENT402_MAX_PER_CALL cap of $${MAX_PER_CALL}. Raise the cap on this MCP server to allow it.` }],
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}" costs ${tool.price}). Restart the MCP server or raise AGENT402_BUDGET.` }],
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",
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.0",
72
+ "@modelcontextprotocol/sdk": "^1.30.1",
73
73
  "@solana/kit": "^5.1.0",
74
- "@x402/core": "2.26.0",
75
- "@x402/evm": "2.26.0",
76
- "@x402/fetch": "2.26.0",
77
- "@x402/svm": "2.26.0",
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": {