x402node-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.
- package/.env.example +4 -0
- package/LICENSE +21 -0
- package/README.md +52 -0
- package/mcp-server.js +146 -0
- package/package.json +54 -0
- package/test-client.mjs +21 -0
package/.env.example
ADDED
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
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/README.md
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# x402-mcp
|
|
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.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
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
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
git clone https://github.com/x402node/x402-mcp
|
|
17
|
+
cd x402-mcp
|
|
18
|
+
npm install
|
|
19
|
+
cp .env.example .env
|
|
20
|
+
```
|
|
21
|
+
|
|
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`:
|
|
27
|
+
|
|
28
|
+
```json
|
|
29
|
+
{
|
|
30
|
+
"mcpServers": {
|
|
31
|
+
"x402-mcp": {
|
|
32
|
+
"command": "node",
|
|
33
|
+
"args": ["/absolute/path/to/x402-mcp/mcp-server.js"],
|
|
34
|
+
"env": { "X402_PRIVATE_KEY": "0xYOUR_KEY" }
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## How it works
|
|
41
|
+
|
|
42
|
+
Agent calls tool → HTTP 402 → x402-fetch signs EIP-3009 → CDP Facilitator settles on Base → response returned. Buyer pays no gas.
|
|
43
|
+
|
|
44
|
+
## Links
|
|
45
|
+
|
|
46
|
+
- x402 protocol: https://x402.org
|
|
47
|
+
- CDP Bazaar: https://docs.cdp.coinbase.com/x402/bazaar
|
|
48
|
+
- MCP: https://modelcontextprotocol.io
|
|
49
|
+
|
|
50
|
+
## License
|
|
51
|
+
|
|
52
|
+
MIT
|
package/mcp-server.js
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
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/package.json
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
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.",
|
|
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",
|
|
36
|
+
"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
|
+
"dependencies": {
|
|
49
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
50
|
+
"dotenv": "^17.4.2",
|
|
51
|
+
"viem": "^2.51.0",
|
|
52
|
+
"x402-fetch": "^1.2.0"
|
|
53
|
+
}
|
|
54
|
+
}
|
package/test-client.mjs
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
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);
|