@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 +115 -0
- package/dist/bundle/index.cjs +110336 -0
- package/dist/chunk-FFXFOKKF.js +267 -0
- package/dist/chunk-KD3GRRAV.js +43 -0
- package/dist/fund-743A34XB.js +19 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +41 -0
- package/dist/install-QDUD2KOS.js +646 -0
- package/dist/server-CD4OSIVL.js +1109 -0
- package/package.json +78 -0
|
@@ -0,0 +1,1109 @@
|
|
|
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
|
+
};
|