x402scan-mcp 0.0.1
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 +105 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +968 -0
- package/package.json +54 -0
package/README.md
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# x402scan-mcp
|
|
2
|
+
|
|
3
|
+
MCP server for calling [x402](https://x402.org)-protected APIs with automatic payment handling.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
### Claude Code
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
claude mcp add x402scan -- npx -y x402scan-mcp@latest
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
### Codex
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
codex mcp add x402scan -- npx -y x402scan-mcp@latest
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
### Cursor
|
|
20
|
+
|
|
21
|
+
[](https://cursor.com/en/install-mcp?name=x402scan&config=eyJjb21tYW5kIjoiL2Jpbi9iYXNoIiwiYXJncyI6WyItYyIsInNvdXJjZSAkSE9NRS8ubnZtL252bS5zaCAyPi9kZXYvbnVsbDsgZXhlYyBucHggLXkgeDQwMnNjYW4tbWNwQGxhdGVzdCJdfQ%3D%3D)
|
|
22
|
+
|
|
23
|
+
### Claude Desktop
|
|
24
|
+
|
|
25
|
+
[](https://github.com/merit-systems/x402scan-mcp/raw/main/x402scan.mcpb)
|
|
26
|
+
|
|
27
|
+
<details>
|
|
28
|
+
<summary>Manual installation</summary>
|
|
29
|
+
|
|
30
|
+
**Codex** - Add to `~/.codex/config.toml`:
|
|
31
|
+
```toml
|
|
32
|
+
[mcp_servers.x402scan]
|
|
33
|
+
command = "npx"
|
|
34
|
+
args = ["-y", "x402scan-mcp@latest"]
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
**Cursor** - Add to `.cursor/mcp.json`:
|
|
38
|
+
```json
|
|
39
|
+
{
|
|
40
|
+
"mcpServers": {
|
|
41
|
+
"x402scan": {
|
|
42
|
+
"command": "/bin/bash",
|
|
43
|
+
"args": ["-c", "source $HOME/.nvm/nvm.sh 2>/dev/null; exec npx -y x402scan-mcp@latest"]
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
**Claude Desktop** - Add to `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) or `%APPDATA%\Claude\claude_desktop_config.json` (Windows):
|
|
50
|
+
```json
|
|
51
|
+
{
|
|
52
|
+
"mcpServers": {
|
|
53
|
+
"x402scan": {
|
|
54
|
+
"command": "/bin/bash",
|
|
55
|
+
"args": ["-c", "source $HOME/.nvm/nvm.sh 2>/dev/null; exec npx -y x402scan-mcp@latest"]
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
</details>
|
|
62
|
+
|
|
63
|
+
## Usage
|
|
64
|
+
|
|
65
|
+
On first run, a wallet is generated at `~/.x402scan-mcp/wallet.json`. Deposit USDC on Base to the wallet address before making paid API calls.
|
|
66
|
+
|
|
67
|
+
**Workflow:**
|
|
68
|
+
1. `check_balance` - Check wallet and get deposit address
|
|
69
|
+
2. `query_endpoint` - Probe endpoint for pricing/schema (optional)
|
|
70
|
+
3. `execute_call` - Make the paid request
|
|
71
|
+
|
|
72
|
+
## Tools (4)
|
|
73
|
+
|
|
74
|
+
| Tool | Description |
|
|
75
|
+
|------|-------------|
|
|
76
|
+
| `check_balance` | Get wallet address and USDC balance |
|
|
77
|
+
| `query_endpoint` | Probe x402 endpoint for pricing/schema without payment |
|
|
78
|
+
| `validate_payment` | Pre-flight check if payment would succeed |
|
|
79
|
+
| `execute_call` | Make paid request to x402 endpoint |
|
|
80
|
+
|
|
81
|
+
## Environment
|
|
82
|
+
|
|
83
|
+
| Variable | Description |
|
|
84
|
+
|----------|-------------|
|
|
85
|
+
| `X402_PRIVATE_KEY` | Override wallet (optional) |
|
|
86
|
+
| `X402_DEBUG` | Set to `true` for verbose logging |
|
|
87
|
+
|
|
88
|
+
## Supported Networks
|
|
89
|
+
|
|
90
|
+
Base, Base Sepolia, Ethereum, Optimism, Arbitrum, Polygon (via CAIP-2)
|
|
91
|
+
|
|
92
|
+
## Develop
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
bun install
|
|
96
|
+
|
|
97
|
+
# Add local server to Claude Code
|
|
98
|
+
claude mcp add x402scan-dev -- bun run /path/to/x402scan-mcp/src/index.ts
|
|
99
|
+
|
|
100
|
+
# Build
|
|
101
|
+
bun run build
|
|
102
|
+
|
|
103
|
+
# Build .mcpb for Claude Desktop
|
|
104
|
+
bun run build:mcpb
|
|
105
|
+
```
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,968 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
5
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
6
|
+
|
|
7
|
+
// src/wallet/manager.ts
|
|
8
|
+
import { randomBytes } from "crypto";
|
|
9
|
+
import { privateKeyToAccount } from "viem/accounts";
|
|
10
|
+
import * as fs from "fs/promises";
|
|
11
|
+
import * as path from "path";
|
|
12
|
+
import * as os from "os";
|
|
13
|
+
|
|
14
|
+
// src/utils/logger.ts
|
|
15
|
+
import { appendFileSync, mkdirSync } from "fs";
|
|
16
|
+
import { join } from "path";
|
|
17
|
+
import { homedir } from "os";
|
|
18
|
+
var LOG_DIR = join(homedir(), ".x402scan-mcp");
|
|
19
|
+
var LOG_FILE = join(LOG_DIR, "mcp.log");
|
|
20
|
+
try {
|
|
21
|
+
mkdirSync(LOG_DIR, { recursive: true });
|
|
22
|
+
} catch {
|
|
23
|
+
}
|
|
24
|
+
function formatTimestamp() {
|
|
25
|
+
return (/* @__PURE__ */ new Date()).toISOString();
|
|
26
|
+
}
|
|
27
|
+
function formatArgs(args) {
|
|
28
|
+
return args.map((arg) => {
|
|
29
|
+
if (typeof arg === "object" && arg !== null) {
|
|
30
|
+
try {
|
|
31
|
+
return JSON.stringify(arg, null, 2);
|
|
32
|
+
} catch {
|
|
33
|
+
return String(arg);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return String(arg);
|
|
37
|
+
}).join(" ");
|
|
38
|
+
}
|
|
39
|
+
function writeLog(level, message, ...args) {
|
|
40
|
+
const formatted = args.length > 0 ? `${message} ${formatArgs(args)}` : message;
|
|
41
|
+
const line = `[${formatTimestamp()}] [${level}] ${formatted}
|
|
42
|
+
`;
|
|
43
|
+
try {
|
|
44
|
+
appendFileSync(LOG_FILE, line);
|
|
45
|
+
} catch {
|
|
46
|
+
}
|
|
47
|
+
console.error(`[x402scan] ${formatted}`);
|
|
48
|
+
}
|
|
49
|
+
var log = {
|
|
50
|
+
info: (message, ...args) => writeLog("INFO", message, ...args),
|
|
51
|
+
error: (message, ...args) => writeLog("ERROR", message, ...args),
|
|
52
|
+
debug: (message, ...args) => writeLog("DEBUG", message, ...args),
|
|
53
|
+
/** Clear the log file (adds separator) */
|
|
54
|
+
clear: () => {
|
|
55
|
+
try {
|
|
56
|
+
appendFileSync(LOG_FILE, `
|
|
57
|
+
--- Log cleared at ${formatTimestamp()} ---
|
|
58
|
+
`);
|
|
59
|
+
} catch {
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
/** Get log file path */
|
|
63
|
+
path: LOG_FILE
|
|
64
|
+
};
|
|
65
|
+
var DEBUG = process.env.X402_DEBUG === "true";
|
|
66
|
+
function logInfo(message) {
|
|
67
|
+
log.info(message);
|
|
68
|
+
}
|
|
69
|
+
function logPaymentRequired(pr) {
|
|
70
|
+
if (DEBUG) {
|
|
71
|
+
log.debug("Payment required:", pr);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
function logSignature(headers) {
|
|
75
|
+
if (DEBUG) {
|
|
76
|
+
log.debug("Payment headers:", Object.keys(headers).join(", "));
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
function logSettlement(response) {
|
|
80
|
+
if (DEBUG) {
|
|
81
|
+
log.debug("Settlement response:", response);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// src/wallet/manager.ts
|
|
86
|
+
var WALLET_DIR = path.join(os.homedir(), ".x402scan-mcp");
|
|
87
|
+
var WALLET_FILE = path.join(WALLET_DIR, "wallet.json");
|
|
88
|
+
function generatePrivateKey() {
|
|
89
|
+
return `0x${randomBytes(32).toString("hex")}`;
|
|
90
|
+
}
|
|
91
|
+
async function loadWalletConfig() {
|
|
92
|
+
try {
|
|
93
|
+
const data = await fs.readFile(WALLET_FILE, "utf-8");
|
|
94
|
+
return JSON.parse(data);
|
|
95
|
+
} catch {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
async function saveWalletConfig(config) {
|
|
100
|
+
await fs.mkdir(WALLET_DIR, { recursive: true });
|
|
101
|
+
await fs.writeFile(WALLET_FILE, JSON.stringify(config, null, 2));
|
|
102
|
+
try {
|
|
103
|
+
await fs.chmod(WALLET_FILE, 384);
|
|
104
|
+
} catch {
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
async function getOrCreateWallet() {
|
|
108
|
+
const envPrivateKey = process.env.X402_PRIVATE_KEY;
|
|
109
|
+
let privateKey;
|
|
110
|
+
let address;
|
|
111
|
+
let config;
|
|
112
|
+
let isNew = false;
|
|
113
|
+
if (envPrivateKey) {
|
|
114
|
+
privateKey = envPrivateKey;
|
|
115
|
+
const account2 = privateKeyToAccount(privateKey);
|
|
116
|
+
address = account2.address;
|
|
117
|
+
config = {
|
|
118
|
+
privateKey,
|
|
119
|
+
address,
|
|
120
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
121
|
+
};
|
|
122
|
+
logInfo(`Using wallet from environment: ${address}`);
|
|
123
|
+
} else {
|
|
124
|
+
const existingConfig = await loadWalletConfig();
|
|
125
|
+
if (!existingConfig) {
|
|
126
|
+
privateKey = generatePrivateKey();
|
|
127
|
+
const account2 = privateKeyToAccount(privateKey);
|
|
128
|
+
address = account2.address;
|
|
129
|
+
config = {
|
|
130
|
+
privateKey,
|
|
131
|
+
address,
|
|
132
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
133
|
+
};
|
|
134
|
+
await saveWalletConfig(config);
|
|
135
|
+
isNew = true;
|
|
136
|
+
logInfo(`Generated new wallet: ${address}`);
|
|
137
|
+
logInfo(`Wallet saved to: ${WALLET_FILE}`);
|
|
138
|
+
} else {
|
|
139
|
+
config = existingConfig;
|
|
140
|
+
privateKey = config.privateKey;
|
|
141
|
+
address = config.address;
|
|
142
|
+
logInfo(`Loaded wallet: ${address}`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
const account = privateKeyToAccount(privateKey);
|
|
146
|
+
return { account, address, config, isNew };
|
|
147
|
+
}
|
|
148
|
+
function getWalletFilePath() {
|
|
149
|
+
return WALLET_FILE;
|
|
150
|
+
}
|
|
151
|
+
async function walletExists() {
|
|
152
|
+
if (process.env.X402_PRIVATE_KEY) {
|
|
153
|
+
return true;
|
|
154
|
+
}
|
|
155
|
+
const config = await loadWalletConfig();
|
|
156
|
+
return config !== null;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// src/balance/usdc.ts
|
|
160
|
+
import { createPublicClient, http } from "viem";
|
|
161
|
+
|
|
162
|
+
// src/utils/networks.ts
|
|
163
|
+
import { base, baseSepolia, mainnet, sepolia, optimism, arbitrum, polygon } from "viem/chains";
|
|
164
|
+
var CHAIN_CONFIGS = {
|
|
165
|
+
// Base Mainnet
|
|
166
|
+
"eip155:8453": {
|
|
167
|
+
chain: base,
|
|
168
|
+
caip2: "eip155:8453",
|
|
169
|
+
v1Name: "base",
|
|
170
|
+
usdcAddress: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"
|
|
171
|
+
},
|
|
172
|
+
// Base Sepolia
|
|
173
|
+
"eip155:84532": {
|
|
174
|
+
chain: baseSepolia,
|
|
175
|
+
caip2: "eip155:84532",
|
|
176
|
+
v1Name: "base-sepolia",
|
|
177
|
+
usdcAddress: "0x036CbD53842c5426634e7929541eC2318f3dCF7e"
|
|
178
|
+
},
|
|
179
|
+
// Ethereum Mainnet
|
|
180
|
+
"eip155:1": {
|
|
181
|
+
chain: mainnet,
|
|
182
|
+
caip2: "eip155:1",
|
|
183
|
+
v1Name: "ethereum",
|
|
184
|
+
usdcAddress: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"
|
|
185
|
+
},
|
|
186
|
+
// Ethereum Sepolia
|
|
187
|
+
"eip155:11155111": {
|
|
188
|
+
chain: sepolia,
|
|
189
|
+
caip2: "eip155:11155111",
|
|
190
|
+
v1Name: "ethereum-sepolia",
|
|
191
|
+
usdcAddress: "0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238"
|
|
192
|
+
},
|
|
193
|
+
// Optimism
|
|
194
|
+
"eip155:10": {
|
|
195
|
+
chain: optimism,
|
|
196
|
+
caip2: "eip155:10",
|
|
197
|
+
v1Name: "optimism",
|
|
198
|
+
usdcAddress: "0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85"
|
|
199
|
+
},
|
|
200
|
+
// Arbitrum
|
|
201
|
+
"eip155:42161": {
|
|
202
|
+
chain: arbitrum,
|
|
203
|
+
caip2: "eip155:42161",
|
|
204
|
+
v1Name: "arbitrum",
|
|
205
|
+
usdcAddress: "0xaf88d065e77c8cC2239327C5EDb3A432268e5831"
|
|
206
|
+
},
|
|
207
|
+
// Polygon
|
|
208
|
+
"eip155:137": {
|
|
209
|
+
chain: polygon,
|
|
210
|
+
caip2: "eip155:137",
|
|
211
|
+
v1Name: "polygon",
|
|
212
|
+
usdcAddress: "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359"
|
|
213
|
+
}
|
|
214
|
+
};
|
|
215
|
+
var V1_TO_CAIP2 = {
|
|
216
|
+
"base": "eip155:8453",
|
|
217
|
+
"base-sepolia": "eip155:84532",
|
|
218
|
+
"ethereum": "eip155:1",
|
|
219
|
+
"ethereum-sepolia": "eip155:11155111",
|
|
220
|
+
"optimism": "eip155:10",
|
|
221
|
+
"arbitrum": "eip155:42161",
|
|
222
|
+
"polygon": "eip155:137"
|
|
223
|
+
};
|
|
224
|
+
var DEFAULT_NETWORK = "eip155:8453";
|
|
225
|
+
function toCaip2(network) {
|
|
226
|
+
if (network.startsWith("eip155:")) {
|
|
227
|
+
return network;
|
|
228
|
+
}
|
|
229
|
+
const caip2 = V1_TO_CAIP2[network.toLowerCase()];
|
|
230
|
+
if (caip2) {
|
|
231
|
+
return caip2;
|
|
232
|
+
}
|
|
233
|
+
return network;
|
|
234
|
+
}
|
|
235
|
+
function getChainConfig(network) {
|
|
236
|
+
const caip2 = toCaip2(network);
|
|
237
|
+
return CHAIN_CONFIGS[caip2];
|
|
238
|
+
}
|
|
239
|
+
function getUSDCAddress(network) {
|
|
240
|
+
const config = getChainConfig(network);
|
|
241
|
+
return config?.usdcAddress;
|
|
242
|
+
}
|
|
243
|
+
function getChain(network) {
|
|
244
|
+
const config = getChainConfig(network);
|
|
245
|
+
return config?.chain;
|
|
246
|
+
}
|
|
247
|
+
function getChainName(network) {
|
|
248
|
+
const config = getChainConfig(network);
|
|
249
|
+
if (config) {
|
|
250
|
+
return config.chain.name;
|
|
251
|
+
}
|
|
252
|
+
return network;
|
|
253
|
+
}
|
|
254
|
+
function getExplorerUrl(network) {
|
|
255
|
+
const config = getChainConfig(network);
|
|
256
|
+
return config?.chain.blockExplorers?.default.url;
|
|
257
|
+
}
|
|
258
|
+
function isTestnet(network) {
|
|
259
|
+
const config = getChainConfig(network);
|
|
260
|
+
if (!config) return false;
|
|
261
|
+
return config.chain.testnet === true;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// src/balance/usdc.ts
|
|
265
|
+
var ERC20_ABI = [
|
|
266
|
+
{
|
|
267
|
+
inputs: [{ name: "account", type: "address" }],
|
|
268
|
+
name: "balanceOf",
|
|
269
|
+
outputs: [{ name: "", type: "uint256" }],
|
|
270
|
+
stateMutability: "view",
|
|
271
|
+
type: "function"
|
|
272
|
+
}
|
|
273
|
+
];
|
|
274
|
+
async function getUSDCBalance(address, network = DEFAULT_NETWORK) {
|
|
275
|
+
const caip2Network = toCaip2(network);
|
|
276
|
+
const chain = getChain(caip2Network);
|
|
277
|
+
const usdcAddress = getUSDCAddress(caip2Network);
|
|
278
|
+
if (!chain) {
|
|
279
|
+
throw new Error(`Unsupported network: ${network} (${caip2Network})`);
|
|
280
|
+
}
|
|
281
|
+
if (!usdcAddress) {
|
|
282
|
+
throw new Error(`No USDC address configured for network: ${network}`);
|
|
283
|
+
}
|
|
284
|
+
log.debug(`Reading USDC balance for ${address} on ${chain.name}`);
|
|
285
|
+
const client = createPublicClient({
|
|
286
|
+
chain,
|
|
287
|
+
transport: http()
|
|
288
|
+
});
|
|
289
|
+
const balance = await client.readContract({
|
|
290
|
+
address: usdcAddress,
|
|
291
|
+
abi: ERC20_ABI,
|
|
292
|
+
functionName: "balanceOf",
|
|
293
|
+
args: [address]
|
|
294
|
+
});
|
|
295
|
+
const decimals = 6;
|
|
296
|
+
const formatted = Number(balance) / Math.pow(10, decimals);
|
|
297
|
+
const formattedString = `$${formatted.toFixed(2)}`;
|
|
298
|
+
log.debug(`Balance: ${formattedString}`);
|
|
299
|
+
return {
|
|
300
|
+
balance,
|
|
301
|
+
formatted,
|
|
302
|
+
formattedString,
|
|
303
|
+
decimals,
|
|
304
|
+
network: caip2Network,
|
|
305
|
+
usdcAddress
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
async function hasSufficientBalance(address, requiredAmount, network = DEFAULT_NETWORK) {
|
|
309
|
+
const required = typeof requiredAmount === "string" ? BigInt(requiredAmount) : requiredAmount;
|
|
310
|
+
const { balance } = await getUSDCBalance(address, network);
|
|
311
|
+
const sufficient = balance >= required;
|
|
312
|
+
const shortfall = sufficient ? 0n : required - balance;
|
|
313
|
+
return {
|
|
314
|
+
sufficient,
|
|
315
|
+
currentBalance: balance,
|
|
316
|
+
requiredAmount: required,
|
|
317
|
+
shortfall
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// src/utils/helpers.ts
|
|
322
|
+
function mcpSuccess(data) {
|
|
323
|
+
return {
|
|
324
|
+
content: [
|
|
325
|
+
{
|
|
326
|
+
type: "text",
|
|
327
|
+
text: JSON.stringify(data, null, 2)
|
|
328
|
+
}
|
|
329
|
+
]
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
function mcpError(error, context) {
|
|
333
|
+
let message;
|
|
334
|
+
let details;
|
|
335
|
+
if (error instanceof Error) {
|
|
336
|
+
message = error.message;
|
|
337
|
+
if ("cause" in error && error.cause) {
|
|
338
|
+
details = { cause: String(error.cause) };
|
|
339
|
+
}
|
|
340
|
+
} else if (typeof error === "string") {
|
|
341
|
+
message = error;
|
|
342
|
+
} else {
|
|
343
|
+
message = String(error);
|
|
344
|
+
}
|
|
345
|
+
const errorResponse = {
|
|
346
|
+
error: message
|
|
347
|
+
};
|
|
348
|
+
if (details) {
|
|
349
|
+
errorResponse.details = details;
|
|
350
|
+
}
|
|
351
|
+
if (context) {
|
|
352
|
+
errorResponse.context = context;
|
|
353
|
+
}
|
|
354
|
+
return {
|
|
355
|
+
content: [
|
|
356
|
+
{
|
|
357
|
+
type: "text",
|
|
358
|
+
text: JSON.stringify(errorResponse, null, 2)
|
|
359
|
+
}
|
|
360
|
+
],
|
|
361
|
+
isError: true
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
function formatUSDC(amount) {
|
|
365
|
+
const usd = Number(amount) / 1e6;
|
|
366
|
+
return `$${usd.toFixed(2)}`;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// src/tools/check_balance.ts
|
|
370
|
+
function registerCheckBalanceTool(server) {
|
|
371
|
+
server.tool(
|
|
372
|
+
"check_balance",
|
|
373
|
+
"Check wallet address and USDC balance. Creates wallet if needed. Returns address, balance, and funding instructions.",
|
|
374
|
+
{},
|
|
375
|
+
async () => {
|
|
376
|
+
try {
|
|
377
|
+
const { address, isNew } = await getOrCreateWallet();
|
|
378
|
+
const walletFile = getWalletFilePath();
|
|
379
|
+
let balance;
|
|
380
|
+
try {
|
|
381
|
+
balance = await getUSDCBalance(address, DEFAULT_NETWORK);
|
|
382
|
+
} catch (err) {
|
|
383
|
+
return mcpSuccess({
|
|
384
|
+
address,
|
|
385
|
+
network: DEFAULT_NETWORK,
|
|
386
|
+
networkName: getChainName(DEFAULT_NETWORK),
|
|
387
|
+
balanceUSDC: null,
|
|
388
|
+
balanceError: err instanceof Error ? err.message : "Failed to fetch balance",
|
|
389
|
+
walletFile,
|
|
390
|
+
isNewWallet: isNew,
|
|
391
|
+
fundingInstructions: getFundingInstructions(address, DEFAULT_NETWORK)
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
const response = {
|
|
395
|
+
address,
|
|
396
|
+
network: balance.network,
|
|
397
|
+
networkName: getChainName(balance.network),
|
|
398
|
+
balanceUSDC: balance.formatted,
|
|
399
|
+
balanceFormatted: balance.formattedString,
|
|
400
|
+
walletFile,
|
|
401
|
+
isNewWallet: isNew
|
|
402
|
+
};
|
|
403
|
+
if (balance.formatted < 1) {
|
|
404
|
+
response.fundingInstructions = getFundingInstructions(address, balance.network);
|
|
405
|
+
response.suggestion = balance.formatted === 0 ? "Your wallet has no USDC. Send USDC to the address above to start making paid API calls." : "Your balance is low. Consider topping up to continue making paid API calls.";
|
|
406
|
+
}
|
|
407
|
+
return mcpSuccess(response);
|
|
408
|
+
} catch (err) {
|
|
409
|
+
return mcpError(err, { tool: "check_balance" });
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
);
|
|
413
|
+
}
|
|
414
|
+
function getFundingInstructions(address, network) {
|
|
415
|
+
const explorerUrl = getExplorerUrl(network);
|
|
416
|
+
const usdcAddress = getUSDCAddress(network);
|
|
417
|
+
const chainName = getChainName(network);
|
|
418
|
+
const testnet = isTestnet(network);
|
|
419
|
+
return {
|
|
420
|
+
chainName,
|
|
421
|
+
isTestnet: testnet,
|
|
422
|
+
depositAddress: address,
|
|
423
|
+
usdcContract: usdcAddress,
|
|
424
|
+
explorerUrl: explorerUrl ? `${explorerUrl}/address/${address}` : void 0,
|
|
425
|
+
instructions: testnet ? `This is a testnet. Get test USDC from a faucet and send to ${address}` : `Send USDC on ${chainName} to ${address}`
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// src/tools/query_endpoint.ts
|
|
430
|
+
import { z } from "zod";
|
|
431
|
+
import { x402HTTPClient as x402HTTPClient2 } from "@x402/core/http";
|
|
432
|
+
import { x402Client as x402Client2 } from "@x402/core/client";
|
|
433
|
+
import { registerExactEvmScheme as registerExactEvmScheme2 } from "@x402/evm/exact/client";
|
|
434
|
+
import { privateKeyToAccount as privateKeyToAccount2 } from "viem/accounts";
|
|
435
|
+
import { randomBytes as randomBytes2 } from "crypto";
|
|
436
|
+
|
|
437
|
+
// src/x402/client.ts
|
|
438
|
+
import { x402Client } from "@x402/core/client";
|
|
439
|
+
import { x402HTTPClient } from "@x402/core/http";
|
|
440
|
+
import { registerExactEvmScheme } from "@x402/evm/exact/client";
|
|
441
|
+
function createX402Client(config) {
|
|
442
|
+
const { account, preferredNetwork } = config;
|
|
443
|
+
const coreClient = new x402Client(
|
|
444
|
+
preferredNetwork ? (_version, accepts) => {
|
|
445
|
+
const preferred = accepts.find((a) => toCaip2(a.network) === toCaip2(preferredNetwork));
|
|
446
|
+
return preferred || accepts[0];
|
|
447
|
+
} : void 0
|
|
448
|
+
);
|
|
449
|
+
registerExactEvmScheme(coreClient, { signer: account });
|
|
450
|
+
const httpClient = new x402HTTPClient(coreClient);
|
|
451
|
+
return { coreClient, httpClient };
|
|
452
|
+
}
|
|
453
|
+
async function makeX402Request(httpClient, url, options = {}) {
|
|
454
|
+
const { method = "GET", body, headers = {} } = options;
|
|
455
|
+
log.debug(`Making initial request: ${method} ${url}`);
|
|
456
|
+
let firstResponse;
|
|
457
|
+
try {
|
|
458
|
+
firstResponse = await fetch(url, {
|
|
459
|
+
method,
|
|
460
|
+
headers: {
|
|
461
|
+
"Content-Type": "application/json",
|
|
462
|
+
...headers
|
|
463
|
+
},
|
|
464
|
+
body: body ? JSON.stringify(body) : void 0
|
|
465
|
+
});
|
|
466
|
+
} catch (err) {
|
|
467
|
+
return {
|
|
468
|
+
success: false,
|
|
469
|
+
statusCode: 0,
|
|
470
|
+
error: {
|
|
471
|
+
phase: "initial_request",
|
|
472
|
+
message: `Network error: ${err instanceof Error ? err.message : String(err)}`
|
|
473
|
+
}
|
|
474
|
+
};
|
|
475
|
+
}
|
|
476
|
+
if (firstResponse.status !== 402) {
|
|
477
|
+
if (firstResponse.ok) {
|
|
478
|
+
try {
|
|
479
|
+
const data2 = await firstResponse.json();
|
|
480
|
+
return {
|
|
481
|
+
success: true,
|
|
482
|
+
statusCode: firstResponse.status,
|
|
483
|
+
data: data2
|
|
484
|
+
};
|
|
485
|
+
} catch {
|
|
486
|
+
return {
|
|
487
|
+
success: true,
|
|
488
|
+
statusCode: firstResponse.status,
|
|
489
|
+
data: await firstResponse.text()
|
|
490
|
+
};
|
|
491
|
+
}
|
|
492
|
+
} else {
|
|
493
|
+
const errorText = await firstResponse.text();
|
|
494
|
+
return {
|
|
495
|
+
success: false,
|
|
496
|
+
statusCode: firstResponse.status,
|
|
497
|
+
error: {
|
|
498
|
+
phase: "initial_request",
|
|
499
|
+
message: `HTTP ${firstResponse.status}: ${errorText}`
|
|
500
|
+
}
|
|
501
|
+
};
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
log.debug("Got 402 Payment Required, parsing requirements...");
|
|
505
|
+
let paymentRequired;
|
|
506
|
+
let responseBody;
|
|
507
|
+
try {
|
|
508
|
+
try {
|
|
509
|
+
responseBody = await firstResponse.clone().json();
|
|
510
|
+
} catch {
|
|
511
|
+
responseBody = void 0;
|
|
512
|
+
}
|
|
513
|
+
paymentRequired = httpClient.getPaymentRequiredResponse(
|
|
514
|
+
(name) => firstResponse.headers.get(name),
|
|
515
|
+
responseBody
|
|
516
|
+
);
|
|
517
|
+
logPaymentRequired(paymentRequired);
|
|
518
|
+
} catch (err) {
|
|
519
|
+
return {
|
|
520
|
+
success: false,
|
|
521
|
+
statusCode: 402,
|
|
522
|
+
error: {
|
|
523
|
+
phase: "parse_requirements",
|
|
524
|
+
message: `Failed to parse payment requirements: ${err instanceof Error ? err.message : String(err)}`,
|
|
525
|
+
details: {
|
|
526
|
+
headers: Object.fromEntries(firstResponse.headers.entries()),
|
|
527
|
+
body: responseBody
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
};
|
|
531
|
+
}
|
|
532
|
+
log.debug("Creating payment payload...");
|
|
533
|
+
let paymentPayload;
|
|
534
|
+
try {
|
|
535
|
+
paymentPayload = await httpClient.createPaymentPayload(paymentRequired);
|
|
536
|
+
log.debug(`Payment created for network: ${paymentPayload.accepted?.network}`);
|
|
537
|
+
} catch (err) {
|
|
538
|
+
return {
|
|
539
|
+
success: false,
|
|
540
|
+
statusCode: 402,
|
|
541
|
+
paymentRequired,
|
|
542
|
+
error: {
|
|
543
|
+
phase: "create_signature",
|
|
544
|
+
message: `Failed to create payment signature: ${err instanceof Error ? err.message : String(err)}`
|
|
545
|
+
}
|
|
546
|
+
};
|
|
547
|
+
}
|
|
548
|
+
const paymentHeaders = httpClient.encodePaymentSignatureHeader(paymentPayload);
|
|
549
|
+
logSignature(paymentHeaders);
|
|
550
|
+
log.debug("Retrying request with payment...");
|
|
551
|
+
let paidResponse;
|
|
552
|
+
try {
|
|
553
|
+
paidResponse = await fetch(url, {
|
|
554
|
+
method,
|
|
555
|
+
headers: {
|
|
556
|
+
"Content-Type": "application/json",
|
|
557
|
+
...paymentHeaders,
|
|
558
|
+
...headers
|
|
559
|
+
},
|
|
560
|
+
body: body ? JSON.stringify(body) : void 0
|
|
561
|
+
});
|
|
562
|
+
} catch (err) {
|
|
563
|
+
return {
|
|
564
|
+
success: false,
|
|
565
|
+
statusCode: 0,
|
|
566
|
+
paymentRequired,
|
|
567
|
+
error: {
|
|
568
|
+
phase: "paid_request",
|
|
569
|
+
message: `Network error on paid request: ${err instanceof Error ? err.message : String(err)}`
|
|
570
|
+
}
|
|
571
|
+
};
|
|
572
|
+
}
|
|
573
|
+
if (!paidResponse.ok) {
|
|
574
|
+
const errorText = await paidResponse.text();
|
|
575
|
+
return {
|
|
576
|
+
success: false,
|
|
577
|
+
statusCode: paidResponse.status,
|
|
578
|
+
paymentRequired,
|
|
579
|
+
error: {
|
|
580
|
+
phase: "paid_request",
|
|
581
|
+
message: `HTTP ${paidResponse.status} after payment: ${errorText}`,
|
|
582
|
+
details: {
|
|
583
|
+
headers: Object.fromEntries(paidResponse.headers.entries())
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
};
|
|
587
|
+
}
|
|
588
|
+
let settlement;
|
|
589
|
+
try {
|
|
590
|
+
const settleResponse = httpClient.getPaymentSettleResponse(
|
|
591
|
+
(name) => paidResponse.headers.get(name)
|
|
592
|
+
);
|
|
593
|
+
logSettlement(settleResponse);
|
|
594
|
+
settlement = {
|
|
595
|
+
transactionHash: settleResponse.transaction,
|
|
596
|
+
network: settleResponse.network,
|
|
597
|
+
payer: settleResponse.payer || paymentPayload.accepted?.payTo || ""
|
|
598
|
+
};
|
|
599
|
+
} catch (err) {
|
|
600
|
+
log.debug(`Could not parse settlement response: ${err instanceof Error ? err.message : String(err)}`);
|
|
601
|
+
}
|
|
602
|
+
let data;
|
|
603
|
+
try {
|
|
604
|
+
data = await paidResponse.json();
|
|
605
|
+
} catch {
|
|
606
|
+
data = await paidResponse.text();
|
|
607
|
+
}
|
|
608
|
+
return {
|
|
609
|
+
success: true,
|
|
610
|
+
statusCode: paidResponse.status,
|
|
611
|
+
data,
|
|
612
|
+
settlement,
|
|
613
|
+
paymentRequired
|
|
614
|
+
};
|
|
615
|
+
}
|
|
616
|
+
async function queryEndpoint(url, httpClient, options = {}) {
|
|
617
|
+
const { method = "GET", body, headers = {} } = options;
|
|
618
|
+
let response;
|
|
619
|
+
try {
|
|
620
|
+
response = await fetch(url, {
|
|
621
|
+
method,
|
|
622
|
+
headers: {
|
|
623
|
+
"Content-Type": "application/json",
|
|
624
|
+
...headers
|
|
625
|
+
},
|
|
626
|
+
body: body ? JSON.stringify(body) : void 0
|
|
627
|
+
});
|
|
628
|
+
} catch (err) {
|
|
629
|
+
return {
|
|
630
|
+
success: false,
|
|
631
|
+
statusCode: 0,
|
|
632
|
+
error: `Network error: ${err instanceof Error ? err.message : String(err)}`
|
|
633
|
+
};
|
|
634
|
+
}
|
|
635
|
+
const rawHeaders = Object.fromEntries(response.headers.entries());
|
|
636
|
+
if (response.status !== 402) {
|
|
637
|
+
return {
|
|
638
|
+
success: true,
|
|
639
|
+
statusCode: response.status,
|
|
640
|
+
rawHeaders
|
|
641
|
+
};
|
|
642
|
+
}
|
|
643
|
+
let rawBody;
|
|
644
|
+
try {
|
|
645
|
+
rawBody = await response.json();
|
|
646
|
+
} catch {
|
|
647
|
+
rawBody = void 0;
|
|
648
|
+
}
|
|
649
|
+
try {
|
|
650
|
+
const paymentRequired = httpClient.getPaymentRequiredResponse(
|
|
651
|
+
(name) => response.headers.get(name),
|
|
652
|
+
rawBody
|
|
653
|
+
);
|
|
654
|
+
return {
|
|
655
|
+
success: true,
|
|
656
|
+
statusCode: 402,
|
|
657
|
+
x402Version: paymentRequired.x402Version,
|
|
658
|
+
paymentRequired,
|
|
659
|
+
rawHeaders,
|
|
660
|
+
rawBody
|
|
661
|
+
};
|
|
662
|
+
} catch (err) {
|
|
663
|
+
return {
|
|
664
|
+
success: false,
|
|
665
|
+
statusCode: 402,
|
|
666
|
+
error: `Failed to parse payment requirements: ${err instanceof Error ? err.message : String(err)}`,
|
|
667
|
+
parseErrors: [err instanceof Error ? err.message : String(err)],
|
|
668
|
+
rawHeaders,
|
|
669
|
+
rawBody
|
|
670
|
+
};
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
// src/tools/query_endpoint.ts
|
|
675
|
+
function registerQueryEndpointTool(server) {
|
|
676
|
+
server.tool(
|
|
677
|
+
"query_endpoint",
|
|
678
|
+
"Probe an x402-protected endpoint to get pricing and schema without making a payment. Returns payment requirements, accepted networks, and Bazaar schema if available.",
|
|
679
|
+
{
|
|
680
|
+
url: z.string().url().describe("The x402-protected endpoint URL to probe"),
|
|
681
|
+
method: z.enum(["GET", "POST", "PUT", "DELETE", "PATCH"]).default("GET").describe("HTTP method to use"),
|
|
682
|
+
body: z.unknown().optional().describe("Request body for POST/PUT/PATCH methods")
|
|
683
|
+
},
|
|
684
|
+
async ({ url, method, body }) => {
|
|
685
|
+
try {
|
|
686
|
+
const tempKey = `0x${randomBytes2(32).toString("hex")}`;
|
|
687
|
+
const tempAccount = privateKeyToAccount2(tempKey);
|
|
688
|
+
const coreClient = new x402Client2();
|
|
689
|
+
registerExactEvmScheme2(coreClient, { signer: tempAccount });
|
|
690
|
+
const httpClient = new x402HTTPClient2(coreClient);
|
|
691
|
+
const result = await queryEndpoint(url, httpClient, {
|
|
692
|
+
method,
|
|
693
|
+
body
|
|
694
|
+
});
|
|
695
|
+
if (!result.success) {
|
|
696
|
+
return mcpError(result.error || "Failed to query endpoint", {
|
|
697
|
+
statusCode: result.statusCode,
|
|
698
|
+
parseErrors: result.parseErrors,
|
|
699
|
+
rawHeaders: result.rawHeaders,
|
|
700
|
+
rawBody: result.rawBody
|
|
701
|
+
});
|
|
702
|
+
}
|
|
703
|
+
if (result.statusCode !== 402) {
|
|
704
|
+
return mcpSuccess({
|
|
705
|
+
isX402Endpoint: false,
|
|
706
|
+
statusCode: result.statusCode,
|
|
707
|
+
message: "This endpoint does not require payment (no 402 response)",
|
|
708
|
+
rawHeaders: result.rawHeaders
|
|
709
|
+
});
|
|
710
|
+
}
|
|
711
|
+
const pr = result.paymentRequired;
|
|
712
|
+
const requirements = pr.accepts.map((req) => ({
|
|
713
|
+
scheme: req.scheme,
|
|
714
|
+
network: req.network,
|
|
715
|
+
networkName: getChainName(req.network),
|
|
716
|
+
price: formatUSDC(BigInt(req.amount)),
|
|
717
|
+
priceRaw: req.amount,
|
|
718
|
+
asset: req.asset,
|
|
719
|
+
payTo: req.payTo,
|
|
720
|
+
maxTimeoutSeconds: req.maxTimeoutSeconds,
|
|
721
|
+
extra: req.extra
|
|
722
|
+
}));
|
|
723
|
+
const response = {
|
|
724
|
+
isX402Endpoint: true,
|
|
725
|
+
x402Version: pr.x402Version,
|
|
726
|
+
requirements
|
|
727
|
+
};
|
|
728
|
+
if (pr.resource) {
|
|
729
|
+
response.resource = {
|
|
730
|
+
url: pr.resource.url,
|
|
731
|
+
description: pr.resource.description,
|
|
732
|
+
mimeType: pr.resource.mimeType
|
|
733
|
+
};
|
|
734
|
+
}
|
|
735
|
+
if (pr.extensions?.bazaar) {
|
|
736
|
+
const bazaar = pr.extensions.bazaar;
|
|
737
|
+
response.bazaar = {
|
|
738
|
+
info: bazaar.info,
|
|
739
|
+
schema: bazaar.schema,
|
|
740
|
+
examples: bazaar.examples,
|
|
741
|
+
hasBazaarExtension: true
|
|
742
|
+
};
|
|
743
|
+
}
|
|
744
|
+
if (pr.error) {
|
|
745
|
+
response.serverError = pr.error;
|
|
746
|
+
}
|
|
747
|
+
return mcpSuccess(response);
|
|
748
|
+
} catch (err) {
|
|
749
|
+
return mcpError(err, { tool: "query_endpoint", url });
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
);
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
// src/tools/validate_payment.ts
|
|
756
|
+
import { z as z2 } from "zod";
|
|
757
|
+
var PaymentRequirementsSchema = z2.object({
|
|
758
|
+
scheme: z2.string(),
|
|
759
|
+
network: z2.string(),
|
|
760
|
+
amount: z2.string(),
|
|
761
|
+
asset: z2.string(),
|
|
762
|
+
payTo: z2.string(),
|
|
763
|
+
maxTimeoutSeconds: z2.number(),
|
|
764
|
+
extra: z2.record(z2.unknown()).optional()
|
|
765
|
+
});
|
|
766
|
+
function registerValidatePaymentTool(server) {
|
|
767
|
+
server.tool(
|
|
768
|
+
"validate_payment",
|
|
769
|
+
"Pre-flight check if a payment would succeed. Validates wallet, network support, and balance. Use after query_endpoint to verify before execute_call.",
|
|
770
|
+
{
|
|
771
|
+
requirements: PaymentRequirementsSchema.describe(
|
|
772
|
+
"Payment requirements from query_endpoint result"
|
|
773
|
+
)
|
|
774
|
+
},
|
|
775
|
+
async ({ requirements }) => {
|
|
776
|
+
try {
|
|
777
|
+
const errors = [];
|
|
778
|
+
const warnings = [];
|
|
779
|
+
const checks = {};
|
|
780
|
+
const hasWallet = await walletExists();
|
|
781
|
+
checks.walletExists = hasWallet;
|
|
782
|
+
if (!hasWallet) {
|
|
783
|
+
errors.push("No wallet found. Run check_balance first to create a wallet.");
|
|
784
|
+
}
|
|
785
|
+
const caip2Network = toCaip2(requirements.network);
|
|
786
|
+
const chainConfig = getChainConfig(caip2Network);
|
|
787
|
+
checks.networkSupported = !!chainConfig;
|
|
788
|
+
if (!chainConfig) {
|
|
789
|
+
errors.push(
|
|
790
|
+
`Network not supported: ${requirements.network}. Supported networks: Base (eip155:8453), Base Sepolia (eip155:84532), Ethereum, Optimism, Arbitrum, Polygon.`
|
|
791
|
+
);
|
|
792
|
+
}
|
|
793
|
+
checks.schemeSupported = requirements.scheme === "exact";
|
|
794
|
+
if (requirements.scheme !== "exact") {
|
|
795
|
+
errors.push(
|
|
796
|
+
`Payment scheme not supported: ${requirements.scheme}. Only 'exact' scheme is supported for EVM chains.`
|
|
797
|
+
);
|
|
798
|
+
}
|
|
799
|
+
if (!hasWallet || !chainConfig) {
|
|
800
|
+
return mcpSuccess({
|
|
801
|
+
valid: false,
|
|
802
|
+
readyToExecute: false,
|
|
803
|
+
checks,
|
|
804
|
+
errors,
|
|
805
|
+
warnings
|
|
806
|
+
});
|
|
807
|
+
}
|
|
808
|
+
const { address } = await getOrCreateWallet();
|
|
809
|
+
let balanceResult;
|
|
810
|
+
try {
|
|
811
|
+
balanceResult = await hasSufficientBalance(
|
|
812
|
+
address,
|
|
813
|
+
requirements.amount,
|
|
814
|
+
caip2Network
|
|
815
|
+
);
|
|
816
|
+
checks.sufficientBalance = balanceResult.sufficient;
|
|
817
|
+
if (!balanceResult.sufficient) {
|
|
818
|
+
errors.push(
|
|
819
|
+
`Insufficient balance. Required: ${formatUSDC(balanceResult.requiredAmount)}, Available: ${formatUSDC(balanceResult.currentBalance)}, Shortfall: ${formatUSDC(balanceResult.shortfall)}`
|
|
820
|
+
);
|
|
821
|
+
}
|
|
822
|
+
} catch (err) {
|
|
823
|
+
checks.sufficientBalance = false;
|
|
824
|
+
errors.push(`Failed to check balance: ${err instanceof Error ? err.message : String(err)}`);
|
|
825
|
+
}
|
|
826
|
+
const expectedUsdc = chainConfig?.usdcAddress?.toLowerCase();
|
|
827
|
+
const providedAsset = requirements.asset.toLowerCase();
|
|
828
|
+
checks.assetIsUSDC = expectedUsdc === providedAsset;
|
|
829
|
+
if (!checks.assetIsUSDC) {
|
|
830
|
+
warnings.push(
|
|
831
|
+
`Asset may not be USDC. Expected: ${expectedUsdc}, Got: ${providedAsset}. Proceeding anyway, but verify the asset is correct.`
|
|
832
|
+
);
|
|
833
|
+
}
|
|
834
|
+
checks.signatureCapable = true;
|
|
835
|
+
const valid = errors.length === 0;
|
|
836
|
+
const response = {
|
|
837
|
+
valid,
|
|
838
|
+
readyToExecute: valid,
|
|
839
|
+
checks
|
|
840
|
+
};
|
|
841
|
+
if (balanceResult) {
|
|
842
|
+
response.balance = {
|
|
843
|
+
current: formatUSDC(balanceResult.currentBalance),
|
|
844
|
+
required: formatUSDC(balanceResult.requiredAmount),
|
|
845
|
+
sufficient: balanceResult.sufficient
|
|
846
|
+
};
|
|
847
|
+
}
|
|
848
|
+
response.network = {
|
|
849
|
+
requested: requirements.network,
|
|
850
|
+
resolved: caip2Network,
|
|
851
|
+
name: getChainName(caip2Network),
|
|
852
|
+
supported: !!chainConfig
|
|
853
|
+
};
|
|
854
|
+
if (errors.length > 0) {
|
|
855
|
+
response.errors = errors;
|
|
856
|
+
}
|
|
857
|
+
if (warnings.length > 0) {
|
|
858
|
+
response.warnings = warnings;
|
|
859
|
+
}
|
|
860
|
+
return mcpSuccess(response);
|
|
861
|
+
} catch (err) {
|
|
862
|
+
return mcpError(err, { tool: "validate_payment" });
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
);
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
// src/tools/execute_call.ts
|
|
869
|
+
import { z as z3 } from "zod";
|
|
870
|
+
function registerExecuteCallTool(server) {
|
|
871
|
+
server.tool(
|
|
872
|
+
"execute_call",
|
|
873
|
+
"Make a paid request to an x402-protected endpoint. Handles 402 payment flow automatically: detects payment requirements, signs payment, and executes request.",
|
|
874
|
+
{
|
|
875
|
+
url: z3.string().url().describe("The x402-protected endpoint URL"),
|
|
876
|
+
method: z3.enum(["GET", "POST", "PUT", "DELETE", "PATCH"]).default("GET").describe("HTTP method"),
|
|
877
|
+
body: z3.unknown().optional().describe("Request body for POST/PUT/PATCH methods"),
|
|
878
|
+
headers: z3.record(z3.string()).optional().describe("Additional headers to include in the request")
|
|
879
|
+
},
|
|
880
|
+
async ({ url, method, body, headers }) => {
|
|
881
|
+
try {
|
|
882
|
+
const { account, address } = await getOrCreateWallet();
|
|
883
|
+
const { httpClient } = createX402Client({ account });
|
|
884
|
+
const result = await makeX402Request(httpClient, url, {
|
|
885
|
+
method,
|
|
886
|
+
body,
|
|
887
|
+
headers
|
|
888
|
+
});
|
|
889
|
+
if (!result.success) {
|
|
890
|
+
const errorResponse = {
|
|
891
|
+
success: false,
|
|
892
|
+
statusCode: result.statusCode,
|
|
893
|
+
error: result.error
|
|
894
|
+
};
|
|
895
|
+
if (result.paymentRequired) {
|
|
896
|
+
errorResponse.paymentRequired = {
|
|
897
|
+
x402Version: result.paymentRequired.x402Version,
|
|
898
|
+
requirements: result.paymentRequired.accepts.map((req) => ({
|
|
899
|
+
network: req.network,
|
|
900
|
+
networkName: getChainName(req.network),
|
|
901
|
+
price: formatUSDC(BigInt(req.amount)),
|
|
902
|
+
priceRaw: req.amount
|
|
903
|
+
}))
|
|
904
|
+
};
|
|
905
|
+
}
|
|
906
|
+
return mcpError(result.error?.message || "Request failed", errorResponse);
|
|
907
|
+
}
|
|
908
|
+
const response = {
|
|
909
|
+
success: true,
|
|
910
|
+
statusCode: result.statusCode,
|
|
911
|
+
data: result.data
|
|
912
|
+
};
|
|
913
|
+
if (result.settlement) {
|
|
914
|
+
response.settlement = {
|
|
915
|
+
transactionHash: result.settlement.transactionHash,
|
|
916
|
+
network: result.settlement.network,
|
|
917
|
+
networkName: getChainName(result.settlement.network),
|
|
918
|
+
payer: address
|
|
919
|
+
};
|
|
920
|
+
if (result.paymentRequired?.accepts?.[0]) {
|
|
921
|
+
response.settlement = {
|
|
922
|
+
...response.settlement,
|
|
923
|
+
amountPaid: formatUSDC(BigInt(result.paymentRequired.accepts[0].amount))
|
|
924
|
+
};
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
if (result.paymentRequired) {
|
|
928
|
+
response.x402Version = result.paymentRequired.x402Version;
|
|
929
|
+
}
|
|
930
|
+
return mcpSuccess(response);
|
|
931
|
+
} catch (err) {
|
|
932
|
+
return mcpError(err, { tool: "execute_call", url });
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
);
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
// src/index.ts
|
|
939
|
+
async function main() {
|
|
940
|
+
log.clear();
|
|
941
|
+
log.info("Starting x402scan-mcp server...");
|
|
942
|
+
const server = new McpServer({
|
|
943
|
+
name: "x402scan",
|
|
944
|
+
version: "0.0.1"
|
|
945
|
+
});
|
|
946
|
+
registerCheckBalanceTool(server);
|
|
947
|
+
registerQueryEndpointTool(server);
|
|
948
|
+
registerValidatePaymentTool(server);
|
|
949
|
+
registerExecuteCallTool(server);
|
|
950
|
+
log.info("Registered 4 tools: check_balance, query_endpoint, validate_payment, execute_call");
|
|
951
|
+
const transport = new StdioServerTransport();
|
|
952
|
+
await server.connect(transport);
|
|
953
|
+
log.info(`Connected to transport, ready for requests. Log file: ${log.path}`);
|
|
954
|
+
process.on("SIGINT", async () => {
|
|
955
|
+
log.info("Shutting down...");
|
|
956
|
+
await server.close();
|
|
957
|
+
process.exit(0);
|
|
958
|
+
});
|
|
959
|
+
process.on("SIGTERM", async () => {
|
|
960
|
+
log.info("Shutting down...");
|
|
961
|
+
await server.close();
|
|
962
|
+
process.exit(0);
|
|
963
|
+
});
|
|
964
|
+
}
|
|
965
|
+
main().catch((error) => {
|
|
966
|
+
log.error("Fatal error", error);
|
|
967
|
+
process.exit(1);
|
|
968
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "x402scan-mcp",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Generic MCP server for calling x402-protected APIs with automatic payment handling",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"x402scan-mcp": "dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist"
|
|
12
|
+
],
|
|
13
|
+
"scripts": {
|
|
14
|
+
"build": "tsup",
|
|
15
|
+
"build:mcpb": "tsx scripts/build-mcpb.ts",
|
|
16
|
+
"dev": "tsx src/index.ts",
|
|
17
|
+
"dev:bun": "bun run src/index.ts",
|
|
18
|
+
"typecheck": "tsc --noEmit",
|
|
19
|
+
"publish-package": "tsx scripts/publish.ts",
|
|
20
|
+
"prepublishOnly": "npm run build"
|
|
21
|
+
},
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"@modelcontextprotocol/sdk": "^1.6.1",
|
|
24
|
+
"@x402/core": "^2.0.0",
|
|
25
|
+
"@x402/evm": "^2.0.0",
|
|
26
|
+
"viem": "^2.31.3",
|
|
27
|
+
"zod": "^3.25.1"
|
|
28
|
+
},
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"@types/node": "^22.15.21",
|
|
31
|
+
"tsup": "^8.5.0",
|
|
32
|
+
"tsx": "^4.20.3",
|
|
33
|
+
"typescript": "^5.8.3"
|
|
34
|
+
},
|
|
35
|
+
"engines": {
|
|
36
|
+
"node": ">=18"
|
|
37
|
+
},
|
|
38
|
+
"publishConfig": {
|
|
39
|
+
"access": "public"
|
|
40
|
+
},
|
|
41
|
+
"keywords": [
|
|
42
|
+
"mcp",
|
|
43
|
+
"x402",
|
|
44
|
+
"payments",
|
|
45
|
+
"ai",
|
|
46
|
+
"claude",
|
|
47
|
+
"model-context-protocol"
|
|
48
|
+
],
|
|
49
|
+
"license": "MIT",
|
|
50
|
+
"repository": {
|
|
51
|
+
"type": "git",
|
|
52
|
+
"url": "https://github.com/merit-systems/x402scan-mcp"
|
|
53
|
+
}
|
|
54
|
+
}
|