x402-gateway-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/README.md ADDED
@@ -0,0 +1,57 @@
1
+ # x402-gateway-mcp
2
+
3
+ Stdio MCP server that exposes every [x402-gateway](../README.md) data endpoint as an
4
+ MCP tool and **pays for calls with your wallet** (USDC on Base, x402 protocol).
5
+ Tool list is fetched from the gateway's `/.well-known/x402.json` at startup —
6
+ new gateway endpoints appear automatically.
7
+
8
+ > ⚠️ **Funded-wallet warning:** `WALLET_PRIVATE_KEY` signs real payments. Use a
9
+ > dedicated wallet holding only small balances (a few dollars of USDC). Never
10
+ > your main wallet. The key is read from env, never logged, and payment headers
11
+ > are never echoed into agent-visible output.
12
+
13
+ ## 5-minute setup (Claude Desktop / Claude Code)
14
+
15
+ 1. Create a fresh wallet and fund it with a small amount of USDC on Base
16
+ (or Base Sepolia test USDC from <https://faucet.circle.com> while testing).
17
+ 2. `npm install -g x402-gateway-mcp` (or use `npx`).
18
+ 3. Add to your MCP config (Claude Desktop `claude_desktop_config.json`, or
19
+ `claude mcp add` for Claude Code):
20
+
21
+ ```json
22
+ {
23
+ "mcpServers": {
24
+ "x402-gateway": {
25
+ "command": "npx",
26
+ "args": ["-y", "x402-gateway-mcp"],
27
+ "env": {
28
+ "GATEWAY_URL": "https://<your-gateway-host>",
29
+ "WALLET_PRIVATE_KEY": "0x<small-balance-wallet-key>",
30
+ "MAX_PER_CALL_USD": "0.25",
31
+ "MAX_SESSION_USD": "2.00"
32
+ }
33
+ }
34
+ }
35
+ }
36
+ ```
37
+
38
+ 4. Restart the client. Every tool description states its price, e.g.
39
+ `[costs $0.005 USDC per call] Get the current USD price of a cryptocurrency…`
40
+
41
+ ## Spend guardrails
42
+
43
+ - `MAX_PER_CALL_USD` (default **0.25**): tools priced above this are refused.
44
+ - `MAX_SESSION_USD` (default **2.00**): cumulative settled spend per server
45
+ session; calls that would exceed it are refused with a clear message the
46
+ agent can relay. Restart the server to reset.
47
+
48
+ Refusals happen **before** any payment is signed.
49
+
50
+ ## Env reference
51
+
52
+ | Var | Default | Purpose |
53
+ |---|---|---|
54
+ | `GATEWAY_URL` | `https://gateway.stride20k.com` | Gateway base URL (override for local/testnet) |
55
+ | `WALLET_PRIVATE_KEY` | — | Buyer key (small balance!). Without it, tools list but calls fail with a clear error |
56
+ | `MAX_PER_CALL_USD` | `0.25` | Per-call cap |
57
+ | `MAX_SESSION_USD` | `2.00` | Per-session cap |
package/dist/guards.js ADDED
@@ -0,0 +1,19 @@
1
+ /** Spend guardrails — pure so they're trivially testable. */
2
+ export function parsePriceUsd(price) {
3
+ const n = Number(price.replace(/^\$/, ""));
4
+ return Number.isFinite(n) && n >= 0 ? n : null;
5
+ }
6
+ /** Returns null when the call may proceed, or a refusal message the agent can relay. */
7
+ export function checkSpend(priceUsd, state, config) {
8
+ if (priceUsd > config.maxPerCallUsd) {
9
+ return (`Refused: this tool costs $${priceUsd} per call, above the MAX_PER_CALL_USD cap of ` +
10
+ `$${config.maxPerCallUsd}. Raise the cap in the MCP server env if this is intended.`);
11
+ }
12
+ if (state.sessionSpentUsd + priceUsd > config.maxSessionUsd) {
13
+ return (`Refused: paying $${priceUsd} would bring this session's spend to ` +
14
+ `$${(state.sessionSpentUsd + priceUsd).toFixed(3)}, above the MAX_SESSION_USD cap of ` +
15
+ `$${config.maxSessionUsd}. Session spend so far: $${state.sessionSpentUsd.toFixed(3)}. ` +
16
+ `Restart the MCP server or raise the cap to continue.`);
17
+ }
18
+ return null;
19
+ }
package/dist/index.js ADDED
@@ -0,0 +1,143 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * x402-gateway-mcp: stdio MCP server that mirrors the gateway's endpoint
4
+ * registry as tools and pays per call with a user-supplied wallet.
5
+ *
6
+ * On startup it fetches GATEWAY_URL/.well-known/x402.json and registers one
7
+ * tool per endpoint — new gateway endpoints appear with zero code changes here.
8
+ *
9
+ * Env:
10
+ * GATEWAY_URL gateway base URL (default http://localhost:8787)
11
+ * WALLET_PRIVATE_KEY buyer key — SMALL BALANCES ONLY; signs real payments
12
+ * MAX_PER_CALL_USD per-call spend cap (default 0.25)
13
+ * MAX_SESSION_USD session spend cap (default 2.00)
14
+ *
15
+ * The private key is never logged; payment headers are never echoed.
16
+ */
17
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
18
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
19
+ import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
20
+ import { wrapFetchWithPaymentFromConfig, decodePaymentResponseHeader } from "@x402/fetch";
21
+ import { ExactEvmScheme } from "@x402/evm/exact/client";
22
+ import { privateKeyToAccount } from "viem/accounts";
23
+ import { checkSpend, parsePriceUsd } from "./guards.js";
24
+ // Canonical production gateway; override with GATEWAY_URL for local/testnet.
25
+ const GATEWAY_URL = (process.env.GATEWAY_URL ?? "https://gateway.stride20k.com").replace(/\/$/, "");
26
+ const spendConfig = {
27
+ maxPerCallUsd: Number(process.env.MAX_PER_CALL_USD ?? "0.25"),
28
+ maxSessionUsd: Number(process.env.MAX_SESSION_USD ?? "2.00"),
29
+ };
30
+ const spendState = { sessionSpentUsd: 0 };
31
+ /** "/crypto/price/:coinId" -> "crypto_price" */
32
+ function toolName(route) {
33
+ return route
34
+ .split("/")
35
+ .filter((seg) => seg && !seg.startsWith(":"))
36
+ .join("_")
37
+ .replace(/[^a-zA-Z0-9_]/g, "_");
38
+ }
39
+ function buildUrl(endpoint, args) {
40
+ let path = endpoint.route;
41
+ const used = new Set();
42
+ path = path.replace(/:([a-zA-Z_][a-zA-Z0-9_]*)/g, (_, name) => {
43
+ used.add(name);
44
+ const v = args[name];
45
+ if (v === undefined)
46
+ throw new Error(`missing required parameter: ${name}`);
47
+ return encodeURIComponent(String(v));
48
+ });
49
+ const query = new URLSearchParams();
50
+ for (const [k, v] of Object.entries(args)) {
51
+ if (!used.has(k) && v !== undefined && v !== null)
52
+ query.set(k, String(v));
53
+ }
54
+ const qs = query.toString();
55
+ return `${GATEWAY_URL}${path}${qs ? `?${qs}` : ""}`;
56
+ }
57
+ /** Belt-and-braces: strip anything key-shaped from text we return to agents. */
58
+ const redact = (s) => s.replace(/0x[a-fA-F0-9]{64}/g, "0x[REDACTED]");
59
+ async function main() {
60
+ // Manifest fetch (free route) — tool list derives entirely from it.
61
+ const manifestRes = await fetch(`${GATEWAY_URL}/.well-known/x402.json`);
62
+ if (!manifestRes.ok) {
63
+ console.error(`Failed to fetch gateway manifest from ${GATEWAY_URL}: HTTP ${manifestRes.status}`);
64
+ process.exit(1);
65
+ }
66
+ const manifest = (await manifestRes.json());
67
+ const byTool = new Map(manifest.endpoints.map((e) => [toolName(e.route), e]));
68
+ // Paying fetch is built lazily so listing tools works without a wallet.
69
+ let payingFetch = null;
70
+ const getPayingFetch = () => {
71
+ const key = process.env.WALLET_PRIVATE_KEY;
72
+ if (!key) {
73
+ throw new Error("WALLET_PRIVATE_KEY is not configured — cannot pay for calls. Set it in the MCP server env (testnet/small balance only).");
74
+ }
75
+ if (!payingFetch) {
76
+ const account = privateKeyToAccount(key);
77
+ payingFetch = wrapFetchWithPaymentFromConfig(fetch, {
78
+ schemes: [{ network: "eip155:*", client: new ExactEvmScheme(account) }],
79
+ });
80
+ }
81
+ return payingFetch;
82
+ };
83
+ const server = new Server({ name: "x402-gateway-mcp", version: "0.1.0" }, { capabilities: { tools: {} } });
84
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
85
+ tools: manifest.endpoints.map((e) => ({
86
+ name: toolName(e.route),
87
+ description: `[costs ${e.price} USDC per call] ${e.summary} ${e.description}`,
88
+ inputSchema: e.inputSchema,
89
+ })),
90
+ }));
91
+ server.setRequestHandler(CallToolRequestSchema, async (req) => {
92
+ const endpoint = byTool.get(req.params.name);
93
+ const fail = (text) => ({ content: [{ type: "text", text: redact(text) }], isError: true });
94
+ if (!endpoint)
95
+ return fail(`Unknown tool: ${req.params.name}`);
96
+ const priceUsd = parsePriceUsd(endpoint.price);
97
+ if (priceUsd === null)
98
+ return fail(`Gateway advertised an unparseable price: ${endpoint.price}`);
99
+ const refusal = checkSpend(priceUsd, spendState, spendConfig);
100
+ if (refusal)
101
+ return fail(refusal);
102
+ let url;
103
+ try {
104
+ url = buildUrl(endpoint, (req.params.arguments ?? {}));
105
+ }
106
+ catch (err) {
107
+ return fail(String(err instanceof Error ? err.message : err));
108
+ }
109
+ try {
110
+ const res = await getPayingFetch()(url, { method: "GET" });
111
+ const body = await res.text();
112
+ let paid = false;
113
+ const settleHeader = res.headers.get("PAYMENT-RESPONSE");
114
+ if (settleHeader) {
115
+ try {
116
+ const settle = decodePaymentResponseHeader(settleHeader);
117
+ paid = settle.success !== false;
118
+ }
119
+ catch {
120
+ paid = res.status === 200; // settled header unparseable; count conservatively
121
+ }
122
+ }
123
+ if (paid)
124
+ spendState.sessionSpentUsd += priceUsd;
125
+ const note = paid
126
+ ? `\n\n[paid ${endpoint.price}; session spend $${spendState.sessionSpentUsd.toFixed(3)} of $${spendConfig.maxSessionUsd}]`
127
+ : "";
128
+ return {
129
+ content: [{ type: "text", text: redact(body) + note }],
130
+ isError: res.status !== 200,
131
+ };
132
+ }
133
+ catch (err) {
134
+ return fail(`Call failed: ${err instanceof Error ? err.message : String(err)}`);
135
+ }
136
+ });
137
+ await server.connect(new StdioServerTransport());
138
+ console.error(`x402-gateway-mcp ready: ${byTool.size} tools from ${GATEWAY_URL} (caps: $${spendConfig.maxPerCallUsd}/call, $${spendConfig.maxSessionUsd}/session)`);
139
+ }
140
+ main().catch((err) => {
141
+ console.error("x402-gateway-mcp failed to start:", err instanceof Error ? err.message : err);
142
+ process.exit(1);
143
+ });
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "x402-gateway-mcp",
3
+ "version": "0.1.0",
4
+ "description": "MCP server exposing every x402-gateway data endpoint as a tool, paying per call with a user-supplied wallet (USDC on Base via x402). Built-in per-call and per-session spend caps. Tools: crypto prices, DeFi TVL, on-chain DEX pools, gas, FX rates, US Treasury yields, US weather, WHOIS, DNS, email validation, web-to-markdown extraction, Hacker News, Wikipedia, npm/PyPI package health, token risk scoring.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "homepage": "https://gateway.stride20k.com",
8
+ "keywords": [
9
+ "mcp",
10
+ "mcp-server",
11
+ "model-context-protocol",
12
+ "x402",
13
+ "micropayments",
14
+ "usdc",
15
+ "base",
16
+ "agent-tools",
17
+ "data-api",
18
+ "crypto-prices",
19
+ "web-scraping",
20
+ "token-risk"
21
+ ],
22
+ "engines": {
23
+ "node": ">=18"
24
+ },
25
+ "bin": {
26
+ "x402-gateway-mcp": "dist/index.js"
27
+ },
28
+ "files": [
29
+ "dist",
30
+ "README.md"
31
+ ],
32
+ "scripts": {
33
+ "build": "tsc -p tsconfig.json",
34
+ "prepublishOnly": "npm run build"
35
+ },
36
+ "dependencies": {
37
+ "@modelcontextprotocol/sdk": "1.29.0",
38
+ "@x402/evm": "2.17.0",
39
+ "@x402/fetch": "2.17.0",
40
+ "viem": "2.54.2"
41
+ },
42
+ "devDependencies": {
43
+ "@types/node": "24.10.9",
44
+ "typescript": "5.9.3"
45
+ }
46
+ }