x402scan-mcp 0.0.1 → 0.0.3
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/dist/index.js +252 -7
- package/package.json +11 -11
package/dist/index.js
CHANGED
|
@@ -438,6 +438,73 @@ import { randomBytes as randomBytes2 } from "crypto";
|
|
|
438
438
|
import { x402Client } from "@x402/core/client";
|
|
439
439
|
import { x402HTTPClient } from "@x402/core/http";
|
|
440
440
|
import { registerExactEvmScheme } from "@x402/evm/exact/client";
|
|
441
|
+
|
|
442
|
+
// src/x402/normalize.ts
|
|
443
|
+
function isV1Response(pr) {
|
|
444
|
+
if (!pr || typeof pr !== "object") return false;
|
|
445
|
+
const obj = pr;
|
|
446
|
+
if (obj.x402Version === 1) return true;
|
|
447
|
+
const accepts = obj.accepts;
|
|
448
|
+
if (Array.isArray(accepts) && accepts.length > 0) {
|
|
449
|
+
return "maxAmountRequired" in accepts[0];
|
|
450
|
+
}
|
|
451
|
+
return false;
|
|
452
|
+
}
|
|
453
|
+
function normalizeV1Requirement(req) {
|
|
454
|
+
if (!req.maxAmountRequired) {
|
|
455
|
+
throw new Error("v1 requirement missing maxAmountRequired field");
|
|
456
|
+
}
|
|
457
|
+
return {
|
|
458
|
+
scheme: req.scheme,
|
|
459
|
+
network: req.network,
|
|
460
|
+
amount: req.maxAmountRequired,
|
|
461
|
+
// Map to unified 'amount' field
|
|
462
|
+
asset: req.asset,
|
|
463
|
+
payTo: req.payTo,
|
|
464
|
+
maxTimeoutSeconds: req.maxTimeoutSeconds,
|
|
465
|
+
extra: req.extra,
|
|
466
|
+
// Preserve v1-specific embedded resource fields
|
|
467
|
+
resource: req.resource,
|
|
468
|
+
description: req.description,
|
|
469
|
+
mimeType: req.mimeType
|
|
470
|
+
};
|
|
471
|
+
}
|
|
472
|
+
function normalizeV2Requirement(req) {
|
|
473
|
+
if (!req.amount) {
|
|
474
|
+
throw new Error("v2 requirement missing amount field");
|
|
475
|
+
}
|
|
476
|
+
return {
|
|
477
|
+
scheme: req.scheme,
|
|
478
|
+
network: req.network,
|
|
479
|
+
amount: req.amount,
|
|
480
|
+
asset: req.asset,
|
|
481
|
+
payTo: req.payTo,
|
|
482
|
+
maxTimeoutSeconds: req.maxTimeoutSeconds,
|
|
483
|
+
extra: req.extra
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
function normalizePaymentRequired(pr) {
|
|
487
|
+
const version = pr.x402Version ?? 1;
|
|
488
|
+
if (isV1Response(pr)) {
|
|
489
|
+
const v1Pr = pr;
|
|
490
|
+
return {
|
|
491
|
+
x402Version: 1,
|
|
492
|
+
error: v1Pr.error,
|
|
493
|
+
accepts: v1Pr.accepts.map(normalizeV1Requirement)
|
|
494
|
+
};
|
|
495
|
+
} else {
|
|
496
|
+
const v2Pr = pr;
|
|
497
|
+
return {
|
|
498
|
+
x402Version: version,
|
|
499
|
+
error: v2Pr.error,
|
|
500
|
+
accepts: v2Pr.accepts.map(normalizeV2Requirement),
|
|
501
|
+
resource: v2Pr.resource,
|
|
502
|
+
extensions: v2Pr.extensions
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// src/x402/client.ts
|
|
441
508
|
function createX402Client(config) {
|
|
442
509
|
const { account, preferredNetwork } = config;
|
|
443
510
|
const coreClient = new x402Client(
|
|
@@ -502,6 +569,7 @@ async function makeX402Request(httpClient, url, options = {}) {
|
|
|
502
569
|
}
|
|
503
570
|
}
|
|
504
571
|
log.debug("Got 402 Payment Required, parsing requirements...");
|
|
572
|
+
let rawPaymentRequired;
|
|
505
573
|
let paymentRequired;
|
|
506
574
|
let responseBody;
|
|
507
575
|
try {
|
|
@@ -510,11 +578,12 @@ async function makeX402Request(httpClient, url, options = {}) {
|
|
|
510
578
|
} catch {
|
|
511
579
|
responseBody = void 0;
|
|
512
580
|
}
|
|
513
|
-
|
|
581
|
+
rawPaymentRequired = httpClient.getPaymentRequiredResponse(
|
|
514
582
|
(name) => firstResponse.headers.get(name),
|
|
515
583
|
responseBody
|
|
516
584
|
);
|
|
517
|
-
|
|
585
|
+
paymentRequired = normalizePaymentRequired(rawPaymentRequired);
|
|
586
|
+
logPaymentRequired(rawPaymentRequired);
|
|
518
587
|
} catch (err) {
|
|
519
588
|
return {
|
|
520
589
|
success: false,
|
|
@@ -532,7 +601,7 @@ async function makeX402Request(httpClient, url, options = {}) {
|
|
|
532
601
|
log.debug("Creating payment payload...");
|
|
533
602
|
let paymentPayload;
|
|
534
603
|
try {
|
|
535
|
-
paymentPayload = await httpClient.createPaymentPayload(
|
|
604
|
+
paymentPayload = await httpClient.createPaymentPayload(rawPaymentRequired);
|
|
536
605
|
log.debug(`Payment created for network: ${paymentPayload.accepted?.network}`);
|
|
537
606
|
} catch (err) {
|
|
538
607
|
return {
|
|
@@ -647,10 +716,11 @@ async function queryEndpoint(url, httpClient, options = {}) {
|
|
|
647
716
|
rawBody = void 0;
|
|
648
717
|
}
|
|
649
718
|
try {
|
|
650
|
-
const
|
|
719
|
+
const rawPaymentRequired = httpClient.getPaymentRequiredResponse(
|
|
651
720
|
(name) => response.headers.get(name),
|
|
652
721
|
rawBody
|
|
653
722
|
);
|
|
723
|
+
const paymentRequired = normalizePaymentRequired(rawPaymentRequired);
|
|
654
724
|
return {
|
|
655
725
|
success: true,
|
|
656
726
|
statusCode: 402,
|
|
@@ -757,12 +827,21 @@ import { z as z2 } from "zod";
|
|
|
757
827
|
var PaymentRequirementsSchema = z2.object({
|
|
758
828
|
scheme: z2.string(),
|
|
759
829
|
network: z2.string(),
|
|
760
|
-
amount: z2.string(),
|
|
830
|
+
amount: z2.string().optional(),
|
|
831
|
+
// v2 field name
|
|
832
|
+
maxAmountRequired: z2.string().optional(),
|
|
833
|
+
// v1 field name
|
|
761
834
|
asset: z2.string(),
|
|
762
835
|
payTo: z2.string(),
|
|
763
836
|
maxTimeoutSeconds: z2.number(),
|
|
764
837
|
extra: z2.record(z2.unknown()).optional()
|
|
765
|
-
})
|
|
838
|
+
}).refine((data) => data.amount || data.maxAmountRequired, {
|
|
839
|
+
message: "Either amount (v2) or maxAmountRequired (v1) must be provided"
|
|
840
|
+
}).transform((data) => ({
|
|
841
|
+
...data,
|
|
842
|
+
// Normalize to 'amount' internally
|
|
843
|
+
amount: data.amount ?? data.maxAmountRequired
|
|
844
|
+
}));
|
|
766
845
|
function registerValidatePaymentTool(server) {
|
|
767
846
|
server.tool(
|
|
768
847
|
"validate_payment",
|
|
@@ -935,6 +1014,170 @@ function registerExecuteCallTool(server) {
|
|
|
935
1014
|
);
|
|
936
1015
|
}
|
|
937
1016
|
|
|
1017
|
+
// src/tools/siwe.ts
|
|
1018
|
+
import { z as z4 } from "zod";
|
|
1019
|
+
import { SiweMessage, generateNonce } from "siwe";
|
|
1020
|
+
var CHAIN_IDS = {
|
|
1021
|
+
mainnet: 1,
|
|
1022
|
+
base: 8453,
|
|
1023
|
+
"base-sepolia": 84532,
|
|
1024
|
+
optimism: 10,
|
|
1025
|
+
arbitrum: 42161,
|
|
1026
|
+
polygon: 137,
|
|
1027
|
+
sepolia: 11155111
|
|
1028
|
+
};
|
|
1029
|
+
function registerCreateSiweProofTool(server) {
|
|
1030
|
+
server.tool(
|
|
1031
|
+
"create_siwe_proof",
|
|
1032
|
+
"Create a SIWE (Sign-In with Ethereum) proof for wallet authentication. Returns a proof string to use in X-SIWE-PROOF header.",
|
|
1033
|
+
{
|
|
1034
|
+
domain: z4.string().describe(
|
|
1035
|
+
'Domain requesting auth (e.g., "localhost:3000" or "stablestudio.io")'
|
|
1036
|
+
),
|
|
1037
|
+
uri: z4.string().url().describe('Full URI of the API (e.g., "http://localhost:3000")'),
|
|
1038
|
+
statement: z4.string().optional().default("Authenticate to API").describe("Human-readable statement"),
|
|
1039
|
+
chainId: z4.number().optional().describe(
|
|
1040
|
+
"Chain ID (default: 8453 for Base). Common IDs: 1=mainnet, 8453=base, 84532=base-sepolia, 10=optimism"
|
|
1041
|
+
),
|
|
1042
|
+
chain: z4.enum(["mainnet", "base", "base-sepolia", "optimism", "arbitrum", "polygon", "sepolia"]).optional().describe("Chain name (alternative to chainId). Default: base"),
|
|
1043
|
+
expirationMinutes: z4.number().optional().default(60).describe("Proof validity in minutes")
|
|
1044
|
+
},
|
|
1045
|
+
async ({ domain, uri, statement, chainId, chain, expirationMinutes }) => {
|
|
1046
|
+
try {
|
|
1047
|
+
const { account, address } = await getOrCreateWallet();
|
|
1048
|
+
const resolvedChainId = chainId ?? (chain ? CHAIN_IDS[chain] : CHAIN_IDS.base);
|
|
1049
|
+
const siweMessage = new SiweMessage({
|
|
1050
|
+
domain,
|
|
1051
|
+
address,
|
|
1052
|
+
statement,
|
|
1053
|
+
uri,
|
|
1054
|
+
version: "1",
|
|
1055
|
+
chainId: resolvedChainId,
|
|
1056
|
+
nonce: generateNonce(),
|
|
1057
|
+
issuedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1058
|
+
expirationTime: new Date(
|
|
1059
|
+
Date.now() + expirationMinutes * 60 * 1e3
|
|
1060
|
+
).toISOString()
|
|
1061
|
+
});
|
|
1062
|
+
const message = siweMessage.prepareMessage();
|
|
1063
|
+
const signature = await account.signMessage({ message });
|
|
1064
|
+
const proof = JSON.stringify({
|
|
1065
|
+
message: JSON.stringify(siweMessage),
|
|
1066
|
+
signature
|
|
1067
|
+
});
|
|
1068
|
+
return mcpSuccess({
|
|
1069
|
+
proof,
|
|
1070
|
+
address,
|
|
1071
|
+
chainId: resolvedChainId,
|
|
1072
|
+
expiresAt: siweMessage.expirationTime,
|
|
1073
|
+
usage: "Add to request headers as: X-SIWE-PROOF: <proof>"
|
|
1074
|
+
});
|
|
1075
|
+
} catch (err) {
|
|
1076
|
+
return mcpError(err, { tool: "create_siwe_proof" });
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
);
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
// src/tools/fetch_with_siwe.ts
|
|
1083
|
+
import { z as z5 } from "zod";
|
|
1084
|
+
import { SiweMessage as SiweMessage2, generateNonce as generateNonce2 } from "siwe";
|
|
1085
|
+
var CHAIN_IDS2 = {
|
|
1086
|
+
mainnet: 1,
|
|
1087
|
+
base: 8453,
|
|
1088
|
+
"base-sepolia": 84532,
|
|
1089
|
+
optimism: 10,
|
|
1090
|
+
arbitrum: 42161,
|
|
1091
|
+
polygon: 137,
|
|
1092
|
+
sepolia: 11155111
|
|
1093
|
+
};
|
|
1094
|
+
async function createSiweProof(account, domain, uri, chainId) {
|
|
1095
|
+
const siweMessage = new SiweMessage2({
|
|
1096
|
+
domain,
|
|
1097
|
+
address: account.address,
|
|
1098
|
+
statement: "Authenticate to API",
|
|
1099
|
+
uri,
|
|
1100
|
+
version: "1",
|
|
1101
|
+
chainId,
|
|
1102
|
+
nonce: generateNonce2(),
|
|
1103
|
+
issuedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1104
|
+
expirationTime: new Date(Date.now() + 36e5).toISOString()
|
|
1105
|
+
});
|
|
1106
|
+
const message = siweMessage.prepareMessage();
|
|
1107
|
+
const signature = await account.signMessage({ message });
|
|
1108
|
+
return JSON.stringify({
|
|
1109
|
+
message: JSON.stringify(siweMessage),
|
|
1110
|
+
signature
|
|
1111
|
+
});
|
|
1112
|
+
}
|
|
1113
|
+
function registerFetchWithSiweTool(server) {
|
|
1114
|
+
server.tool(
|
|
1115
|
+
"fetch_with_siwe",
|
|
1116
|
+
"Make an HTTP request with automatic SIWE wallet authentication. Useful for APIs that require wallet ownership proof.",
|
|
1117
|
+
{
|
|
1118
|
+
url: z5.string().url().describe("The URL to fetch"),
|
|
1119
|
+
method: z5.enum(["GET", "POST", "PUT", "DELETE", "PATCH"]).optional().default("GET"),
|
|
1120
|
+
body: z5.unknown().optional().describe("Request body for POST/PUT/PATCH"),
|
|
1121
|
+
headers: z5.record(z5.string()).optional().describe("Additional headers"),
|
|
1122
|
+
siweHeader: z5.string().optional().default("X-SIWE-PROOF").describe("Header name for SIWE proof"),
|
|
1123
|
+
chainId: z5.number().optional().describe(
|
|
1124
|
+
"Chain ID for SIWE proof (default: 8453 for Base). Common IDs: 1=mainnet, 8453=base, 84532=base-sepolia"
|
|
1125
|
+
),
|
|
1126
|
+
chain: z5.enum(["mainnet", "base", "base-sepolia", "optimism", "arbitrum", "polygon", "sepolia"]).optional().describe("Chain name (alternative to chainId). Default: base")
|
|
1127
|
+
},
|
|
1128
|
+
async ({ url, method, body, headers, siweHeader, chainId, chain }) => {
|
|
1129
|
+
try {
|
|
1130
|
+
const { account } = await getOrCreateWallet();
|
|
1131
|
+
const parsedUrl = new URL(url);
|
|
1132
|
+
const resolvedChainId = chainId ?? (chain ? CHAIN_IDS2[chain] : CHAIN_IDS2.base);
|
|
1133
|
+
const proof = await createSiweProof(
|
|
1134
|
+
account,
|
|
1135
|
+
parsedUrl.host,
|
|
1136
|
+
parsedUrl.origin,
|
|
1137
|
+
resolvedChainId
|
|
1138
|
+
);
|
|
1139
|
+
const response = await fetch(url, {
|
|
1140
|
+
method,
|
|
1141
|
+
headers: {
|
|
1142
|
+
"Content-Type": "application/json",
|
|
1143
|
+
[siweHeader]: proof,
|
|
1144
|
+
...headers
|
|
1145
|
+
},
|
|
1146
|
+
body: body ? JSON.stringify(body) : void 0
|
|
1147
|
+
});
|
|
1148
|
+
const responseHeaders = Object.fromEntries(response.headers.entries());
|
|
1149
|
+
if (!response.ok) {
|
|
1150
|
+
let errorBody;
|
|
1151
|
+
try {
|
|
1152
|
+
errorBody = await response.json();
|
|
1153
|
+
} catch {
|
|
1154
|
+
errorBody = await response.text();
|
|
1155
|
+
}
|
|
1156
|
+
return mcpError(`HTTP ${response.status}`, {
|
|
1157
|
+
statusCode: response.status,
|
|
1158
|
+
headers: responseHeaders,
|
|
1159
|
+
body: errorBody
|
|
1160
|
+
});
|
|
1161
|
+
}
|
|
1162
|
+
let data;
|
|
1163
|
+
const contentType = response.headers.get("content-type");
|
|
1164
|
+
if (contentType?.includes("application/json")) {
|
|
1165
|
+
data = await response.json();
|
|
1166
|
+
} else {
|
|
1167
|
+
data = await response.text();
|
|
1168
|
+
}
|
|
1169
|
+
return mcpSuccess({
|
|
1170
|
+
statusCode: response.status,
|
|
1171
|
+
headers: responseHeaders,
|
|
1172
|
+
data
|
|
1173
|
+
});
|
|
1174
|
+
} catch (err) {
|
|
1175
|
+
return mcpError(err, { tool: "fetch_with_siwe", url });
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
);
|
|
1179
|
+
}
|
|
1180
|
+
|
|
938
1181
|
// src/index.ts
|
|
939
1182
|
async function main() {
|
|
940
1183
|
log.clear();
|
|
@@ -947,7 +1190,9 @@ async function main() {
|
|
|
947
1190
|
registerQueryEndpointTool(server);
|
|
948
1191
|
registerValidatePaymentTool(server);
|
|
949
1192
|
registerExecuteCallTool(server);
|
|
950
|
-
|
|
1193
|
+
registerCreateSiweProofTool(server);
|
|
1194
|
+
registerFetchWithSiweTool(server);
|
|
1195
|
+
log.info("Registered 6 tools: check_balance, query_endpoint, validate_payment, execute_call, create_siwe_proof, fetch_with_siwe");
|
|
951
1196
|
const transport = new StdioServerTransport();
|
|
952
1197
|
await server.connect(transport);
|
|
953
1198
|
log.info(`Connected to transport, ready for requests. Log file: ${log.path}`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "x402scan-mcp",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.3",
|
|
4
4
|
"description": "Generic MCP server for calling x402-protected APIs with automatic payment handling",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -10,19 +10,11 @@
|
|
|
10
10
|
"files": [
|
|
11
11
|
"dist"
|
|
12
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
13
|
"dependencies": {
|
|
23
14
|
"@modelcontextprotocol/sdk": "^1.6.1",
|
|
24
15
|
"@x402/core": "^2.0.0",
|
|
25
16
|
"@x402/evm": "^2.0.0",
|
|
17
|
+
"siwe": "^2.3.2",
|
|
26
18
|
"viem": "^2.31.3",
|
|
27
19
|
"zod": "^3.25.1"
|
|
28
20
|
},
|
|
@@ -50,5 +42,13 @@
|
|
|
50
42
|
"repository": {
|
|
51
43
|
"type": "git",
|
|
52
44
|
"url": "https://github.com/merit-systems/x402scan-mcp"
|
|
45
|
+
},
|
|
46
|
+
"scripts": {
|
|
47
|
+
"build": "tsup",
|
|
48
|
+
"build:mcpb": "tsx scripts/build-mcpb.ts",
|
|
49
|
+
"dev": "tsx src/index.ts",
|
|
50
|
+
"dev:bun": "bun run src/index.ts",
|
|
51
|
+
"typecheck": "tsc --noEmit",
|
|
52
|
+
"publish-package": "tsx scripts/publish.ts"
|
|
53
53
|
}
|
|
54
|
-
}
|
|
54
|
+
}
|