agent402-mcp 0.13.13 → 0.13.15
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 +1 -0
- package/discovery.js +5 -1
- package/index.js +9 -5
- package/networks.js +112 -1
- package/output-schemas.js +1 -1
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -87,6 +87,7 @@ so set `AGENT402_MAX_PER_CALL` if you want a hard per-call ceiling.
|
|
|
87
87
|
- **Decide** (`decide.plan`, `decide.execute`, `decide.feedback`): describe a job and get a call-ready plan over the catalog and outside x402 sellers (tools in order, fallbacks, validated params, chained steps), then run it yourself or through Agent402. Listed when the server offers Decide; see https://agent402.tools/decide.
|
|
88
88
|
- The rest of the 500+ tool catalog is reachable via the meta tools `catalog.search` / `catalog.find` + `catalog.call` - keeping your context window small. The same dotted names are listed on the hosted connector; the older snake names (`search_tools`, `find_tool`, `call_tool`, `get_payment_info`, `describe_server`, `list_top_sellers`, and the flagship `search_web` …) remain accepted as CallTool aliases, so existing configs keep working.
|
|
89
89
|
- When a call hits HTTP 402: with a wallet key set (`AGENT_KEY` for the EVM chains, Robinhood Chain included; `SOLANA_AGENT_KEY` for Solana), the server signs an x402 payment on a chain the seller accepts and retries; with only a credits key it sends `Authorization: Bearer a402_…` and the server debits the balance on success; with neither it solves the tool's proof-of-work challenge (~0.2 s of CPU) on the eligible tools. (The service settles USDC on Base, Solana, Polygon, Arbitrum, Monad, Celo, Avalanche, Sei, Optimism, Stellar and Algorand, plus USDG on Robinhood Chain - 12 chains total - for callers using a raw x402 client rather than this package.)
|
|
90
|
+
- Since 0.13.15 the server's requests to the catalog and to sellers (catalog reads, the 402 and the paid retry) refuse a TLS chain through one of the intermediate CAs listed in `networks.js` (keys reported recoverable in Mozilla bug 2083449), keyed on each certificate's SubjectPublicKeyInfo hash, on a dedicated dispatcher. With a proxy or custom dispatcher installed, requests go out through it unchanged.
|
|
90
91
|
- `payment.info` tells the model which mode it's in and what a wallet or a credits key would unlock.
|
|
91
92
|
- `server.describe` returns orientation (flagship-first tools, install one-liners, free vs paid, discovery URLs). Call it first.
|
|
92
93
|
- `sellers.list` returns the live leaderboards - `wire: "x402"` (default) ranks x402 sellers settling the most USDC (primarily on Base) in the last ~24h from on-chain transfers; `wire: "mpp"` ranks live-verified MPP sellers by inbound USDC.e transfers on Tempo. Free to call (no payment, no proof-of-work). Useful for agents discovering the wider x402 / MPP economy beyond this single service's catalog.
|
package/discovery.js
CHANGED
|
@@ -5,10 +5,14 @@
|
|
|
5
5
|
// DISCOVERY_ATTEMPTS tries in all, each wait capped at DISCOVERY_MAX_WAIT_MS.
|
|
6
6
|
// A paid or priced call never goes through here: a repeat of one could settle
|
|
7
7
|
// twice.
|
|
8
|
+
import { distrustFetch } from "./networks.js";
|
|
9
|
+
|
|
8
10
|
export const DISCOVERY_ATTEMPTS = 3;
|
|
9
11
|
export const DISCOVERY_MAX_WAIT_MS = 5000;
|
|
10
12
|
|
|
11
13
|
const sleepMs = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
14
|
+
// Discovery reads go through the distrusted-intermediate refusal (networks.js).
|
|
15
|
+
const ownFetch = distrustFetch();
|
|
12
16
|
|
|
13
17
|
/** The wait (ms) a shed 503 asks for, or null when `r` is not one. */
|
|
14
18
|
export async function shedRetryWaitMs(r, maxWaitMs = DISCOVERY_MAX_WAIT_MS) {
|
|
@@ -30,7 +34,7 @@ export async function shedRetryWaitMs(r, maxWaitMs = DISCOVERY_MAX_WAIT_MS) {
|
|
|
30
34
|
* response (2xx or not) and how many attempts it took; the caller decides how
|
|
31
35
|
* to report a failure, naming the surface and `res.status`.
|
|
32
36
|
*/
|
|
33
|
-
export async function fetchDiscovery(url, init, { fetchImpl =
|
|
37
|
+
export async function fetchDiscovery(url, init, { fetchImpl = ownFetch, sleep = sleepMs, attempts: max = DISCOVERY_ATTEMPTS } = {}) {
|
|
34
38
|
let res;
|
|
35
39
|
let attempts = 0;
|
|
36
40
|
for (;;) {
|
package/index.js
CHANGED
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
// Robinhood Chain, "base,solana", or a raw CAIP-2 like eip155:4663), optional
|
|
25
25
|
import { createHash } from "node:crypto";
|
|
26
26
|
import { readFileSync } from "node:fs";
|
|
27
|
-
import { parseNetworkPrefs, withNetworkPreference } from "./networks.js";
|
|
27
|
+
import { parseNetworkPrefs, withNetworkPreference, distrustFetch } from "./networks.js";
|
|
28
28
|
import { fetchDiscovery, discoveryFailure, getDiscoveryJson } from "./discovery.js";
|
|
29
29
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
30
30
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
@@ -51,6 +51,10 @@ import {
|
|
|
51
51
|
} from "./output-schemas.js";
|
|
52
52
|
|
|
53
53
|
const BASE = (process.env.AGENT402_URL || "https://agent402.tools").replace(/\/$/, "");
|
|
54
|
+
// Catalog and seller requests (the 402 and the paid retry included) go through
|
|
55
|
+
// a dedicated dispatcher that refuses a TLS chain through a distrusted
|
|
56
|
+
// intermediate CA (networks.js).
|
|
57
|
+
const ownFetch = distrustFetch();
|
|
54
58
|
const AGENT_KEY = process.env.AGENT_KEY || "";
|
|
55
59
|
// Solana secret key: base58 string (Phantom/solana-keygen export) or a JSON
|
|
56
60
|
// byte array. Either key alone enables paid calls; with both, the buyer can
|
|
@@ -165,7 +169,7 @@ function getPayFetch() {
|
|
|
165
169
|
// Without it the client effectively always picks Base on multi-chain
|
|
166
170
|
// sellers, leaving non-default rails unreachable.
|
|
167
171
|
withNetworkPreference(client, parseNetworkPrefs(process.env.AGENT402_NETWORKS));
|
|
168
|
-
return wrapFetchWithPayment(
|
|
172
|
+
return wrapFetchWithPayment(ownFetch, client);
|
|
169
173
|
})();
|
|
170
174
|
return payFetchPromise;
|
|
171
175
|
}
|
|
@@ -265,7 +269,7 @@ function usdFromAccepts(accepts) {
|
|
|
265
269
|
async function quotedUsd(url, init, listUsd) {
|
|
266
270
|
if (!CAPS_CONFIGURED) return listUsd;
|
|
267
271
|
try {
|
|
268
|
-
const pre = await
|
|
272
|
+
const pre = await ownFetch(url, init);
|
|
269
273
|
if (pre.status !== 402) return listUsd;
|
|
270
274
|
let quoted = null;
|
|
271
275
|
const header = pre.headers.get("payment-required");
|
|
@@ -321,7 +325,7 @@ async function callEndpoint(tool, args = {}) {
|
|
|
321
325
|
if (spentUsd + price > BUDGET) {
|
|
322
326
|
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 };
|
|
323
327
|
}
|
|
324
|
-
res = await
|
|
328
|
+
res = await ownFetch(url, { ...init, headers: { ...init.headers, Authorization: `Bearer ${CREDITS_KEY}` } });
|
|
325
329
|
if (res.ok) spentUsd += price;
|
|
326
330
|
if (res.status === 402) {
|
|
327
331
|
let body = {}; try { body = await res.clone().json(); } catch { /* plain */ }
|
|
@@ -355,7 +359,7 @@ async function callEndpoint(tool, args = {}) {
|
|
|
355
359
|
// 402 round-trip entirely (challenges are single-use and tool-scoped).
|
|
356
360
|
const challenge = await getDiscoveryJson(`proof-of-work challenge for "${tool.slug}"`, `${BASE}/api/pow/challenge?slug=${encodeURIComponent(tool.slug)}`);
|
|
357
361
|
const nonce = solvePow(challenge);
|
|
358
|
-
res = await
|
|
362
|
+
res = await ownFetch(url, { ...init, headers: { ...init.headers, "X-Pow-Solution": `${challenge.token}:${nonce}` } });
|
|
359
363
|
} else {
|
|
360
364
|
return { content: [{ type: "text", text: walletRequiredText(tool) }], isError: true };
|
|
361
365
|
}
|
package/networks.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
// Network preference for the x402 buyer, pure helpers, no side effects
|
|
1
|
+
// Network preference for the x402 buyer, pure helpers, no side effects; and
|
|
2
|
+
// the TLS refusal at the end of the file (its only load-time step is a data:
|
|
3
|
+
// URL fetch that makes no connection).
|
|
2
4
|
//
|
|
3
5
|
// AGENT402_NETWORKS (comma-separated, e.g. "robinhood" or "base,solana" or a
|
|
4
6
|
// raw CAIP-2 like "eip155:4663") restricts AND orders which payment options
|
|
@@ -6,6 +8,8 @@
|
|
|
6
8
|
// underlying @x402 client picks whichever registered scheme matches first,
|
|
7
9
|
// which on a multi-chain seller is effectively always Base, making rails like
|
|
8
10
|
// USDG on Robinhood Chain unreachable no matter what the wallet holds.
|
|
11
|
+
import tls from "node:tls";
|
|
12
|
+
import { createHash, X509Certificate } from "node:crypto";
|
|
9
13
|
|
|
10
14
|
export const NETWORK_CAIP2 = {
|
|
11
15
|
base: "eip155:8453",
|
|
@@ -56,3 +60,110 @@ export function withNetworkPreference(client, prefs) {
|
|
|
56
60
|
orig({ ...paymentRequired, accepts: filterAcceptsByNetworks(paymentRequired?.accepts, prefs) });
|
|
57
61
|
return client;
|
|
58
62
|
}
|
|
63
|
+
|
|
64
|
+
// ---------------------------------------------------------------------------
|
|
65
|
+
// Transport: refuse TLS chains through a distrusted intermediate CA, for this
|
|
66
|
+
// server's own requests (a dedicated dispatcher, nothing global).
|
|
67
|
+
//
|
|
68
|
+
// The private keys of the intermediate CAs below are recoverable (Mozilla bug
|
|
69
|
+
// 2083449). A cross-signed path makes them validate against "emSign ECC Root
|
|
70
|
+
// CA - G3" in Node's bundled store, and Node checks no revocation by default,
|
|
71
|
+
// so a certificate minted with one of these keys for a seller's host would be
|
|
72
|
+
// accepted, and a forged 402 could name a different payTo. Keyed on the
|
|
73
|
+
// SubjectPublicKeyInfo sha256 of every certificate in the presented chain.
|
|
74
|
+
// The same code as agent402-client's tls-distrust.js (the parent repo's
|
|
75
|
+
// scripts/test-tls-distrust.js holds the copies equal); it lives in this file
|
|
76
|
+
// because the images copy an explicit file list. It applies only when the
|
|
77
|
+
// process-wide dispatcher is still Node's own default (a dispatcher the
|
|
78
|
+
// caller installed, with its proxy, CA or timeouts, is left in charge). It can be removed once the
|
|
79
|
+
// Node releases this package supports ship a CA store that no longer builds
|
|
80
|
+
// the path.
|
|
81
|
+
export const DISTRUSTED_SPKI_SHA256 = Object.freeze({
|
|
82
|
+
"ebed6475c2c2d80fcb199f3fe8dd6e6b3ce60b77eb15d4e42c3dec0c6f5b5105": "EM DV TLS CA - G3B",
|
|
83
|
+
"43bffa19d6ef651451f6841e019931e96f6f2eaeff28ab4b6f1ce4576de2717b": "EM OV TLS CA - G3B",
|
|
84
|
+
"e0254c7fcdc265e2f0f8a428dabb75ab4ae039b4872d62f5ee57f0877d5a8a41": "EM EV TLS CA - G3B",
|
|
85
|
+
"49dd353dc2b919a3770645117d0e60c16115e7d1bc259eefe734b00ea434fe5f": "EM DV TLS CA - G3C",
|
|
86
|
+
"63ecefee431d49523bc1136c4e72d0206a477086caf606cd5cf025686c109cf8": "EM OV TLS CA - G3C",
|
|
87
|
+
"5c0660a2b164910d8ff589c18e14552e9e0ec237dcefe2cb736d41a4811f81a0": "EM EV TLS CA - G3C",
|
|
88
|
+
"ec4ffa1ad0ac88081314b7aaffef6a91f97cd57291bb5ba686c5415f5eb4f6b6": "EM OV CS CA - G3B",
|
|
89
|
+
"92c063d67e8f14db71d288e11266ffdaf69cbf115f7bab14f3bc2f68d25e0e30": "EM OV CS CA - G3C",
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
/** checkServerIdentity that refuses a chain through a listed key (plus any
|
|
93
|
+
* `extraSpki` hex hashes), else defers to Node's hostname check. */
|
|
94
|
+
export function distrustCheckServerIdentity(extraSpki = []) {
|
|
95
|
+
const extra = new Set([...extraSpki].map((h) => String(h).toLowerCase()));
|
|
96
|
+
const nodeCheck = tls.checkServerIdentity;
|
|
97
|
+
return (host, cert) => {
|
|
98
|
+
const seen = [];
|
|
99
|
+
for (let c = cert; c && typeof c === "object" && !seen.includes(c) && seen.length < 16; c = c.issuerCertificate === c ? null : c.issuerCertificate) {
|
|
100
|
+
seen.push(c);
|
|
101
|
+
if (!c.raw) continue;
|
|
102
|
+
let spki;
|
|
103
|
+
try {
|
|
104
|
+
spki = createHash("sha256").update(new X509Certificate(c.raw).publicKey.export({ type: "spki", format: "der" })).digest("hex");
|
|
105
|
+
} catch {
|
|
106
|
+
return Object.assign(new Error(`Refusing the TLS certificate for ${host}: distrusted intermediate check could not read the certificate chain`), { code: "ERR_TLS_DISTRUSTED_INTERMEDIATE" });
|
|
107
|
+
}
|
|
108
|
+
const name = DISTRUSTED_SPKI_SHA256[spki] || (extra.has(spki) ? "listed key" : null);
|
|
109
|
+
if (name) return Object.assign(new Error(`Refusing the TLS certificate for ${host}: distrusted intermediate (${name}) in the certificate chain`), { code: "ERR_TLS_DISTRUSTED_INTERMEDIATE" });
|
|
110
|
+
}
|
|
111
|
+
return nodeCheck(host, cert);
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const GLOBAL_DISPATCHER = Symbol.for("undici.globalDispatcher.1");
|
|
116
|
+
|
|
117
|
+
// Node's own default dispatcher, observed when this module loads. Node fills
|
|
118
|
+
// the slot when fetch is first called (synchronously, before any connection),
|
|
119
|
+
// so when the slot is still empty here a data: URL fetch fills it with Node's
|
|
120
|
+
// instance and nothing else can have put it there. When the slot is already
|
|
121
|
+
// filled at load, whoever filled it is unknown and it is treated as the
|
|
122
|
+
// caller's (no dedicated dispatcher), unless another copy of this code (the
|
|
123
|
+
// client and the MCP server share it) observed that same instance as Node's.
|
|
124
|
+
const OBSERVED = Symbol.for("agent402.tlsDistrust.nodeDefaultDispatcher");
|
|
125
|
+
function observeNodeDefault() {
|
|
126
|
+
if (typeof process !== "object" || !process?.versions?.node || process.versions.bun) return null;
|
|
127
|
+
const current = globalThis[GLOBAL_DISPATCHER];
|
|
128
|
+
if (current) return globalThis[OBSERVED] === current ? current : null;
|
|
129
|
+
if (typeof globalThis.fetch !== "function") return null;
|
|
130
|
+
try { globalThis.fetch("data:,").catch(() => {}); } catch { return null; }
|
|
131
|
+
const observed = globalThis[GLOBAL_DISPATCHER] || null;
|
|
132
|
+
if (observed) Object.defineProperty(globalThis, OBSERVED, { value: observed, configurable: false, enumerable: false, writable: false });
|
|
133
|
+
return observed;
|
|
134
|
+
}
|
|
135
|
+
const NODE_DEFAULT_DISPATCHER = observeNodeDefault();
|
|
136
|
+
|
|
137
|
+
/** True when the process-wide dispatcher is not Node's own default: the
|
|
138
|
+
* caller installed one, or its origin is unknown. */
|
|
139
|
+
export function callerDispatcherInstalled() {
|
|
140
|
+
return !NODE_DEFAULT_DISPATCHER || globalThis[GLOBAL_DISPATCHER] !== NODE_DEFAULT_DISPATCHER;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** A dedicated undici Agent carrying the refusal, or null when this platform
|
|
144
|
+
* cannot take one or the caller installed their own global dispatcher (see
|
|
145
|
+
* the header). */
|
|
146
|
+
export async function distrustDispatcher({ extraSpki = [] } = {}) {
|
|
147
|
+
if (callerDispatcherInstalled()) return null;
|
|
148
|
+
const AgentClass = NODE_DEFAULT_DISPATCHER.constructor;
|
|
149
|
+
if (typeof AgentClass !== "function") return null;
|
|
150
|
+
return new AgentClass({ connect: { checkServerIdentity: distrustCheckServerIdentity(extraSpki) } });
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** Wrap a fetch so every request it makes goes through distrustDispatcher().
|
|
154
|
+
* With no baseFetch it calls globalThis.fetch as it is at request time. A
|
|
155
|
+
* caller's own init.dispatcher, or a global dispatcher the caller installed
|
|
156
|
+
* (checked on every request), is left alone. Pass the result as the base
|
|
157
|
+
* fetch of a payment wrapper (wrapFetchWithPayment(distrustFetch(), client))
|
|
158
|
+
* so the 402 and the paid retry both go through it. */
|
|
159
|
+
export function distrustFetch(baseFetch, opts = {}) {
|
|
160
|
+
if (baseFetch !== undefined && typeof baseFetch !== "function") throw new Error("distrustFetch needs a fetch function");
|
|
161
|
+
let pending = null;
|
|
162
|
+
return async (input, init = {}) => {
|
|
163
|
+
const f = baseFetch || globalThis.fetch;
|
|
164
|
+
if (callerDispatcherInstalled() || init?.dispatcher !== undefined) return f(input, init);
|
|
165
|
+
pending ||= distrustDispatcher(opts).catch(() => null);
|
|
166
|
+
const dispatcher = await pending;
|
|
167
|
+
return f(input, dispatcher ? { ...init, dispatcher } : init);
|
|
168
|
+
};
|
|
169
|
+
}
|
package/output-schemas.js
CHANGED
|
@@ -541,7 +541,7 @@ export function mcpInitializeInstructions(baseUrl, { decide = false, mpp = false
|
|
|
541
541
|
// can pay the other tools there too.
|
|
542
542
|
`${mpp ? "Hosted (proof-of-work tools free; the rest payable there over MPP from a wallet)" : "No wallet yet (hosted, proof-of-work tools only)"}: ${install.claudeCodeHosted}`,
|
|
543
543
|
`Cursor mcp.json: { "mcpServers": { "agent402": { "url": "${hosted}" } } }`,
|
|
544
|
-
`Why pay here (usage priced under a quoted ceiling, a failed call is not charged and the receipt
|
|
544
|
+
`Why pay here (usage priced under a quoted ceiling, a failed x402 call is not charged and the receipt shows it, keyed retries never pay twice, one key for tools + models + reports, no wallet needed for the proof-of-work tools or a report by card, proof at /status and /revenue): ${baseUrl}/#why`,
|
|
545
545
|
`Docs: ${baseUrl}/llms.txt · ${baseUrl}/api/find?q=… · status ${baseUrl}/status`,
|
|
546
546
|
].join("\n");
|
|
547
547
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent402-mcp",
|
|
3
|
-
"version": "0.13.
|
|
3
|
+
"version": "0.13.15",
|
|
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",
|
|
@@ -70,7 +70,7 @@
|
|
|
70
70
|
],
|
|
71
71
|
"license": "MIT",
|
|
72
72
|
"dependencies": {
|
|
73
|
-
"@modelcontextprotocol/sdk": "^1.
|
|
73
|
+
"@modelcontextprotocol/sdk": "^1.32.1",
|
|
74
74
|
"@solana/kit": "^8.4.0",
|
|
75
75
|
"@x402/core": "2.28.0",
|
|
76
76
|
"@x402/evm": "2.28.0",
|
|
@@ -81,7 +81,7 @@
|
|
|
81
81
|
"overrides": {
|
|
82
82
|
"qs": "6.16.0",
|
|
83
83
|
"ws": "^8.21.0",
|
|
84
|
-
"hono": "4.13.
|
|
84
|
+
"hono": "4.13.13",
|
|
85
85
|
"fast-uri": "3.1.8",
|
|
86
86
|
"body-parser": "2.3.0",
|
|
87
87
|
"@hono/node-server": "2.0.12",
|