x402node-mcp 0.1.1 → 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 +40 -41
- package/index.mjs +120 -0
- package/package.json +9 -48
- package/.env.example +0 -4
- package/LICENSE +0 -21
- package/mcp-server.js +0 -146
- package/test-client.mjs +0 -21
package/README.md
CHANGED
|
@@ -1,63 +1,62 @@
|
|
|
1
|
-
|
|
1
|
+
# x402node-mcp
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
10
|
+
## Install (local)
|
|
8
11
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
## Install (Claude Desktop)
|
|
12
|
+
```bash
|
|
13
|
+
git clone <this folder> # or copy it
|
|
14
|
+
cd x402node-mcp
|
|
15
|
+
npm install
|
|
16
|
+
```
|
|
16
17
|
|
|
17
|
-
|
|
18
|
+
Then add to your MCP client config. **Claude Desktop** (`claude_desktop_config.json`):
|
|
18
19
|
|
|
19
20
|
```json
|
|
20
21
|
{
|
|
21
22
|
"mcpServers": {
|
|
22
|
-
"
|
|
23
|
-
"command": "
|
|
24
|
-
"args": ["
|
|
25
|
-
"env": {
|
|
23
|
+
"x402node": {
|
|
24
|
+
"command": "node",
|
|
25
|
+
"args": ["/absolute/path/to/x402node-mcp/index.mjs"],
|
|
26
|
+
"env": {
|
|
27
|
+
"PRIVATE_KEY": "0xYOUR_BASE_WALLET_KEY",
|
|
28
|
+
"MAX_USDC": "1.00"
|
|
29
|
+
}
|
|
26
30
|
}
|
|
27
31
|
}
|
|
28
32
|
}
|
|
29
33
|
```
|
|
30
34
|
|
|
31
|
-
|
|
35
|
+
## Test it before wiring into a client
|
|
32
36
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
- Hard cap: `MAX_PRICE_USD` env var (default $0.10/call); calls above are blocked
|
|
37
|
-
- Use a fresh burner wallet, not your main wallet
|
|
38
|
-
|
|
39
|
-
## How it works
|
|
37
|
+
```bash
|
|
38
|
+
PRIVATE_KEY=0x... MAX_USDC=0.50 npx @modelcontextprotocol/inspector node index.mjs
|
|
39
|
+
```
|
|
40
40
|
|
|
41
|
-
|
|
41
|
+
The Inspector gives you a UI to call `x402_list_endpoints` and `x402_call` and watch real payments settle.
|
|
42
42
|
|
|
43
|
-
##
|
|
43
|
+
## Environment
|
|
44
44
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
node mcp-server.js
|
|
51
|
-
```
|
|
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. |
|
|
52
50
|
|
|
53
|
-
##
|
|
51
|
+
## Money / security
|
|
54
52
|
|
|
55
|
-
-
|
|
56
|
-
-
|
|
57
|
-
- MCP
|
|
58
|
-
- npm: https://www.npmjs.com/package/x402node-mcp
|
|
59
|
-
- Glama: https://glama.ai/mcp/servers/x402node/x402-mcp
|
|
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.
|
|
60
56
|
|
|
61
|
-
##
|
|
57
|
+
## Publish (optional, for `npx x402node-mcp`)
|
|
62
58
|
|
|
63
|
-
|
|
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,55 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "x402node-mcp",
|
|
3
|
-
"
|
|
4
|
-
"
|
|
5
|
-
"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.",
|
|
6
5
|
"type": "module",
|
|
7
|
-
"
|
|
8
|
-
"
|
|
9
|
-
|
|
10
|
-
},
|
|
11
|
-
"files": [
|
|
12
|
-
"mcp-server.js",
|
|
13
|
-
"test-client.mjs",
|
|
14
|
-
".env.example",
|
|
15
|
-
"README.md",
|
|
16
|
-
"LICENSE"
|
|
17
|
-
],
|
|
18
|
-
"scripts": {
|
|
19
|
-
"start": "node mcp-server.js",
|
|
20
|
-
"test": "node test-client.mjs"
|
|
21
|
-
},
|
|
22
|
-
"keywords": [
|
|
23
|
-
"mcp",
|
|
24
|
-
"model-context-protocol",
|
|
25
|
-
"x402",
|
|
26
|
-
"usdc",
|
|
27
|
-
"base",
|
|
28
|
-
"coinbase",
|
|
29
|
-
"payment",
|
|
30
|
-
"agent",
|
|
31
|
-
"claude",
|
|
32
|
-
"anthropic",
|
|
33
|
-
"ai-agent",
|
|
34
|
-
"stablecoin"
|
|
35
|
-
],
|
|
36
|
-
"author": "CKING LLC",
|
|
6
|
+
"bin": { "x402node-mcp": "index.mjs" },
|
|
7
|
+
"files": ["index.mjs", "README.md"],
|
|
8
|
+
"engines": { "node": ">=18" },
|
|
37
9
|
"license": "MIT",
|
|
38
|
-
"homepage": "https://github.com/x402node/x402-mcp#readme",
|
|
39
|
-
"repository": {
|
|
40
|
-
"type": "git",
|
|
41
|
-
"url": "git+https://github.com/x402node/x402-mcp.git"
|
|
42
|
-
},
|
|
43
|
-
"bugs": {
|
|
44
|
-
"url": "https://github.com/x402node/x402-mcp/issues"
|
|
45
|
-
},
|
|
46
|
-
"engines": {
|
|
47
|
-
"node": ">=18"
|
|
48
|
-
},
|
|
49
10
|
"dependencies": {
|
|
50
|
-
"@modelcontextprotocol/sdk": "^1.
|
|
51
|
-
"
|
|
52
|
-
"viem": "^2.
|
|
53
|
-
"
|
|
11
|
+
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
12
|
+
"x402-fetch": "^1.1.0",
|
|
13
|
+
"viem": "^2.21.0",
|
|
14
|
+
"zod": "^3.25.0"
|
|
54
15
|
}
|
|
55
16
|
}
|
package/.env.example
DELETED
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);
|