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.
Files changed (2) hide show
  1. package/dist/index.js +252 -7
  2. 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
- paymentRequired = httpClient.getPaymentRequiredResponse(
581
+ rawPaymentRequired = httpClient.getPaymentRequiredResponse(
514
582
  (name) => firstResponse.headers.get(name),
515
583
  responseBody
516
584
  );
517
- logPaymentRequired(paymentRequired);
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(paymentRequired);
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 paymentRequired = httpClient.getPaymentRequiredResponse(
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
- log.info("Registered 4 tools: check_balance, query_endpoint, validate_payment, execute_call");
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.1",
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
+ }