x402node-mcp 0.1.0 → 1.0.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.
package/README.md CHANGED
@@ -1,52 +1,62 @@
1
- # x402-mcp
1
+ # x402node-mcp
2
2
 
3
- MCP server bringing 100+ x402-paid APIs to AI agents (Claude, Cursor, MCP-aware clients). Auto-discovers tools from CDP Bazaar; handles USDC micropayments on Base.
3
+ An MCP server that drops the entire **cn402.com + x402node.dev** paid-API catalog
4
+ into any MCP client (Claude Desktop, etc.) as two tools. Your agent discovers and
5
+ calls 230+ endpoints; payment is handled automatically over **x402 (USDC on Base)**.
4
6
 
5
- ## Features
7
+ - `x402_list_endpoints` — browse the catalog (path, price, category, description), with optional `category` / `query` filters.
8
+ - `x402_call` — call any endpoint by path; auto-pays, capped at `MAX_USDC` per call.
6
9
 
7
- - Auto-discovers x402 endpoints from CDP Bazaar (no hard-coded list)
8
- - Reads metadata directly from Bazaar (description, schema, pricing)
9
- - Refreshes every 5 min — new endpoints appear without restart
10
- - Handles HTTP 402 + USDC payment automatically
11
- - Multi-chain ready via x402 protocol (Base today; Solana, Polygon, BNB, EVM expansion)
12
-
13
- ## Install
10
+ ## Install (local)
14
11
 
15
12
  ```bash
16
- git clone https://github.com/x402node/x402-mcp
17
- cd x402-mcp
13
+ git clone <this folder> # or copy it
14
+ cd x402node-mcp
18
15
  npm install
19
- cp .env.example .env
20
16
  ```
21
17
 
22
- Edit `.env`: set `X402_PRIVATE_KEY` to a Base EOA with USDC.
23
-
24
- ## Use with Claude Desktop
25
-
26
- Add to `claude_desktop_config.json`:
18
+ Then add to your MCP client config. **Claude Desktop** (`claude_desktop_config.json`):
27
19
 
28
20
  ```json
29
21
  {
30
22
  "mcpServers": {
31
- "x402-mcp": {
23
+ "x402node": {
32
24
  "command": "node",
33
- "args": ["/absolute/path/to/x402-mcp/mcp-server.js"],
34
- "env": { "X402_PRIVATE_KEY": "0xYOUR_KEY" }
25
+ "args": ["/absolute/path/to/x402node-mcp/index.mjs"],
26
+ "env": {
27
+ "PRIVATE_KEY": "0xYOUR_BASE_WALLET_KEY",
28
+ "MAX_USDC": "1.00"
29
+ }
35
30
  }
36
31
  }
37
32
  }
38
33
  ```
39
34
 
40
- ## How it works
35
+ ## Test it before wiring into a client
36
+
37
+ ```bash
38
+ PRIVATE_KEY=0x... MAX_USDC=0.50 npx @modelcontextprotocol/inspector node index.mjs
39
+ ```
40
+
41
+ The Inspector gives you a UI to call `x402_list_endpoints` and `x402_call` and watch real payments settle.
42
+
43
+ ## Environment
41
44
 
42
- Agent calls tool HTTP 402 x402-fetch signs EIP-3009 → CDP Facilitator settles on Base → response returned. Buyer pays no gas.
45
+ | var | required | default | meaning |
46
+ |---|---|---|---|
47
+ | `PRIVATE_KEY` | yes | — | Base wallet private key (`0x` + 64 hex) holding USDC. **Spends real money.** |
48
+ | `MAX_USDC` | no | `1.00` | Hard per-call spend cap (USDC). A call costing more than this is rejected. |
49
+ | `MANIFESTS` | no | cn402 + x402node | Comma-separated manifest URLs to load the catalog from. |
43
50
 
44
- ## Links
51
+ ## Money / security
45
52
 
46
- - x402 protocol: https://x402.org
47
- - CDP Bazaar: https://docs.cdp.coinbase.com/x402/bazaar
48
- - MCP: https://modelcontextprotocol.io
53
+ - The key spends **real USDC on Base** on every `x402_call`. Use a **dedicated low-balance wallet**, not your main one.
54
+ - `MAX_USDC` is your safety cap — set it to the most you want any single call to spend.
55
+ - The key lives only in your local MCP client config and is **never sent anywhere** except to sign x402 payments. It never touches cn402/x402node servers.
49
56
 
50
- ## License
57
+ ## Publish (optional, for `npx x402node-mcp`)
51
58
 
52
- MIT
59
+ ```bash
60
+ npm publish # then anyone can run: npx -y x402node-mcp
61
+ ```
62
+ After publishing, the Claude Desktop config can use `"command": "npx", "args": ["-y","x402node-mcp"]`.
package/index.mjs ADDED
@@ -0,0 +1,120 @@
1
+ #!/usr/bin/env node
2
+ // x402node-mcp — exposes the cn402.com + x402node.dev paid x402 API catalog
3
+ // to any MCP client (Claude Desktop, etc.) as two tools, paying with USDC on Base.
4
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
5
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
6
+ import { z } from "zod";
7
+ import { wrapFetchWithPayment } from "x402-fetch";
8
+ import { createWalletClient, http } from "viem";
9
+ import { privateKeyToAccount } from "viem/accounts";
10
+ import { base } from "viem/chains";
11
+
12
+ // ---- config / env ----
13
+ const PK = process.env.PRIVATE_KEY || "";
14
+ if (!/^0x[0-9a-fA-F]{64}$/.test(PK)) {
15
+ console.error("[x402node-mcp] FATAL: set PRIVATE_KEY to a Base wallet private key (0x + 64 hex) holding USDC.");
16
+ process.exit(1);
17
+ }
18
+ const MAX_USDC = parseFloat(process.env.MAX_USDC || "1.00"); // hard per-call spend cap
19
+ const MAX_ATOMIC = BigInt(Math.round(MAX_USDC * 1e6)); // USDC has 6 decimals
20
+ const MANIFEST_URLS = (process.env.MANIFESTS ||
21
+ "https://api.cn402.com/.well-known/x402-manifest.json,https://api.x402node.dev/.well-known/x402-manifest.json"
22
+ ).split(",").map(s => s.trim()).filter(Boolean);
23
+
24
+ const account = privateKeyToAccount(PK);
25
+ const wallet = createWalletClient({ account, chain: base, transport: http() });
26
+ const pay = wrapFetchWithPayment(fetch, wallet, MAX_ATOMIC);
27
+
28
+ // ---- build catalog from the live manifests ----
29
+ const catalog = []; // {host, path, method, price, description, category}
30
+ async function loadCatalog() {
31
+ for (const url of MANIFEST_URLS) {
32
+ try {
33
+ const host = new URL(url).host;
34
+ const r = await fetch(url, { signal: AbortSignal.timeout(8000) });
35
+ if (!r.ok) { console.error(`[x402node-mcp] ${url} -> HTTP ${r.status}`); continue; }
36
+ const m = await r.json();
37
+ const catByPath = {};
38
+ for (const s of (m.subjects || [])) for (const p of (s.paths || [])) catByPath[p] = s.name;
39
+ for (const e of (m.endpoints || [])) {
40
+ catalog.push({
41
+ host,
42
+ path: e.path,
43
+ method: (e.method || "GET").toUpperCase(),
44
+ price: e.price || "",
45
+ description: e.description || "",
46
+ category: catByPath[e.path] || "",
47
+ });
48
+ }
49
+ console.error(`[x402node-mcp] loaded ${(m.endpoints || []).length} endpoints from ${host}`);
50
+ } catch (err) {
51
+ console.error(`[x402node-mcp] failed to load ${url}: ${err && err.message}`);
52
+ }
53
+ }
54
+ console.error(`[x402node-mcp] catalog ready: ${catalog.length} endpoints, per-call cap $${MAX_USDC}`);
55
+ }
56
+
57
+ // ---- MCP server ----
58
+ const server = new McpServer({ name: "x402node", version: "1.0.0" });
59
+
60
+ server.registerTool(
61
+ "x402_list_endpoints",
62
+ {
63
+ title: "List x402 endpoints",
64
+ description:
65
+ "Discover the paid API endpoints available from cn402.com (Chinese almanac / fortune / I-Ching / TCM / fengshui) and x402node.dev (developer & data tools: on-chain data, crypto, parsing, conversion, text, generation). Returns path, price in USDC, category, host and description. Call this first, then call x402_call with a chosen path. Use the category or query filter to narrow results.",
66
+ inputSchema: {
67
+ category: z.string().optional().describe("Filter by category name (case-insensitive substring)"),
68
+ query: z.string().optional().describe("Free-text filter over path, description and category"),
69
+ },
70
+ },
71
+ async ({ category, query }) => {
72
+ let rows = catalog;
73
+ if (category) { const c = category.toLowerCase(); rows = rows.filter(e => e.category.toLowerCase().includes(c)); }
74
+ if (query) { const q = query.toLowerCase(); rows = rows.filter(e => (e.path + " " + e.description + " " + e.category).toLowerCase().includes(q)); }
75
+ const endpoints = rows.map(e => ({ path: e.path, method: e.method, price: e.price, category: e.category, host: e.host, description: e.description }));
76
+ return { content: [{ type: "text", text: JSON.stringify({ count: endpoints.length, endpoints }, null, 2) }] };
77
+ }
78
+ );
79
+
80
+ server.registerTool(
81
+ "x402_call",
82
+ {
83
+ title: "Call a paid x402 endpoint",
84
+ description:
85
+ "Call one of the paid endpoints (find valid paths via x402_list_endpoints). Payment is automatic: USDC on Base, signed with the configured wallet, capped at MAX_USDC per call. Pass the path exactly as listed (e.g. /chain/tx-decode). Put GET parameters in `query`; put POST data in `body`.",
86
+ inputSchema: {
87
+ path: z.string().describe("Endpoint path exactly as listed, e.g. /chain/tx-decode or /almanac"),
88
+ method: z.enum(["GET", "POST"]).optional().describe("Override HTTP method (defaults to the catalog method, usually GET)"),
89
+ query: z.record(z.string()).optional().describe("Query-string parameters as key/value pairs"),
90
+ body: z.any().optional().describe("JSON body for POST endpoints"),
91
+ },
92
+ },
93
+ async ({ path, method, query, body }) => {
94
+ const hits = catalog.filter(e => e.path === path);
95
+ if (hits.length === 0) {
96
+ return { content: [{ type: "text", text: `Unknown path "${path}". Call x402_list_endpoints to see valid paths.` }], isError: true };
97
+ }
98
+ const e = hits[0];
99
+ const m = (method || e.method || "GET").toUpperCase();
100
+ let url = `https://${e.host}${path}`;
101
+ if (query && Object.keys(query).length) {
102
+ url += (url.includes("?") ? "&" : "?") + new URLSearchParams(query).toString();
103
+ }
104
+ const opts = { method: m };
105
+ if (m === "POST") { opts.headers = { "content-type": "application/json" }; opts.body = JSON.stringify(body ?? {}); }
106
+ try {
107
+ const res = await pay(url, opts);
108
+ const text = await res.text();
109
+ let out; try { out = JSON.parse(text); } catch { out = text; }
110
+ return { content: [{ type: "text", text: typeof out === "string" ? out : JSON.stringify(out, null, 2) }] };
111
+ } catch (err) {
112
+ const msg = (err && err.message) || String(err);
113
+ return { content: [{ type: "text", text: `Request/payment failed for ${m} ${url}: ${msg}. Endpoint price is ${e.price}; if this was a spend-cap error, raise MAX_USDC.` }], isError: true };
114
+ }
115
+ }
116
+ );
117
+
118
+ await loadCatalog();
119
+ await server.connect(new StdioServerTransport());
120
+ console.error("[x402node-mcp] ready on stdio");
package/package.json CHANGED
@@ -1,54 +1,16 @@
1
1
  {
2
2
  "name": "x402node-mcp",
3
- "version": "0.1.0",
4
- "description": "MCP server that exposes x402 paid APIs as Model Context Protocol tools. Auto-discovers endpoints from the Coinbase Bazaar registry and handles USDC payments on Base mainnet transparently.",
3
+ "version": "1.0.0",
4
+ "description": "MCP server exposing the cn402.com + x402node.dev x402 paid-API catalog (Chinese almanac/fortune + developer & data tools) to AI agents, with automatic USDC-on-Base payment.",
5
5
  "type": "module",
6
- "main": "mcp-server.js",
7
- "bin": {
8
- "x402node-mcp": "./mcp-server.js"
9
- },
10
- "files": [
11
- "mcp-server.js",
12
- "test-client.mjs",
13
- ".env.example",
14
- "README.md",
15
- "LICENSE"
16
- ],
17
- "scripts": {
18
- "start": "node mcp-server.js",
19
- "test": "node test-client.mjs"
20
- },
21
- "keywords": [
22
- "mcp",
23
- "model-context-protocol",
24
- "x402",
25
- "usdc",
26
- "base",
27
- "coinbase",
28
- "payment",
29
- "agent",
30
- "claude",
31
- "anthropic",
32
- "ai-agent",
33
- "stablecoin"
34
- ],
35
- "author": "CKING LLC",
6
+ "bin": { "x402node-mcp": "index.mjs" },
7
+ "files": ["index.mjs", "README.md"],
8
+ "engines": { "node": ">=18" },
36
9
  "license": "MIT",
37
- "homepage": "https://github.com/x402node/x402-mcp#readme",
38
- "repository": {
39
- "type": "git",
40
- "url": "git+https://github.com/x402node/x402-mcp.git"
41
- },
42
- "bugs": {
43
- "url": "https://github.com/x402node/x402-mcp/issues"
44
- },
45
- "engines": {
46
- "node": ">=18"
47
- },
48
10
  "dependencies": {
49
- "@modelcontextprotocol/sdk": "^1.29.0",
50
- "dotenv": "^17.4.2",
51
- "viem": "^2.51.0",
52
- "x402-fetch": "^1.2.0"
11
+ "@modelcontextprotocol/sdk": "^1.0.0",
12
+ "x402-fetch": "^1.1.0",
13
+ "viem": "^2.21.0",
14
+ "zod": "^3.25.0"
53
15
  }
54
16
  }
package/.env.example DELETED
@@ -1,4 +0,0 @@
1
- X402_PRIVATE_KEY=0x_replace_with_your_64hex_private_key
2
- BASE_RPC_URL=https://mainnet.base.org
3
- X402_MAX_PRICE_USD=0.5
4
- X402_REFRESH_MIN=5
package/LICENSE DELETED
@@ -1,21 +0,0 @@
1
- MIT License
2
-
3
- Copyright (c) 2026 x402node
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.
package/mcp-server.js DELETED
@@ -1,146 +0,0 @@
1
- #!/usr/bin/env node
2
- import "dotenv/config";
3
- import { Server } from "@modelcontextprotocol/sdk/server/index.js";
4
- import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
5
- import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
6
- import { privateKeyToAccount } from "viem/accounts";
7
- import { createWalletClient, http } from "viem";
8
- import { base } from "viem/chains";
9
- import { wrapFetchWithPayment } from "x402-fetch";
10
-
11
- const PAY_TO = process.env.X402_PAY_TO || "0x4466d4A84b7c49a6A094ec6eef4a0712D6dd125e";
12
- const PRIVATE_KEY = process.env.X402_PRIVATE_KEY;
13
- const RPC_URL = process.env.BASE_RPC_URL || "https://mainnet.base.org";
14
- const MAX_PRICE_USD = parseFloat(process.env.X402_MAX_PRICE_USD || "0.5");
15
- const REFRESH_MIN = parseInt(process.env.X402_REFRESH_MIN || "5");
16
-
17
- if (!PRIVATE_KEY) {
18
- console.error("ERROR: Set X402_PRIVATE_KEY in environment");
19
- process.exit(1);
20
- }
21
-
22
- const DISCOVERY = `https://api.cdp.coinbase.com/platform/v2/x402/discovery/merchant?payTo=${PAY_TO}`;
23
-
24
- const account = privateKeyToAccount(PRIVATE_KEY);
25
- const wallet = createWalletClient({ account, chain: base, transport: http(RPC_URL) });
26
- const payFetch = wrapFetchWithPayment(fetch, wallet, BigInt(Math.floor(MAX_PRICE_USD * 1e6)));
27
-
28
- async function loadResources() {
29
- const all = [];
30
- let offset = 0;
31
- const limit = 100;
32
- while (true) {
33
- const r = await fetch(`${DISCOVERY}&limit=${limit}&offset=${offset}`);
34
- if (!r.ok) throw new Error(`Bazaar HTTP ${r.status}`);
35
- const j = await r.json();
36
- const items = j.resources || [];
37
- all.push(...items);
38
- const total = j.pagination?.total || items.length;
39
- if (offset + items.length >= total || items.length === 0) break;
40
- offset += items.length;
41
- }
42
- return all;
43
- }
44
-
45
- function pathToToolName(resourceUrl) {
46
- const u = new URL(resourceUrl);
47
- const site = u.hostname.replace(/^api\./, "").replace(/\.(dev|com|net|io)$/, "").replace(/[^a-z0-9]/gi, "");
48
- const path = u.pathname.replace(/^\/+/, "").replace(/\//g, "_");
49
- return `${site}_${path}`.replace(/[^a-zA-Z0-9_-]/g, "_").slice(0, 60);
50
- }
51
-
52
- function resourceToTool(resource) {
53
- const accept = (resource.accepts && resource.accepts[0]) || {};
54
- const bazaar = (accept.extensions && accept.extensions.bazaar && accept.extensions.bazaar.info) || {};
55
- const input = bazaar.input || {};
56
- const queryParams = input.queryParams || {};
57
- const method = (input.method || "GET").toUpperCase();
58
- const priceUsd = accept.amount ? parseInt(accept.amount) / 1e6 : null;
59
- const priceStr = priceUsd !== null ? `$${priceUsd.toFixed(4)} USDC` : "unknown";
60
-
61
- const props = {};
62
- const required = [];
63
- for (const [k, desc] of Object.entries(queryParams)) {
64
- props[k] = { type: "string", description: String(desc) };
65
- if (/required/i.test(String(desc))) required.push(k);
66
- }
67
-
68
- const name = pathToToolName(resource.resource);
69
- const desc = (accept.description || resource.resource).slice(0, 500);
70
-
71
- return {
72
- name,
73
- description: `${desc}\n\nPrice: ${priceStr} on Base (auto-paid in USDC).`,
74
- inputSchema: { type: "object", properties: props, required },
75
- _resource: resource.resource,
76
- _method: method,
77
- _priceUsd: priceUsd,
78
- };
79
- }
80
-
81
- const server = new Server({ name: "x402-cn402-x402node", version: "0.1.0" }, { capabilities: { tools: {} } });
82
-
83
- let TOOLS = [];
84
- let TOOL_MAP = {};
85
-
86
- async function refreshTools() {
87
- try {
88
- const resources = await loadResources();
89
- const tools = resources.map(resourceToTool);
90
- const seen = new Set();
91
- TOOLS = tools.filter(t => {
92
- if (seen.has(t.name)) return false;
93
- seen.add(t.name);
94
- return true;
95
- });
96
- TOOL_MAP = Object.fromEntries(TOOLS.map(t => [t.name, t]));
97
- console.error(`[mcp] loaded ${TOOLS.length} tools from Bazaar (payTo=${PAY_TO})`);
98
- } catch (e) {
99
- console.error(`[mcp] refresh failed: ${e.message}`);
100
- }
101
- }
102
-
103
- server.setRequestHandler(ListToolsRequestSchema, async () => ({
104
- tools: TOOLS.map(t => ({ name: t.name, description: t.description, inputSchema: t.inputSchema })),
105
- }));
106
-
107
- server.setRequestHandler(CallToolRequestSchema, async (req) => {
108
- const tool = TOOL_MAP[req.params.name];
109
- if (!tool) {
110
- return { content: [{ type: "text", text: `Unknown tool: ${req.params.name}` }], isError: true };
111
- }
112
- if (tool._priceUsd !== null && tool._priceUsd > MAX_PRICE_USD) {
113
- return { content: [{ type: "text", text: `Tool costs $${tool._priceUsd} > max $${MAX_PRICE_USD}` }], isError: true };
114
- }
115
- const url = new URL(tool._resource);
116
- const args = req.params.arguments || {};
117
- try {
118
- let resp;
119
- if (tool._method === "GET") {
120
- for (const [k, v] of Object.entries(args)) {
121
- if (v !== undefined && v !== null) url.searchParams.set(k, String(v));
122
- }
123
- resp = await payFetch(url.toString());
124
- } else {
125
- resp = await payFetch(url.toString(), {
126
- method: tool._method,
127
- headers: { "Content-Type": "application/json" },
128
- body: JSON.stringify(args),
129
- });
130
- }
131
- const text = await resp.text();
132
- if (resp.status !== 200) {
133
- return { content: [{ type: "text", text: `HTTP ${resp.status}: ${text.slice(0, 800)}` }], isError: true };
134
- }
135
- return { content: [{ type: "text", text }] };
136
- } catch (e) {
137
- return { content: [{ type: "text", text: `x402 call failed: ${e.message}` }], isError: true };
138
- }
139
- });
140
-
141
- await refreshTools();
142
- setInterval(refreshTools, REFRESH_MIN * 60 * 1000);
143
-
144
- const transport = new StdioServerTransport();
145
- await server.connect(transport);
146
- console.error("[mcp] x402-cn402-x402node MCP server ready on stdio");
package/test-client.mjs DELETED
@@ -1,21 +0,0 @@
1
- import { Client } from "@modelcontextprotocol/sdk/client/index.js";
2
- import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
3
-
4
- const transport = new StdioClientTransport({
5
- command: "node",
6
- args: ["mcp-server.js"],
7
- });
8
-
9
- const client = new Client({ name: "test", version: "1.0.0" }, { capabilities: {} });
10
- await client.connect(transport);
11
- console.log("Connected.");
12
-
13
- const tools = await client.listTools();
14
- console.log("Tool count:", tools.tools.length);
15
-
16
- console.log("\nCalling x402node_dev_uuid...");
17
- const result = await client.callTool({ name: "x402node_dev_uuid", arguments: {} });
18
- console.log("Result:", JSON.stringify(result, null, 2));
19
-
20
- await client.close();
21
- process.exit(0);