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 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
+ [![Install MCP Server](https://cursor.com/deeplink/mcp-install-dark.svg)](https://cursor.com/en/install-mcp?name=x402scan&config=eyJjb21tYW5kIjoiL2Jpbi9iYXNoIiwiYXJncyI6WyItYyIsInNvdXJjZSAkSE9NRS8ubnZtL252bS5zaCAyPi9kZXYvbnVsbDsgZXhlYyBucHggLXkgeDQwMnNjYW4tbWNwQGxhdGVzdCJdfQ%3D%3D)
22
+
23
+ ### Claude Desktop
24
+
25
+ [![Add to Claude](https://img.shields.io/badge/Add_to_Claude-x402scan-blue?logo=anthropic)](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
+ ```
@@ -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
+ }