agent402-mcp 0.1.0

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 +55 -0
  2. package/index.js +296 -0
  3. package/package.json +34 -0
package/README.md ADDED
@@ -0,0 +1,55 @@
1
+ # agent402-mcp
2
+
3
+ MCP server for [Agent402](https://agent402.tools) — 1000+ pay-per-call web tools for AI agents, paid per call in USDC via the [x402 protocol](https://www.x402.org), or **with compute (proof-of-work)** when no wallet is configured.
4
+
5
+ Your agent gets browser rendering, screenshots, PDF text extraction, URL→markdown, live DNS/TLS/WHOIS, wallet-keyed shared memory, and ~1000 utility/conversion tools — with payment handled invisibly underneath the MCP calls. No signup, no API key.
6
+
7
+ ## Quick start
8
+
9
+ With a funded wallet (USDC on Base) — every tool available:
10
+
11
+ ```json
12
+ {
13
+ "mcpServers": {
14
+ "agent402": {
15
+ "command": "npx",
16
+ "args": ["-y", "agent402-mcp"],
17
+ "env": { "AGENT_KEY": "0xYOUR_PRIVATE_KEY" }
18
+ }
19
+ }
20
+ }
21
+ ```
22
+
23
+ Without a wallet — the ~1000 pure-CPU tools work free via proof-of-work (the network/browser/memory tools will ask for a wallet):
24
+
25
+ ```json
26
+ {
27
+ "mcpServers": {
28
+ "agent402": { "command": "npx", "args": ["-y", "agent402-mcp"] }
29
+ }
30
+ }
31
+ ```
32
+
33
+ Claude Code: `claude mcp add agent402 -- npx -y agent402-mcp`
34
+
35
+ ## How it works
36
+
37
+ - On startup the server reads the live catalog from `https://agent402.tools/api/pricing` + `/openapi.json`.
38
+ - The high-value tools (`extract`, `render`, `screenshot`, `pdf`, `meta`, `dns`, `http-check`, `tls-cert`, `whois`, the `memory-*` coordination tools, `hash`) are exposed as first-class MCP tools.
39
+ - The other ~1000 tools are reachable via `search_tools` (find by description) + `call_tool` (call by slug) — keeping your context window small.
40
+ - When a call hits HTTP 402: with `AGENT_KEY` set, the server signs an x402 USDC payment and retries; without a key it solves the tool's proof-of-work challenge (~0.2 s of CPU) on the eligible tools.
41
+ - `payment_info` tells the model which mode it's in and what a wallet would unlock.
42
+
43
+ ## Configuration
44
+
45
+ | env | default | meaning |
46
+ | --- | --- | --- |
47
+ | `AGENT_KEY` | _(unset)_ | Hex private key of a wallet funded with USDC on Base. Unset = proof-of-work mode. |
48
+ | `AGENT402_URL` | `https://agent402.tools` | Target service (point at your own deployment). |
49
+ | `AGENT402_TOOLS` | curated set | Comma-separated slugs to expose as first-class tools. |
50
+
51
+ Use a dedicated low-value wallet for `AGENT_KEY`, funded only with what you intend to spend — calls cost $0.001–$0.02 each.
52
+
53
+ ## Test
54
+
55
+ From the repo root: `node mcp/test.js` (boots a local paywalled instance and drives the MCP server with a real client; the proof-of-work path settles real challenges).
package/index.js ADDED
@@ -0,0 +1,296 @@
1
+ #!/usr/bin/env node
2
+ // Agent402 MCP server — exposes the agent402.tools catalog (1000+ pay-per-call
3
+ // web tools) to any MCP client (Claude, ChatGPT, custom agents) and settles
4
+ // payment underneath, so the model never sees the 402 dance:
5
+ //
6
+ // • AGENT_KEY=0x… pay per call in USDC via x402 (any tool)
7
+ // • no key pay with compute (proof-of-work) on the eligible tools
8
+ //
9
+ // The full catalog is too large to register as individual MCP tools, so the
10
+ // high-value tools are first-class and everything else is reachable through
11
+ // search_tools + call_tool.
12
+ //
13
+ // Config (env):
14
+ // AGENT402_URL target service (default https://agent402.tools)
15
+ // AGENT_KEY hex private key of a funded wallet (USDC on Base) — optional
16
+ // AGENT402_TOOLS comma-separated slugs to expose first-class (overrides default)
17
+ import { createHash } from "node:crypto";
18
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
19
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
20
+ import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
21
+
22
+ const BASE = (process.env.AGENT402_URL || "https://agent402.tools").replace(/\/$/, "");
23
+ const AGENT_KEY = process.env.AGENT_KEY || "";
24
+ const VERSION = "0.1.0";
25
+
26
+ const DEFAULT_CURATED = [
27
+ // the tools agents can't replicate locally: browser, live web, PDF, shared memory
28
+ "extract", "render", "screenshot", "pdf", "meta", "dns", "http-check", "tls-cert", "whois",
29
+ "memory-write", "memory-read", "memory-remember", "memory-recall",
30
+ // one cheap pure-CPU tool so wallet-less clients see the proof-of-work path work
31
+ "hash",
32
+ ];
33
+
34
+ // stdout is the MCP protocol channel — all logging goes to stderr.
35
+ const log = (...a) => console.error("[agent402-mcp]", ...a);
36
+
37
+ // ---------------------------------------------------------------------------
38
+ // Catalog: built from the service's own machine-readable surfaces.
39
+ const catalog = new Map(); // slug -> { slug, method, path, price, description, category, computePayable, inputSchema }
40
+
41
+ async function loadCatalog() {
42
+ const [pricing, openapi] = await Promise.all([
43
+ fetch(`${BASE}/api/pricing`).then((r) => r.json()),
44
+ fetch(`${BASE}/openapi.json`).then((r) => r.json()),
45
+ ]);
46
+ for (const e of pricing.endpoints) {
47
+ const slug = e.slug ?? e.docs?.split("/tools/").pop();
48
+ if (!slug) continue;
49
+ const op = openapi.paths?.[e.path]?.[e.method.toLowerCase()];
50
+ let inputSchema = { type: "object" };
51
+ if (op) {
52
+ if (e.method === "GET") {
53
+ const params = op.parameters ?? [];
54
+ inputSchema = {
55
+ type: "object",
56
+ properties: Object.fromEntries(
57
+ params.map((p) => [p.name, { type: p.schema?.type ?? "string", ...(p.description ? { description: p.description } : {}) }])
58
+ ),
59
+ };
60
+ const required = params.filter((p) => p.required).map((p) => p.name);
61
+ if (required.length) inputSchema.required = required;
62
+ } else {
63
+ const body = op.requestBody?.content?.["application/json"]?.schema;
64
+ if (body) inputSchema = { type: "object", properties: body.properties ?? {}, ...(body.required?.length ? { required: body.required } : {}) };
65
+ }
66
+ }
67
+ catalog.set(slug, {
68
+ slug,
69
+ method: e.method,
70
+ path: e.path,
71
+ price: e.price,
72
+ description: e.description,
73
+ category: e.category,
74
+ computePayable: !!e.computePayable,
75
+ inputSchema,
76
+ });
77
+ }
78
+ return pricing;
79
+ }
80
+
81
+ // ---------------------------------------------------------------------------
82
+ // Payment: USDC via x402 when a key is configured, else proof-of-work.
83
+ let payFetchPromise;
84
+ function getPayFetch() {
85
+ payFetchPromise ??= (async () => {
86
+ const { x402Client } = await import("@x402/core/client");
87
+ const { registerExactEvmScheme } = await import("@x402/evm/exact/client");
88
+ const { wrapFetchWithPayment } = await import("@x402/fetch");
89
+ const { privateKeyToAccount } = await import("viem/accounts");
90
+ const client = new x402Client();
91
+ registerExactEvmScheme(client, { signer: privateKeyToAccount(AGENT_KEY) });
92
+ return wrapFetchWithPayment(fetch, client);
93
+ })();
94
+ return payFetchPromise;
95
+ }
96
+
97
+ function solvePow(challenge) {
98
+ const leadingZeroBits = (buf) => {
99
+ let total = 0;
100
+ for (const byte of buf) {
101
+ if (byte === 0) { total += 8; continue; }
102
+ total += Math.clz32(byte) - 24;
103
+ break;
104
+ }
105
+ return total;
106
+ };
107
+ let nonce = 0;
108
+ while (leadingZeroBits(createHash("sha256").update(`${challenge.challenge}:${nonce}`).digest()) < challenge.difficulty) nonce++;
109
+ return nonce;
110
+ }
111
+
112
+ function walletRequiredText(tool) {
113
+ return [
114
+ `"${tool.slug}" costs ${tool.price}/call and requires a USDC wallet (it is not eligible for the proof-of-work tier).`,
115
+ `To enable it: set the AGENT_KEY environment variable on this MCP server to the hex private key of a wallet`,
116
+ `funded with USDC on Base. Payment is per call via the x402 protocol — no signup or API key.`,
117
+ `Pricing and details: ${BASE}/tools/${tool.slug}`,
118
+ ].join(" ");
119
+ }
120
+
121
+ async function callEndpoint(tool, args = {}) {
122
+ const url = new URL(`${BASE}${tool.path}`);
123
+ const init = { method: tool.method, headers: { Accept: "application/json" } };
124
+ if (tool.method === "GET") {
125
+ for (const [k, v] of Object.entries(args)) {
126
+ if (v === undefined || v === null) continue;
127
+ url.searchParams.set(k, typeof v === "object" ? JSON.stringify(v) : String(v));
128
+ }
129
+ } else {
130
+ init.headers["Content-Type"] = "application/json";
131
+ init.body = JSON.stringify(args);
132
+ }
133
+
134
+ let res;
135
+ if (AGENT_KEY) {
136
+ const payFetch = await getPayFetch();
137
+ res = await payFetch(url, init);
138
+ } else if (tool.computePayable) {
139
+ // No wallet: pay with compute up front — solving before the call skips the
140
+ // 402 round-trip entirely (challenges are single-use and tool-scoped).
141
+ const challenge = await (await fetch(`${BASE}/api/pow/challenge?slug=${encodeURIComponent(tool.slug)}`)).json();
142
+ const nonce = solvePow(challenge);
143
+ res = await fetch(url, { ...init, headers: { ...init.headers, "X-Pow-Solution": `${challenge.token}:${nonce}` } });
144
+ } else {
145
+ return { content: [{ type: "text", text: walletRequiredText(tool) }], isError: true };
146
+ }
147
+
148
+ const contentType = (res.headers.get("content-type") || "").split(";")[0];
149
+ if (contentType.startsWith("image/")) {
150
+ const data = Buffer.from(await res.arrayBuffer()).toString("base64");
151
+ return { content: [{ type: "image", data, mimeType: contentType }] };
152
+ }
153
+ const text = await res.text();
154
+ return { content: [{ type: "text", text }], ...(res.status >= 400 ? { isError: true } : {}) };
155
+ }
156
+
157
+ // ---------------------------------------------------------------------------
158
+ // Tool search over the full catalog (for everything not exposed first-class).
159
+ function searchTools(query, limit = 10) {
160
+ const terms = String(query).toLowerCase().split(/[^a-z0-9]+/).filter(Boolean);
161
+ const scored = [];
162
+ for (const t of catalog.values()) {
163
+ const slug = t.slug.toLowerCase();
164
+ const hay = `${t.description} ${t.category}`.toLowerCase();
165
+ let score = 0;
166
+ for (const term of terms) {
167
+ if (slug === term) score += 10;
168
+ if (slug.includes(term)) score += 4;
169
+ if (hay.includes(term)) score += 1;
170
+ }
171
+ if (score > 0) scored.push([score, t]);
172
+ }
173
+ scored.sort((a, b) => b[0] - a[0]);
174
+ return scored.slice(0, limit).map(([, t]) => ({
175
+ slug: t.slug,
176
+ method: t.method,
177
+ path: t.path,
178
+ price: t.price,
179
+ payment: t.computePayable ? "USDC or free via proof-of-work" : "USDC (wallet required)",
180
+ description: t.description.length > 220 ? `${t.description.slice(0, 220)}…` : t.description,
181
+ inputSchema: t.inputSchema,
182
+ }));
183
+ }
184
+
185
+ // ---------------------------------------------------------------------------
186
+ // MCP wiring
187
+ const server = new Server({ name: "agent402", version: VERSION }, { capabilities: { tools: {} } });
188
+
189
+ let curated = [];
190
+ let pricingInfo = null;
191
+
192
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
193
+ const tools = curated.map((t) => ({
194
+ name: t.slug,
195
+ description: `[${t.price}/call${t.computePayable ? ", or free via proof-of-work" : ", wallet required"}] ${t.description}`,
196
+ inputSchema: t.inputSchema,
197
+ }));
198
+ tools.push(
199
+ {
200
+ name: "search_tools",
201
+ description:
202
+ `Search the full Agent402 catalog (${catalog.size} pay-per-call tools: encoding, crypto, data conversion, text, time, validation, math, unit conversions, network, browser, memory). Returns matching tools with their price, payment options, and input schema. Call them with call_tool.`,
203
+ inputSchema: {
204
+ type: "object",
205
+ properties: {
206
+ query: { type: "string", description: "What you need, e.g. \"convert miles to km\", \"decode JWT\", \"cron next run\"" },
207
+ limit: { type: "number", description: "Max results (default 10)" },
208
+ },
209
+ required: ["query"],
210
+ },
211
+ },
212
+ {
213
+ name: "call_tool",
214
+ description:
215
+ "Call any Agent402 tool by slug (find slugs and input schemas with search_tools). Payment is handled automatically: USDC via x402 if this server has a wallet key, otherwise proof-of-work on eligible tools.",
216
+ inputSchema: {
217
+ type: "object",
218
+ properties: {
219
+ slug: { type: "string", description: "Tool slug from search_tools, e.g. \"convert-miles-to-kilometers\"" },
220
+ params: { type: "object", description: "Tool input parameters, matching the tool's inputSchema" },
221
+ },
222
+ required: ["slug"],
223
+ },
224
+ },
225
+ {
226
+ name: "payment_info",
227
+ description: "How this MCP server is paying for Agent402 calls (USDC wallet vs proof-of-work), and what that unlocks.",
228
+ inputSchema: { type: "object", properties: {} },
229
+ }
230
+ );
231
+ return { tools };
232
+ });
233
+
234
+ server.setRequestHandler(CallToolRequestSchema, async (req) => {
235
+ const { name, arguments: args = {} } = req.params;
236
+ try {
237
+ if (name === "search_tools") {
238
+ const results = searchTools(args.query ?? "", args.limit ?? 10);
239
+ return {
240
+ content: [{
241
+ type: "text",
242
+ text: results.length
243
+ ? JSON.stringify({ results, usage: "call_tool {\"slug\": …, \"params\": …}" }, null, 2)
244
+ : `No tools matched "${args.query}". Browse the catalog at ${BASE}/tools or ${BASE}/api/pricing.`,
245
+ }],
246
+ };
247
+ }
248
+ if (name === "payment_info") {
249
+ let address = null;
250
+ if (AGENT_KEY) {
251
+ const { privateKeyToAccount } = await import("viem/accounts");
252
+ address = privateKeyToAccount(AGENT_KEY).address;
253
+ }
254
+ const computePayable = [...catalog.values()].filter((t) => t.computePayable).length;
255
+ return {
256
+ content: [{
257
+ type: "text",
258
+ text: JSON.stringify({
259
+ service: BASE,
260
+ mode: AGENT_KEY ? "usdc" : "proof-of-work",
261
+ wallet: address,
262
+ network: pricingInfo?.payment?.network ?? "base",
263
+ tools: catalog.size,
264
+ payableWithCompute: computePayable,
265
+ walletOnly: catalog.size - computePayable,
266
+ note: AGENT_KEY
267
+ ? "Every tool is available; each call is paid in USDC via x402 from the configured wallet."
268
+ : `No AGENT_KEY configured: ${computePayable} pure-CPU tools are free via proof-of-work; the ${catalog.size - computePayable} network/browser/memory tools need a funded wallet (set AGENT_KEY).`,
269
+ }, null, 2),
270
+ }],
271
+ };
272
+ }
273
+ const tool = catalog.get(name === "call_tool" ? args.slug : name);
274
+ if (!tool) {
275
+ return { content: [{ type: "text", text: `Unknown tool slug "${name === "call_tool" ? args.slug : name}". Use search_tools to find the right slug.` }], isError: true };
276
+ }
277
+ return await callEndpoint(tool, name === "call_tool" ? (args.params ?? {}) : args);
278
+ } catch (err) {
279
+ return { content: [{ type: "text", text: `Agent402 call failed: ${err.message}` }], isError: true };
280
+ }
281
+ });
282
+
283
+ // ---------------------------------------------------------------------------
284
+ try {
285
+ pricingInfo = await loadCatalog();
286
+ } catch (err) {
287
+ log(`Could not load the catalog from ${BASE}: ${err.message}`);
288
+ process.exit(1);
289
+ }
290
+ const requested = (process.env.AGENT402_TOOLS || DEFAULT_CURATED.join(","))
291
+ .split(",").map((s) => s.trim()).filter(Boolean);
292
+ curated = requested.map((slug) => catalog.get(slug)).filter(Boolean);
293
+ log(`catalog: ${catalog.size} tools from ${BASE}; ${curated.length} first-class, rest via search_tools/call_tool`);
294
+ log(AGENT_KEY ? "payment: USDC via x402 (wallet configured)" : "payment: proof-of-work on eligible tools (no AGENT_KEY)");
295
+
296
+ await server.connect(new StdioServerTransport());
package/package.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "agent402-mcp",
3
+ "version": "0.1.0",
4
+ "description": "MCP server for Agent402 (agent402.tools) — 1000+ pay-per-call web tools for AI agents. Pays per call in USDC via the x402 protocol, or with compute (proof-of-work) when no wallet is configured.",
5
+ "type": "module",
6
+ "bin": {
7
+ "agent402-mcp": "index.js"
8
+ },
9
+ "files": [
10
+ "index.js",
11
+ "README.md"
12
+ ],
13
+ "engines": {
14
+ "node": ">=20"
15
+ },
16
+ "keywords": [
17
+ "mcp",
18
+ "model-context-protocol",
19
+ "x402",
20
+ "agents",
21
+ "ai",
22
+ "payments",
23
+ "usdc",
24
+ "tools"
25
+ ],
26
+ "license": "ISC",
27
+ "dependencies": {
28
+ "@modelcontextprotocol/sdk": "^1.12.0",
29
+ "@x402/core": "^2.14.0",
30
+ "@x402/evm": "^2.14.0",
31
+ "@x402/fetch": "^2.14.0",
32
+ "viem": "^2.21.0"
33
+ }
34
+ }