@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.
@@ -1,1109 +0,0 @@
1
- import {
2
- getUSDCBalance,
3
- tokenStringToNumber
4
- } from "./chunk-KD3GRRAV.js";
5
- import {
6
- DEFAULT_NETWORK,
7
- getChainName,
8
- getDepositLink,
9
- getWallet,
10
- log,
11
- openDepositLink,
12
- requestSchema,
13
- requestWithHeadersSchema
14
- } from "./chunk-FFXFOKKF.js";
15
-
16
- // src/server/index.ts
17
- import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
18
- import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
19
-
20
- // src/server/tools/fetch-x402-resource.ts
21
- import { x402Client, x402HTTPClient } from "@x402/core/client";
22
- import { ExactEvmScheme } from "@x402/evm/exact/client";
23
- import { wrapFetchWithPayment } from "@x402/fetch";
24
-
25
- // src/server/lib/response.ts
26
- var mcpSuccess = (data) => {
27
- return {
28
- content: [{ type: "text", text: JSON.stringify(data, null, 2) }]
29
- };
30
- };
31
- var mcpError = (error, context) => {
32
- const message = error instanceof Error ? error.message : typeof error === "string" ? error : String(error);
33
- const details = error instanceof Error && error.cause ? { cause: JSON.stringify(error.cause) } : void 0;
34
- return {
35
- content: [
36
- {
37
- type: "text",
38
- text: JSON.stringify(
39
- {
40
- error: message,
41
- ...details && { details },
42
- ...context && { context }
43
- },
44
- null,
45
- 2
46
- )
47
- }
48
- ],
49
- isError: true
50
- };
51
- };
52
-
53
- // src/server/lib/check-balance.ts
54
- var checkBalance = async ({
55
- server,
56
- address,
57
- amountNeeded,
58
- message,
59
- flags
60
- }) => {
61
- const balance = await getUSDCBalance({
62
- address
63
- });
64
- if (balance < amountNeeded) {
65
- const capabilities = server.server.getClientCapabilities();
66
- if (!capabilities?.elicitation) {
67
- throw new Error(
68
- `${message(balance)}
69
-
70
- You can deposit USDC at ${getDepositLink(address, flags)}`
71
- );
72
- }
73
- const result = await server.server.elicitInput({
74
- mode: "form",
75
- message: message(balance),
76
- requestedSchema: {
77
- type: "object",
78
- properties: {}
79
- }
80
- });
81
- if (result.action === "accept") {
82
- await openDepositLink(address, flags);
83
- }
84
- }
85
- return balance;
86
- };
87
-
88
- // src/server/tools/fetch-x402-resource.ts
89
- var registerFetchX402ResourceTool = ({
90
- server,
91
- account,
92
- flags
93
- }) => {
94
- server.registerTool(
95
- "fetch",
96
- {
97
- description: "Fetches an x402-protected resource and handles payment automatically. If the resource is not x402-protected, it will return the raw response.",
98
- inputSchema: requestWithHeadersSchema
99
- },
100
- async ({ url, method, body, headers }) => {
101
- const coreClient = x402Client.fromConfig({
102
- schemes: [
103
- { network: DEFAULT_NETWORK, client: new ExactEvmScheme(account) }
104
- ]
105
- });
106
- let state = "initial_request" /* INITIAL_REQUEST */;
107
- coreClient.onBeforePaymentCreation(async ({ selectedRequirements }) => {
108
- const amount = tokenStringToNumber(selectedRequirements.amount);
109
- await checkBalance({
110
- server,
111
- address: account.address,
112
- amountNeeded: amount,
113
- message: (balance) => `This request costs ${amount} USDC. Your current balance is ${balance} USDC.`,
114
- flags
115
- });
116
- state = "payment_required" /* PAYMENT_REQUIRED */;
117
- });
118
- coreClient.onAfterPaymentCreation(async (ctx) => {
119
- state = "payment_created" /* PAYMENT_CREATED */;
120
- log.info("After payment creation", ctx);
121
- return Promise.resolve();
122
- });
123
- coreClient.onPaymentCreationFailure(async (ctx) => {
124
- state = "payment_failed" /* PAYMENT_FAILED */;
125
- log.info("Payment creation failure", ctx);
126
- return Promise.resolve();
127
- });
128
- const client = new x402HTTPClient(coreClient);
129
- const fetchWithPay = wrapFetchWithPayment(fetch, client);
130
- try {
131
- const response = await fetchWithPay(url, {
132
- method,
133
- body: body ? JSON.stringify(body) : void 0,
134
- headers: {
135
- ...body ? { "Content-Type": "application/json" } : {},
136
- ...headers
137
- }
138
- });
139
- if (!response.ok) {
140
- const errorData = await response.text();
141
- const errorResponse = {
142
- data: errorData,
143
- statusCode: response.status,
144
- state
145
- };
146
- if (response.status === 402) {
147
- return mcpError("Payment required", errorResponse);
148
- }
149
- return mcpError(
150
- response.statusText ?? "Request failed",
151
- errorResponse
152
- );
153
- }
154
- const getSettlement = () => {
155
- try {
156
- return client.getPaymentSettleResponse(
157
- (name) => response.headers.get(name)
158
- );
159
- } catch {
160
- return void 0;
161
- }
162
- };
163
- const settlement = getSettlement();
164
- return mcpSuccess({
165
- data: await response.text().catch(() => void 0),
166
- payment: settlement
167
- });
168
- } catch (err) {
169
- return mcpError(err, { state });
170
- }
171
- }
172
- );
173
- };
174
-
175
- // src/server/tools/auth.ts
176
- import { x402Client as x402Client2, x402HTTPClient as x402HTTPClient2 } from "@x402/core/client";
177
-
178
- // src/server/lib/x402/protocol.ts
179
- function isV1Response(pr) {
180
- if (!pr || typeof pr !== "object") return false;
181
- const obj = pr;
182
- if (obj.x402Version === 1) return true;
183
- const accepts = obj.accepts;
184
- if (Array.isArray(accepts) && accepts.length > 0) {
185
- return "maxAmountRequired" in accepts[0];
186
- }
187
- return false;
188
- }
189
- function normalizeV1Requirement(req) {
190
- if (!req.maxAmountRequired) {
191
- throw new Error("v1 requirement missing maxAmountRequired field");
192
- }
193
- return {
194
- scheme: req.scheme,
195
- network: req.network,
196
- amount: req.maxAmountRequired,
197
- asset: req.asset,
198
- payTo: req.payTo,
199
- maxTimeoutSeconds: req.maxTimeoutSeconds,
200
- extra: req.extra,
201
- resource: req.resource,
202
- description: req.description,
203
- mimeType: req.mimeType
204
- };
205
- }
206
- function normalizeV2Requirement(req) {
207
- if (!req.amount) {
208
- throw new Error("v2 requirement missing amount field");
209
- }
210
- return {
211
- scheme: req.scheme,
212
- network: req.network,
213
- amount: req.amount,
214
- asset: req.asset,
215
- payTo: req.payTo,
216
- maxTimeoutSeconds: req.maxTimeoutSeconds,
217
- extra: req.extra
218
- };
219
- }
220
- function normalizePaymentRequired(pr) {
221
- const version = pr.x402Version ?? 1;
222
- if (isV1Response(pr)) {
223
- const v1 = pr;
224
- return {
225
- x402Version: 1,
226
- error: v1.error,
227
- accepts: v1.accepts.map(normalizeV1Requirement)
228
- };
229
- }
230
- const v2 = pr;
231
- return {
232
- x402Version: version,
233
- error: v2.error,
234
- accepts: v2.accepts.map(normalizeV2Requirement),
235
- resource: v2.resource,
236
- extensions: v2.extensions
237
- };
238
- }
239
-
240
- // src/server/vendor/sign-in-with-x/solana.ts
241
- import { base58 } from "@scure/base";
242
- import nacl from "tweetnacl";
243
- function extractSolanaChainReference(chainId) {
244
- const [, reference] = chainId.split(":");
245
- return reference;
246
- }
247
- function formatSIWSMessage(info, address) {
248
- const lines = [
249
- `${info.domain} wants you to sign in with your Solana account:`,
250
- address,
251
- ""
252
- ];
253
- if (info.statement) {
254
- lines.push(info.statement, "");
255
- }
256
- lines.push(
257
- `URI: ${info.uri}`,
258
- `Version: ${info.version}`,
259
- `Chain ID: ${extractSolanaChainReference(info.chainId)}`,
260
- `Nonce: ${info.nonce}`,
261
- `Issued At: ${info.issuedAt}`
262
- );
263
- if (info.expirationTime) {
264
- lines.push(`Expiration Time: ${info.expirationTime}`);
265
- }
266
- if (info.notBefore) {
267
- lines.push(`Not Before: ${info.notBefore}`);
268
- }
269
- if (info.requestId) {
270
- lines.push(`Request ID: ${info.requestId}`);
271
- }
272
- if (info.resources && info.resources.length > 0) {
273
- lines.push("Resources:");
274
- for (const resource of info.resources) {
275
- lines.push(`- ${resource}`);
276
- }
277
- }
278
- return lines.join("\n");
279
- }
280
- function encodeBase58(bytes) {
281
- return base58.encode(bytes);
282
- }
283
-
284
- // src/server/vendor/sign-in-with-x/sign.ts
285
- function getEVMAddress(signer) {
286
- if (signer.account?.address) {
287
- return signer.account.address;
288
- }
289
- if (signer.address) {
290
- return signer.address;
291
- }
292
- throw new Error("EVM signer missing address");
293
- }
294
- function getSolanaAddress(signer) {
295
- const pk = signer.publicKey;
296
- return typeof pk === "string" ? pk : pk.toBase58();
297
- }
298
- async function signEVMMessage(message, signer) {
299
- if (signer.account) {
300
- return signer.signMessage({ message, account: signer.account });
301
- }
302
- return signer.signMessage({ message });
303
- }
304
- async function signSolanaMessage(message, signer) {
305
- const messageBytes = new TextEncoder().encode(message);
306
- const signatureBytes = await signer.signMessage(messageBytes);
307
- return encodeBase58(signatureBytes);
308
- }
309
-
310
- // src/server/vendor/sign-in-with-x/evm.ts
311
- import { verifyMessage } from "viem";
312
- import { SiweMessage } from "siwe";
313
- function extractEVMChainId(chainId) {
314
- const match = /^eip155:(\d+)$/.exec(chainId);
315
- if (!match) {
316
- throw new Error(
317
- `Invalid EVM chainId format: ${chainId}. Expected eip155:<number>`
318
- );
319
- }
320
- return parseInt(match[1], 10);
321
- }
322
- function formatSIWEMessage(info, address) {
323
- const numericChainId = extractEVMChainId(info.chainId);
324
- const siweMessage = new SiweMessage({
325
- domain: info.domain,
326
- address,
327
- statement: info.statement,
328
- uri: info.uri,
329
- version: info.version,
330
- chainId: numericChainId,
331
- nonce: info.nonce,
332
- issuedAt: info.issuedAt,
333
- expirationTime: info.expirationTime,
334
- notBefore: info.notBefore,
335
- requestId: info.requestId,
336
- resources: info.resources
337
- });
338
- return siweMessage.prepareMessage();
339
- }
340
-
341
- // src/server/vendor/sign-in-with-x/message.ts
342
- function createSIWxMessage(serverInfo, address) {
343
- if (serverInfo.chainId.startsWith("eip155:")) {
344
- return formatSIWEMessage(serverInfo, address);
345
- }
346
- if (serverInfo.chainId.startsWith("solana:")) {
347
- return formatSIWSMessage(serverInfo, address);
348
- }
349
- throw new Error(
350
- `Unsupported chain namespace: ${serverInfo.chainId}. Supported: eip155:* (EVM), solana:* (Solana)`
351
- );
352
- }
353
-
354
- // src/server/vendor/sign-in-with-x/client.ts
355
- async function createSIWxPayload(serverExtension, signer) {
356
- const isSolana = serverExtension.chainId.startsWith("solana:");
357
- const address = isSolana ? getSolanaAddress(signer) : getEVMAddress(signer);
358
- const message = createSIWxMessage(serverExtension, address);
359
- const signature = isSolana ? await signSolanaMessage(message, signer) : await signEVMMessage(message, signer);
360
- return {
361
- domain: serverExtension.domain,
362
- address,
363
- statement: serverExtension.statement,
364
- uri: serverExtension.uri,
365
- version: serverExtension.version,
366
- chainId: serverExtension.chainId,
367
- type: serverExtension.type,
368
- nonce: serverExtension.nonce,
369
- issuedAt: serverExtension.issuedAt,
370
- expirationTime: serverExtension.expirationTime,
371
- notBefore: serverExtension.notBefore,
372
- requestId: serverExtension.requestId,
373
- resources: serverExtension.resources,
374
- signatureScheme: serverExtension.signatureScheme,
375
- signature
376
- };
377
- }
378
-
379
- // src/server/vendor/sign-in-with-x/encode.ts
380
- import { safeBase64Encode } from "@x402/core/utils";
381
- function encodeSIWxHeader(payload) {
382
- return safeBase64Encode(JSON.stringify(payload));
383
- }
384
-
385
- // src/server/tools/auth.ts
386
- var registerAuthTools = ({ server, account }) => {
387
- server.registerTool(
388
- "authed_call",
389
- {
390
- description: "Make a request to a SIWX-protected endpoint. Handles auth flow automatically: detects SIWX requirement from 402 response, signs proof with server-provided challenge, retries.",
391
- inputSchema: requestWithHeadersSchema
392
- },
393
- async ({ url, method, body, headers }) => {
394
- try {
395
- const httpClient = new x402HTTPClient2(new x402Client2());
396
- const firstResponse = await fetch(url, {
397
- method,
398
- headers: {
399
- "Content-Type": "application/json",
400
- ...headers
401
- },
402
- body: body ? JSON.stringify(body) : void 0
403
- });
404
- if (firstResponse.status !== 402) {
405
- const responseHeaders2 = Object.fromEntries(
406
- firstResponse.headers.entries()
407
- );
408
- if (firstResponse.ok) {
409
- let data2;
410
- const contentType2 = firstResponse.headers.get("content-type");
411
- if (contentType2?.includes("application/json")) {
412
- data2 = await firstResponse.json();
413
- } else {
414
- data2 = await firstResponse.text();
415
- }
416
- return mcpSuccess({
417
- statusCode: firstResponse.status,
418
- headers: responseHeaders2,
419
- data: data2
420
- });
421
- }
422
- let errorBody;
423
- try {
424
- errorBody = await firstResponse.json();
425
- } catch {
426
- errorBody = await firstResponse.text();
427
- }
428
- return mcpError(`HTTP ${firstResponse.status}`, {
429
- statusCode: firstResponse.status,
430
- headers: responseHeaders2,
431
- body: errorBody
432
- });
433
- }
434
- let rawBody;
435
- try {
436
- rawBody = await firstResponse.clone().json();
437
- } catch {
438
- rawBody = void 0;
439
- }
440
- const rawPaymentRequired = httpClient.getPaymentRequiredResponse(
441
- (name) => firstResponse.headers.get(name),
442
- rawBody
443
- );
444
- const paymentRequired = normalizePaymentRequired(rawPaymentRequired);
445
- const siwxExtension = paymentRequired.extensions?.["sign-in-with-x"];
446
- if (!siwxExtension?.info) {
447
- return mcpError(
448
- "Endpoint returned 402 but no sign-in-with-x extension found",
449
- {
450
- statusCode: 402,
451
- x402Version: paymentRequired.x402Version,
452
- extensions: Object.keys(paymentRequired.extensions ?? {}),
453
- hint: "This endpoint may require payment instead of authentication. Use execute_call for paid requests."
454
- }
455
- );
456
- }
457
- const serverInfo = siwxExtension.info;
458
- const requiredFields = [
459
- "domain",
460
- "uri",
461
- "version",
462
- "chainId",
463
- "nonce",
464
- "issuedAt"
465
- ];
466
- const missingFields = requiredFields.filter(
467
- (f) => !serverInfo[f]
468
- );
469
- if (missingFields.length > 0) {
470
- return mcpError(
471
- "Invalid sign-in-with-x extension: missing required fields",
472
- {
473
- missingFields,
474
- receivedInfo: serverInfo
475
- }
476
- );
477
- }
478
- if (serverInfo.chainId.startsWith("solana:")) {
479
- return mcpError("Solana authentication not supported", {
480
- chainId: serverInfo.chainId,
481
- hint: "This endpoint requires a Solana wallet. The MCP server currently only supports EVM wallets."
482
- });
483
- }
484
- const payload = await createSIWxPayload(serverInfo, account);
485
- const siwxHeader = encodeSIWxHeader(payload);
486
- const authedResponse = await fetch(url, {
487
- method,
488
- headers: {
489
- "Content-Type": "application/json",
490
- "SIGN-IN-WITH-X": siwxHeader,
491
- ...headers
492
- },
493
- body: body ? JSON.stringify(body) : void 0
494
- });
495
- const responseHeaders = Object.fromEntries(
496
- authedResponse.headers.entries()
497
- );
498
- if (!authedResponse.ok) {
499
- let errorBody;
500
- try {
501
- errorBody = await authedResponse.json();
502
- } catch {
503
- errorBody = await authedResponse.text();
504
- }
505
- return mcpError(
506
- `HTTP ${authedResponse.status} after authentication`,
507
- {
508
- statusCode: authedResponse.status,
509
- headers: responseHeaders,
510
- body: errorBody,
511
- authAddress: account.address
512
- }
513
- );
514
- }
515
- let data;
516
- const contentType = authedResponse.headers.get("content-type");
517
- if (contentType?.includes("application/json")) {
518
- data = await authedResponse.json();
519
- } else {
520
- data = await authedResponse.text();
521
- }
522
- return mcpSuccess({
523
- statusCode: authedResponse.status,
524
- headers: responseHeaders,
525
- data,
526
- authentication: {
527
- address: account.address,
528
- domain: serverInfo.domain,
529
- chainId: serverInfo.chainId
530
- }
531
- });
532
- } catch (err) {
533
- return mcpError(err, { tool: "authed_call", url });
534
- }
535
- }
536
- );
537
- };
538
-
539
- // src/server/tools/wallet.ts
540
- var registerWalletTools = ({
541
- server,
542
- account: { address }
543
- }) => {
544
- server.registerTool(
545
- "check_balance",
546
- {
547
- description: "Check wallet address and USDC balance. Creates wallet if needed."
548
- },
549
- async () => {
550
- const balance = await getUSDCBalance({
551
- address
552
- });
553
- return mcpSuccess({
554
- address,
555
- network: DEFAULT_NETWORK,
556
- networkName: getChainName(DEFAULT_NETWORK),
557
- usdcBalance: balance,
558
- balanceFormatted: balance.toString(),
559
- isNewWallet: balance === 0
560
- });
561
- }
562
- );
563
- server.registerTool(
564
- "get_wallet_address",
565
- {
566
- description: "Get the wallet address."
567
- },
568
- () => mcpSuccess({ address })
569
- );
570
- };
571
-
572
- // src/server/tools/check-endpoint-schema.ts
573
- import { x402Client as x402Client3, x402HTTPClient as x402HTTPClient3 } from "@x402/core/client";
574
-
575
- // src/server/lib/x402/get-route-details.ts
576
- var getRouteDetails = (paymentRequired) => {
577
- const { accepts, extensions, resource } = paymentRequired;
578
- return {
579
- ...resource,
580
- schema: getSchema(extensions),
581
- paymentMethods: accepts.map((accept) => ({
582
- price: tokenStringToNumber(accept.amount),
583
- network: accept.network,
584
- asset: accept.asset
585
- }))
586
- };
587
- };
588
- var getSchema = (extensions) => {
589
- const { bazaar } = extensions ?? {};
590
- if (!bazaar) {
591
- return void 0;
592
- }
593
- const { schema } = bazaar;
594
- return schema.properties.input;
595
- };
596
-
597
- // src/server/tools/check-endpoint-schema.ts
598
- var registerCheckX402EndpointTool = ({ server }) => {
599
- server.registerTool(
600
- "check_x402_endpoint",
601
- {
602
- description: "Check if an endpoint is x402-protected and get pricing options, schema, and auth requirements (if applicable).",
603
- inputSchema: requestSchema
604
- },
605
- async ({ url, method, body }) => {
606
- try {
607
- log.info("Querying endpoint", { url, method, body });
608
- const response = await fetch(url, {
609
- method,
610
- body: body ? typeof body === "string" ? body : JSON.stringify(body) : void 0,
611
- headers: {
612
- "Content-Type": "application/json"
613
- }
614
- });
615
- const bodyText = await response.text().catch(() => void 0);
616
- if (response.status !== 402) {
617
- return mcpSuccess({
618
- data: bodyText,
619
- statusCode: response.status,
620
- requiresPayment: false
621
- });
622
- }
623
- const paymentRequired = new x402HTTPClient3(
624
- new x402Client3()
625
- ).getPaymentRequiredResponse(
626
- (name) => response.headers.get(name),
627
- JSON.parse(bodyText ?? "{}")
628
- );
629
- const routeDetails = getRouteDetails(paymentRequired);
630
- return mcpSuccess({
631
- requiresPayment: true,
632
- statusCode: response.status,
633
- routeDetails
634
- });
635
- } catch (err) {
636
- return mcpError(err, { tool: "query_endpoint", url });
637
- }
638
- }
639
- );
640
- };
641
-
642
- // src/server/resources/origins.ts
643
- import { x402HTTPClient as x402HTTPClient4 } from "@x402/core/client";
644
- import { x402Client as x402Client4 } from "@x402/core/client";
645
-
646
- // src/server/resources/_lib.ts
647
- var getWebPageMetadata = async (url) => {
648
- try {
649
- const response = await fetch(url);
650
- if (!response.ok) {
651
- return null;
652
- }
653
- const html = await response.text();
654
- const titleMatch = /<title[^>]*>([\s\S]*?)<\/title>/i.exec(html);
655
- const title = titleMatch ? titleMatch[1].trim().replace(/\s+/g, " ") : null;
656
- let descriptionMatch = /<meta\s+name=["']description["']\s+content=["']([^"']*)["']/i.exec(html);
657
- descriptionMatch ??= /<meta\s+property=["']og:description["']\s+content=["']([^"']*)["']/i.exec(
658
- html
659
- );
660
- descriptionMatch ??= /<meta\s+content=["']([^"']*)["']\s+name=["']description["']/i.exec(html);
661
- descriptionMatch ??= /<meta\s+content=["']([^"']*)["']\s+property=["']og:description["']/i.exec(
662
- html
663
- );
664
- const description = descriptionMatch ? descriptionMatch[1].trim().replace(/\s+/g, " ") : null;
665
- return {
666
- title,
667
- description
668
- };
669
- } catch (error) {
670
- throw new Error(
671
- `Failed to fetch web page metadata: ${error instanceof Error ? error.message : String(error)}`
672
- );
673
- }
674
- };
675
-
676
- // src/server/resources/origins.ts
677
- var origins = ["enrichx402.com"];
678
- var registerOrigins = async ({ server }) => {
679
- await Promise.all(
680
- origins.map(async (origin) => {
681
- const metadata = await getWebPageMetadata(`https://${origin}`);
682
- server.registerResource(
683
- origin,
684
- `api://${origin}`,
685
- {
686
- title: metadata?.title ?? origin,
687
- description: metadata?.description ?? "",
688
- mimeType: "application/json"
689
- },
690
- async (uri) => {
691
- const response = await fetch(
692
- `${uri.toString().replace("api://", "https://")}/.well-known/x402`
693
- ).then((response2) => response2.json());
694
- const resources = await Promise.all(
695
- response.resources.map(async (resource) => {
696
- const resourceResponse = await getResourceResponse(
697
- resource,
698
- await fetch(resource, {
699
- method: "POST",
700
- headers: {
701
- "Content-Type": "application/json"
702
- }
703
- })
704
- );
705
- if (resourceResponse) {
706
- return resourceResponse;
707
- }
708
- const getResponse = await getResourceResponse(
709
- resource,
710
- await fetch(resource, {
711
- method: "GET"
712
- })
713
- );
714
- if (getResponse) {
715
- return getResponse;
716
- }
717
- console.error(`Failed to get resource response for ${resource}`);
718
- return null;
719
- })
720
- );
721
- return {
722
- contents: [
723
- {
724
- uri: origin,
725
- text: JSON.stringify({
726
- server: origin,
727
- name: metadata?.title,
728
- description: metadata?.description,
729
- resources: resources.filter(Boolean).map((resource) => {
730
- if (!resource) return null;
731
- const schema = getSchema(
732
- resource.paymentRequired?.extensions
733
- );
734
- return {
735
- url: resource.resource,
736
- schema,
737
- mimeType: resource.paymentRequired.resource.mimeType
738
- };
739
- })
740
- }),
741
- mimeType: "application/json"
742
- }
743
- ]
744
- };
745
- }
746
- );
747
- })
748
- );
749
- };
750
- var getResourceResponse = async (resource, response) => {
751
- const client = new x402HTTPClient4(new x402Client4());
752
- if (response.status === 402) {
753
- const paymentRequired = client.getPaymentRequiredResponse(
754
- (name) => response.headers.get(name),
755
- JSON.parse(await response.text())
756
- );
757
- return {
758
- paymentRequired,
759
- resource
760
- };
761
- }
762
- return null;
763
- };
764
-
765
- // src/server/tools/discover-resources.ts
766
- import { z } from "zod";
767
- import { x402Client as x402Client5, x402HTTPClient as x402HTTPClient5 } from "@x402/core/client";
768
- var DiscoveryDocumentSchema = z.object({
769
- version: z.number().refine((v) => v === 1, { message: "version must be 1" }),
770
- resources: z.array(z.url()),
771
- ownershipProofs: z.array(z.string()).optional(),
772
- instructions: z.string().optional()
773
- });
774
- async function lookupDnsTxtRecord(hostname) {
775
- const dnsQuery = `_x402.${hostname}`;
776
- log.debug(`Looking up DNS TXT record: ${dnsQuery}`);
777
- try {
778
- const response = await fetch(
779
- `https://cloudflare-dns.com/dns-query?name=${encodeURIComponent(
780
- dnsQuery
781
- )}&type=TXT`,
782
- {
783
- headers: { Accept: "application/dns-json" }
784
- }
785
- );
786
- if (!response.ok) {
787
- log.debug(`DNS lookup failed: HTTP ${response.status}`);
788
- return null;
789
- }
790
- const data = await response.json();
791
- if (!data.Answer || data.Answer.length === 0) {
792
- log.debug("No DNS TXT record found");
793
- return null;
794
- }
795
- const txtValue = data.Answer[0].data.replace(/^"|"$/g, "");
796
- log.debug(`Found DNS TXT record: ${txtValue}`);
797
- try {
798
- new URL(txtValue);
799
- return txtValue;
800
- } catch {
801
- log.debug(`DNS TXT value is not a valid URL: ${txtValue}`);
802
- return null;
803
- }
804
- } catch (err) {
805
- log.debug(
806
- `DNS lookup error: ${err instanceof Error ? err.message : String(err)}`
807
- );
808
- return null;
809
- }
810
- }
811
- async function fetchLlmsTxt(origin) {
812
- const llmsTxtUrl = `${origin}/llms.txt`;
813
- log.debug(`Fetching llms.txt from: ${llmsTxtUrl}`);
814
- try {
815
- const response = await fetch(llmsTxtUrl, {
816
- headers: { Accept: "text/plain" }
817
- });
818
- if (!response.ok) {
819
- if (response.status === 404) {
820
- return { found: false, error: "No llms.txt found" };
821
- }
822
- return { found: false, error: `HTTP ${response.status}` };
823
- }
824
- const content = await response.text();
825
- if (!content || content.trim().length === 0) {
826
- return { found: false, error: "llms.txt is empty" };
827
- }
828
- return { found: true, content };
829
- } catch (err) {
830
- return {
831
- found: false,
832
- error: `Network error: ${err instanceof Error ? err.message : String(err)}`
833
- };
834
- }
835
- }
836
- async function fetchDiscoveryFromUrl(url) {
837
- log.debug(`Fetching discovery document from: ${url}`);
838
- try {
839
- const response = await fetch(url, {
840
- headers: { Accept: "application/json" }
841
- });
842
- if (!response.ok) {
843
- if (response.status === 404) {
844
- return { found: false, error: `Not found at ${url}` };
845
- }
846
- return {
847
- found: false,
848
- error: `HTTP ${response.status}: ${await response.text()}`
849
- };
850
- }
851
- let rawData;
852
- try {
853
- rawData = await response.json();
854
- } catch {
855
- return {
856
- found: false,
857
- error: "Failed to parse discovery document as JSON"
858
- };
859
- }
860
- const parsed = DiscoveryDocumentSchema.safeParse(rawData);
861
- if (!parsed.success) {
862
- return {
863
- found: false,
864
- error: `Invalid discovery document: ${parsed.error.issues.map((e) => e.message).join(", ")}`,
865
- rawResponse: rawData
866
- };
867
- }
868
- return { found: true, document: parsed.data };
869
- } catch (err) {
870
- return {
871
- found: false,
872
- error: `Network error: ${err instanceof Error ? err.message : String(err)}`
873
- };
874
- }
875
- }
876
- async function fetchDiscoveryDocument(origin) {
877
- const attemptedSources = [];
878
- const hostname = getHostname(origin);
879
- const wellKnownUrl = `${origin}/.well-known/x402`;
880
- attemptedSources.push(wellKnownUrl);
881
- const wellKnownResult = await fetchDiscoveryFromUrl(wellKnownUrl);
882
- if (wellKnownResult.found && wellKnownResult.document) {
883
- return {
884
- found: true,
885
- source: "well-known",
886
- document: wellKnownResult.document,
887
- attemptedSources
888
- };
889
- }
890
- attemptedSources.push(`DNS TXT _x402.${hostname}`);
891
- const dnsUrl = await lookupDnsTxtRecord(hostname);
892
- if (dnsUrl) {
893
- attemptedSources.push(dnsUrl);
894
- const dnsResult = await fetchDiscoveryFromUrl(dnsUrl);
895
- if (dnsResult.found && dnsResult.document) {
896
- return {
897
- found: true,
898
- source: "dns-txt",
899
- document: dnsResult.document,
900
- attemptedSources
901
- };
902
- }
903
- }
904
- attemptedSources.push(`${origin}/llms.txt`);
905
- const llmsResult = await fetchLlmsTxt(origin);
906
- if (llmsResult.found && llmsResult.content) {
907
- return {
908
- found: true,
909
- source: "llms-txt",
910
- llmsTxtContent: llmsResult.content,
911
- attemptedSources
912
- };
913
- }
914
- return {
915
- found: false,
916
- error: "No discovery document found. Tried: .well-known/x402, DNS TXT record, llms.txt",
917
- attemptedSources
918
- };
919
- }
920
- async function queryResource(url) {
921
- log.debug(`Querying resource: ${url}`);
922
- try {
923
- const result = await fetch(url, { method: "GET" });
924
- if (!result.ok) {
925
- return {
926
- url,
927
- isX402Endpoint: false,
928
- error: result.statusText ?? "Failed to query endpoint"
929
- };
930
- }
931
- if (result.status !== 402) {
932
- return {
933
- url,
934
- isX402Endpoint: false
935
- };
936
- }
937
- const pr = new x402HTTPClient5(new x402Client5()).getPaymentRequiredResponse(
938
- (name) => result.headers.get(name),
939
- JSON.parse(await result.text())
940
- );
941
- const firstReq = pr.accepts[0];
942
- const resource = {
943
- url,
944
- isX402Endpoint: true,
945
- x402Version: pr.x402Version,
946
- price: tokenStringToNumber(firstReq.amount),
947
- priceRaw: firstReq.amount,
948
- network: firstReq.network,
949
- networkName: getChainName(firstReq.network)
950
- };
951
- if (pr.extensions?.bazaar) {
952
- const bazaar = pr.extensions.bazaar;
953
- resource.bazaar = { info: bazaar.info, schema: bazaar.schema };
954
- const info = bazaar.info;
955
- if (info?.description) {
956
- resource.description = info.description;
957
- }
958
- }
959
- if (pr.extensions?.["sign-in-with-x"]) {
960
- const siwx = pr.extensions["sign-in-with-x"];
961
- resource.signInWithX = { required: true, info: siwx.info };
962
- }
963
- return resource;
964
- } catch (err) {
965
- return {
966
- url,
967
- isX402Endpoint: false,
968
- error: err instanceof Error ? err.message : String(err)
969
- };
970
- }
971
- }
972
- function registerDiscoveryTools(server) {
973
- server.registerTool(
974
- "discover_resources",
975
- {
976
- description: `Discover x402-protected resources from an origin. Fetches the /.well-known/x402 discovery document and optionally tests each resource to get pricing and requirements.
977
-
978
- Known default origins with resource packs. Discover if more needed:
979
- - https://enrichx402.com -> People + Org search, Google Maps (places + locations), grok twitter search, exa web search, clado linkedin data, firecrawl web scrape
980
- - https://stablestudio.io -> generate images / videos
981
- `,
982
- inputSchema: {
983
- url: z.url().describe(
984
- "The origin URL or any URL on the origin to discover resources from"
985
- ),
986
- testResources: z.boolean().default(false).describe(
987
- "Whether to query each discovered resource for full pricing/schema info (default: false - just return URLs from discovery doc)"
988
- ),
989
- concurrency: z.number().int().min(1).max(10).default(5).describe(
990
- "Max concurrent requests when querying resources (default: 5)"
991
- )
992
- }
993
- },
994
- async ({ url, testResources, concurrency }) => {
995
- try {
996
- const origin = getOrigin(url);
997
- log.info(`Discovering resources for origin: ${origin}`);
998
- const discoveryResult = await fetchDiscoveryDocument(origin);
999
- if (discoveryResult.found && discoveryResult.source === "llms-txt") {
1000
- return mcpSuccess({
1001
- found: true,
1002
- origin,
1003
- source: "llms-txt",
1004
- usage: "Found llms.txt but no structured x402 discovery document. The content below may contain information about x402 resources. Parse it to find relevant endpoints.",
1005
- llmsTxtContent: discoveryResult.llmsTxtContent,
1006
- attemptedSources: discoveryResult.attemptedSources,
1007
- resources: []
1008
- });
1009
- }
1010
- if (!discoveryResult.found || !discoveryResult.document) {
1011
- return mcpSuccess({
1012
- found: false,
1013
- origin,
1014
- error: discoveryResult.error,
1015
- attemptedSources: discoveryResult.attemptedSources,
1016
- rawResponse: discoveryResult.rawResponse
1017
- });
1018
- }
1019
- const doc = discoveryResult.document;
1020
- const result = {
1021
- found: true,
1022
- origin,
1023
- source: discoveryResult.source,
1024
- instructions: doc.instructions,
1025
- usage: "Use query_endpoint to get full pricing/requirements for a resource. Use execute_call (for payment) or authed_call (for SIWX auth) to call it.",
1026
- resources: []
1027
- };
1028
- if (!testResources) {
1029
- result.resources = doc.resources.map((resourceUrl) => ({
1030
- url: resourceUrl
1031
- }));
1032
- return mcpSuccess(result);
1033
- }
1034
- const resourceUrls = doc.resources;
1035
- const allResources = [];
1036
- for (let i = 0; i < resourceUrls.length; i += concurrency) {
1037
- const batch = resourceUrls.slice(i, i + concurrency);
1038
- const batchResults = await Promise.all(
1039
- batch.map((resourceUrl) => queryResource(resourceUrl))
1040
- );
1041
- allResources.push(...batchResults);
1042
- }
1043
- result.resources = allResources;
1044
- return mcpSuccess(result);
1045
- } catch (err) {
1046
- return mcpError(err, { tool: "discover_resources", url });
1047
- }
1048
- }
1049
- );
1050
- }
1051
- function getOrigin(urlString) {
1052
- try {
1053
- return new URL(urlString).origin;
1054
- } catch {
1055
- return urlString;
1056
- }
1057
- }
1058
- function getHostname(origin) {
1059
- try {
1060
- return new URL(origin).hostname;
1061
- } catch {
1062
- return origin;
1063
- }
1064
- }
1065
-
1066
- // src/server/index.ts
1067
- var startServer = async (flags) => {
1068
- log.info("Starting x402scan-mcp...");
1069
- const { account } = await getWallet();
1070
- const server = new McpServer(
1071
- {
1072
- name: "@x402scan/mcp",
1073
- version: "0.0.1",
1074
- websiteUrl: "https://x402scan.com/mcp",
1075
- icons: [{ src: "https://x402scan.com/logo.svg" }]
1076
- },
1077
- {
1078
- capabilities: {
1079
- resources: {
1080
- subscribe: true,
1081
- listChanged: true
1082
- }
1083
- }
1084
- }
1085
- );
1086
- const props = {
1087
- server,
1088
- account,
1089
- flags
1090
- };
1091
- registerFetchX402ResourceTool(props);
1092
- registerAuthTools(props);
1093
- registerWalletTools(props);
1094
- registerCheckX402EndpointTool(props);
1095
- registerDiscoveryTools(server);
1096
- await registerOrigins({ server, flags });
1097
- const transport = new StdioServerTransport();
1098
- await server.connect(transport);
1099
- const shutdown = async () => {
1100
- log.info("Shutting down...");
1101
- await server.close();
1102
- process.exit(0);
1103
- };
1104
- process.on("SIGINT", () => void shutdown());
1105
- process.on("SIGTERM", () => void shutdown());
1106
- };
1107
- export {
1108
- startServer
1109
- };