@persistenceone/bridgekitty 0.3.0

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 (63) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +232 -0
  3. package/dist/backends/across.d.ts +10 -0
  4. package/dist/backends/across.js +285 -0
  5. package/dist/backends/debridge.d.ts +11 -0
  6. package/dist/backends/debridge.js +380 -0
  7. package/dist/backends/lifi.d.ts +19 -0
  8. package/dist/backends/lifi.js +295 -0
  9. package/dist/backends/persistence.d.ts +86 -0
  10. package/dist/backends/persistence.js +642 -0
  11. package/dist/backends/relay.d.ts +11 -0
  12. package/dist/backends/relay.js +292 -0
  13. package/dist/backends/squid.d.ts +31 -0
  14. package/dist/backends/squid.js +476 -0
  15. package/dist/backends/types.d.ts +125 -0
  16. package/dist/backends/types.js +11 -0
  17. package/dist/index.d.ts +2 -0
  18. package/dist/index.js +154 -0
  19. package/dist/routing/engine.d.ts +49 -0
  20. package/dist/routing/engine.js +336 -0
  21. package/dist/tools/check-status.d.ts +3 -0
  22. package/dist/tools/check-status.js +93 -0
  23. package/dist/tools/execute-bridge.d.ts +3 -0
  24. package/dist/tools/execute-bridge.js +428 -0
  25. package/dist/tools/get-chains.d.ts +3 -0
  26. package/dist/tools/get-chains.js +162 -0
  27. package/dist/tools/get-quote.d.ts +3 -0
  28. package/dist/tools/get-quote.js +534 -0
  29. package/dist/tools/get-tokens.d.ts +3 -0
  30. package/dist/tools/get-tokens.js +128 -0
  31. package/dist/tools/help.d.ts +2 -0
  32. package/dist/tools/help.js +204 -0
  33. package/dist/tools/multi-quote.d.ts +3 -0
  34. package/dist/tools/multi-quote.js +310 -0
  35. package/dist/tools/onboard.d.ts +3 -0
  36. package/dist/tools/onboard.js +218 -0
  37. package/dist/tools/wallet.d.ts +14 -0
  38. package/dist/tools/wallet.js +744 -0
  39. package/dist/tools/xprt-farm.d.ts +3 -0
  40. package/dist/tools/xprt-farm.js +1308 -0
  41. package/dist/tools/xprt-rewards.d.ts +2 -0
  42. package/dist/tools/xprt-rewards.js +177 -0
  43. package/dist/tools/xprt-staking.d.ts +2 -0
  44. package/dist/tools/xprt-staking.js +565 -0
  45. package/dist/utils/chains.d.ts +22 -0
  46. package/dist/utils/chains.js +154 -0
  47. package/dist/utils/circuit-breaker.d.ts +64 -0
  48. package/dist/utils/circuit-breaker.js +160 -0
  49. package/dist/utils/evm.d.ts +18 -0
  50. package/dist/utils/evm.js +46 -0
  51. package/dist/utils/fill-detector.d.ts +70 -0
  52. package/dist/utils/fill-detector.js +298 -0
  53. package/dist/utils/gas-estimator.d.ts +67 -0
  54. package/dist/utils/gas-estimator.js +340 -0
  55. package/dist/utils/sanitize-error.d.ts +23 -0
  56. package/dist/utils/sanitize-error.js +101 -0
  57. package/dist/utils/token-registry.d.ts +70 -0
  58. package/dist/utils/token-registry.js +669 -0
  59. package/dist/utils/tokens.d.ts +17 -0
  60. package/dist/utils/tokens.js +37 -0
  61. package/dist/utils/tx-simulator.d.ts +27 -0
  62. package/dist/utils/tx-simulator.js +105 -0
  63. package/package.json +75 -0
@@ -0,0 +1,428 @@
1
+ import { z } from "zod";
2
+ import { ethers } from "ethers";
3
+ import { simulateTransaction } from "../utils/tx-simulator.js";
4
+ import { getChainName, isSolanaChain } from "../utils/chains.js";
5
+ import { sanitizeError } from "../utils/sanitize-error.js";
6
+ import { getKey } from "./wallet.js";
7
+ import { getProvider } from "../utils/gas-estimator.js";
8
+ // H-3: Quote execution locking — prevent double-execution
9
+ const executingQuotes = new Set();
10
+ /** Default timeout for buildTransaction API calls (ms) */
11
+ const BUILD_TX_TIMEOUT_MS = Number(process.env.BRIDGEKITTY_TX_TIMEOUT_MS) || 30_000;
12
+ /**
13
+ * Race a promise against a timeout. Returns a clear error on timeout instead of hanging.
14
+ */
15
+ function withTimeout(promise, ms, label) {
16
+ return Promise.race([
17
+ promise,
18
+ new Promise((_, reject) => setTimeout(() => reject(new Error(`${label} timed out after ${ms}ms`)), ms)),
19
+ ]);
20
+ }
21
+ /** Best-effort chain explorer URL for a tx hash. */
22
+ function getExplorerTxUrl(chainIdOrQuoteData, txHash) {
23
+ const chainId = typeof chainIdOrQuoteData === "number"
24
+ ? chainIdOrQuoteData
25
+ : (chainIdOrQuoteData?.sourceChainId ?? chainIdOrQuoteData?.chainId ?? 0);
26
+ const explorers = {
27
+ 1: "https://etherscan.io/tx/",
28
+ 10: "https://optimistic.etherscan.io/tx/",
29
+ 56: "https://bscscan.com/tx/",
30
+ 137: "https://polygonscan.com/tx/",
31
+ 8453: "https://basescan.org/tx/",
32
+ 42161: "https://arbiscan.io/tx/",
33
+ 43114: "https://snowtrace.io/tx/",
34
+ 250: "https://ftmscan.com/tx/",
35
+ 324: "https://explorer.zksync.io/tx/",
36
+ 59144: "https://lineascan.build/tx/",
37
+ 534352: "https://scrollscan.com/tx/",
38
+ 81457: "https://blastscan.io/tx/",
39
+ };
40
+ const base = explorers[chainId];
41
+ return base ? `${base}${txHash}` : `chainId:${chainId}/tx/${txHash}`;
42
+ }
43
+ export function registerExecuteBridge(server, engine) {
44
+ server.tool("bridge_execute", "Get the unsigned transaction data to execute a cross-chain bridge transfer. " +
45
+ "Supports all providers: LI.FI, Squid Router, deBridge, Across, Relay, Persistence Interop. " +
46
+ "By default returns unsigned transaction(s) for the agent/user to sign and send. " +
47
+ "Set sign_and_send=true to sign and broadcast using the locally-stored wallet key (requires wallet_setup). " +
48
+ "Preconditions: Call bridge_get_quote first and pass the quoteId. Ensure sufficient gas on source chain. " +
49
+ "If an approval is needed (unsigned mode), send the approvalTransaction first, then the main transaction. " +
50
+ "After execution, use bridge_status with the returned trackingId to monitor progress.", {
51
+ quoteId: z
52
+ .string()
53
+ .describe("Quote ID from bridge_get_quote result"),
54
+ slippage: z
55
+ .number()
56
+ .default(0.005)
57
+ .describe("Max slippage tolerance (0.005 = 0.5%). Applied by backends during quoting; reserved for future per-execution override."),
58
+ sign_and_send: z
59
+ .boolean()
60
+ .default(false)
61
+ .describe("If true, sign and broadcast the transaction server-side using the wallet key from wallet_setup. Supports EVM chains and Persistence Interop. Solana falls back to unsigned."),
62
+ }, async (params) => {
63
+ let quote = engine.getCachedQuote(params.quoteId);
64
+ // Auto-refresh expired quotes using the original parameters
65
+ if (!quote) {
66
+ const cached = engine.getCachedQuoteWithExpiry(params.quoteId);
67
+ if (cached?.expired && cached.quote.quoteData) {
68
+ try {
69
+ const qd = cached.quote.quoteData;
70
+ const p = qd?.params;
71
+ if (p) {
72
+ console.warn("[bridge_execute] Quote expired, auto-refreshing...");
73
+ const freshQuotes = await engine.getQuotes({
74
+ fromChainId: p.srcChainId,
75
+ toChainId: p.dstChainId,
76
+ fromTokenAddress: p.srcChainTokenIn,
77
+ toTokenAddress: p.dstChainTokenOut,
78
+ amountRaw: p.srcChainTokenInAmount,
79
+ fromAddress: p.fromAddress,
80
+ toAddress: p.toAddress,
81
+ providers: [cached.quote.backendName],
82
+ preference: "cheapest",
83
+ });
84
+ if (freshQuotes.length > 0) {
85
+ quote = freshQuotes[0];
86
+ }
87
+ }
88
+ }
89
+ catch (err) {
90
+ console.error("[bridge_execute] Auto-refresh failed:", err.message);
91
+ }
92
+ }
93
+ }
94
+ if (!quote) {
95
+ return {
96
+ content: [
97
+ {
98
+ type: "text",
99
+ text: "Quote expired or not found. Please call bridge_get_quote again to get a fresh quote.",
100
+ },
101
+ ],
102
+ };
103
+ }
104
+ const backend = engine.getBackend(quote.backendName);
105
+ if (!backend) {
106
+ return {
107
+ content: [
108
+ {
109
+ type: "text",
110
+ text: `Backend '${quote.backendName}' not available. Known backends: ${engine.getAllBackends().map((b) => b.name).join(", ")}`,
111
+ },
112
+ ],
113
+ };
114
+ }
115
+ // H-3: Prevent double-execution of the same quote
116
+ if (executingQuotes.has(params.quoteId)) {
117
+ return {
118
+ content: [
119
+ {
120
+ type: "text",
121
+ text: "This quote is already being executed. Please wait for the current execution to complete.",
122
+ },
123
+ ],
124
+ isError: true,
125
+ };
126
+ }
127
+ executingQuotes.add(params.quoteId);
128
+ try {
129
+ // MEDIUM-004: Pre-build expiry check — reject quotes that would expire during building
130
+ const BUILD_TIMEOUT_MS = 15_000;
131
+ if (quote.expiresAt && quote.expiresAt <= Date.now() + BUILD_TIMEOUT_MS) {
132
+ return {
133
+ content: [
134
+ {
135
+ type: "text",
136
+ text: "Quote is about to expire (or already expired) and may not survive the build step. Please call bridge_get_quote again to get a fresh quote.",
137
+ },
138
+ ],
139
+ };
140
+ }
141
+ // ── sign_and_send flow ──────────────────────────────────────────
142
+ if (params.sign_and_send) {
143
+ const privateKey = getKey("privateKey");
144
+ if (!privateKey) {
145
+ return {
146
+ content: [{
147
+ type: "text",
148
+ text: "No wallet key found. Run wallet_setup first to generate or import a wallet before using sign_and_send.",
149
+ }],
150
+ isError: true,
151
+ };
152
+ }
153
+ // Persistence Interop (EIP-712): use signAndExecute directly
154
+ if (quote.backendName === "persistence") {
155
+ const persistenceBackend = backend;
156
+ const signer = new ethers.Wallet(privateKey);
157
+ const result = await persistenceBackend.signAndExecute(quote, signer);
158
+ return {
159
+ content: [{
160
+ type: "text",
161
+ text: JSON.stringify({
162
+ status: "sent",
163
+ provider: quote.provider,
164
+ txHash: result.txHash,
165
+ orderId: result.orderId,
166
+ trackingId: result.trackingId,
167
+ explorerUrl: getExplorerTxUrl(quote.quoteData, result.txHash),
168
+ instructions: "Transaction signed and sent. Use bridge_status with the trackingId to monitor progress.",
169
+ }, null, 2),
170
+ }],
171
+ };
172
+ }
173
+ // Solana: not yet supported for sign_and_send — fall through to unsigned flow
174
+ if (isSolanaChain(quote.quoteData && typeof quote.quoteData === "object" && "srcChainId" in quote.quoteData ? quote.quoteData.srcChainId : 0)) {
175
+ // Fall through to the unsigned flow below with a note
176
+ const txRequest = await withTimeout(backend.buildTransaction(quote), BUILD_TX_TIMEOUT_MS, `buildTransaction (${backend.name})`);
177
+ if (txRequest.solanaTransaction) {
178
+ const response = {
179
+ provider: txRequest.provider,
180
+ trackingId: txRequest.trackingId,
181
+ slippage: params.slippage,
182
+ transaction: {
183
+ type: "solana",
184
+ serializedTx: txRequest.solanaTransaction.serializedTx,
185
+ chainId: txRequest.chainId,
186
+ },
187
+ note: "sign_and_send is not yet supported for Solana transactions. Returning unsigned transaction instead.",
188
+ instructions: "This is a Solana transaction. Sign and send it using a Solana wallet. " +
189
+ "The serializedTx is hex-encoded (0x-prefixed) — decode with Buffer.from(data.slice(2), 'hex'), " +
190
+ "then deserialize as a VersionedTransaction. IMPORTANT: replace the recentBlockhash with a " +
191
+ "fresh one from getLatestBlockhash() before signing, as the embedded blockhash may be stale. " +
192
+ "After sending, use bridge_status with the trackingId to monitor progress.",
193
+ };
194
+ return {
195
+ content: [{ type: "text", text: JSON.stringify(response, null, 2) }],
196
+ };
197
+ }
198
+ }
199
+ // Standard EVM flow: sign & send approval + main tx
200
+ const txRequest = await withTimeout(backend.buildTransaction(quote), BUILD_TX_TIMEOUT_MS, `buildTransaction (${backend.name})`);
201
+ const signer = new ethers.Wallet(privateKey);
202
+ const provider = await getProvider(txRequest.chainId);
203
+ const connectedSigner = signer.connect(provider);
204
+ // Handle approval tx if present
205
+ if (txRequest.approvalTx) {
206
+ const approvalResponse = await connectedSigner.sendTransaction({
207
+ to: txRequest.approvalTx.to,
208
+ data: txRequest.approvalTx.data,
209
+ value: txRequest.approvalTx.value,
210
+ });
211
+ await approvalResponse.wait();
212
+ // If backend needs post-approval rebuild, re-fetch the main tx
213
+ if (txRequest.needsPostApprovalBuild) {
214
+ const freshTx = await withTimeout(backend.buildTransaction(quote), BUILD_TX_TIMEOUT_MS, `buildTransaction post-approval (${backend.name})`);
215
+ txRequest.to = freshTx.to;
216
+ txRequest.data = freshTx.data;
217
+ txRequest.value = freshTx.value;
218
+ txRequest.gasLimit = freshTx.gasLimit;
219
+ }
220
+ }
221
+ // Simulate before sending
222
+ const sim = await simulateTransaction(txRequest.chainId, {
223
+ to: txRequest.to,
224
+ data: txRequest.data,
225
+ value: txRequest.value,
226
+ from: connectedSigner.address,
227
+ });
228
+ if (!sim.success) {
229
+ return {
230
+ content: [{
231
+ type: "text",
232
+ text: JSON.stringify({
233
+ error: "Transaction simulation failed",
234
+ message: sim.error,
235
+ advice: "The transaction would likely revert on-chain. Please get a fresh quote and try again.",
236
+ provider: txRequest.provider,
237
+ }, null, 2),
238
+ }],
239
+ isError: true,
240
+ };
241
+ }
242
+ // Send the main transaction
243
+ const txResponse = await connectedSigner.sendTransaction({
244
+ to: txRequest.to,
245
+ data: txRequest.data,
246
+ value: txRequest.value,
247
+ ...(txRequest.gasLimit ? { gasLimit: txRequest.gasLimit } : {}),
248
+ ...(sim.estimatedGas ? { gasLimit: sim.estimatedGas } : {}),
249
+ });
250
+ const receipt = await txResponse.wait();
251
+ return {
252
+ content: [{
253
+ type: "text",
254
+ text: JSON.stringify({
255
+ status: "sent",
256
+ provider: txRequest.provider,
257
+ txHash: txResponse.hash,
258
+ blockNumber: receipt?.blockNumber,
259
+ trackingId: txRequest.trackingId,
260
+ explorerUrl: getExplorerTxUrl(txRequest.chainId, txResponse.hash),
261
+ instructions: "Transaction signed and sent. Use bridge_status with the trackingId to monitor progress.",
262
+ }, null, 2),
263
+ }],
264
+ };
265
+ }
266
+ // ── Unsigned flow (default) ─────────────────────────────────────
267
+ const txRequest = await withTimeout(backend.buildTransaction(quote), BUILD_TX_TIMEOUT_MS, `buildTransaction (${backend.name})`);
268
+ // EIP-712 flow (e.g. Persistence Interop): skip on-chain simulation,
269
+ // return the typed data for the agent to sign externally.
270
+ if (txRequest.eip712) {
271
+ const response = {
272
+ provider: txRequest.provider,
273
+ trackingId: txRequest.trackingId,
274
+ slippage: params.slippage,
275
+ signingRequest: {
276
+ type: "eip712",
277
+ domain: txRequest.eip712.domain,
278
+ types: txRequest.eip712.types,
279
+ value: txRequest.eip712.value,
280
+ description: txRequest.eip712.description,
281
+ },
282
+ instructions: "Three steps required: " +
283
+ "(1) Send the approvalTransaction to approve the token for Permit2 (exact amount, not unlimited). " +
284
+ "(2) Sign the EIP-712 message in signingRequest with your wallet (eth_signTypedData_v4). " +
285
+ "(3) Call the settlement contract's initiate() with the order struct and your signature. " +
286
+ "Alternatively, use the xprt_farm_prepare / xprt_farm_start tools for automated server-side execution, " +
287
+ "or pass sign_and_send=true to handle all steps automatically.",
288
+ };
289
+ if (txRequest.approvalTx) {
290
+ response.approvalTransaction = {
291
+ to: txRequest.approvalTx.to,
292
+ data: txRequest.approvalTx.data,
293
+ value: txRequest.approvalTx.value,
294
+ chainId: txRequest.approvalTx.chainId,
295
+ };
296
+ response.approvalNote =
297
+ "Approval is for the EXACT bridge amount only — not unlimited. " +
298
+ "A new approval is needed for each bridge transaction.";
299
+ }
300
+ return {
301
+ content: [{ type: "text", text: JSON.stringify(response, null, 2) }],
302
+ };
303
+ }
304
+ // Solana transaction flow: return serialized tx for signing
305
+ if (txRequest.solanaTransaction) {
306
+ const response = {
307
+ provider: txRequest.provider,
308
+ trackingId: txRequest.trackingId,
309
+ slippage: params.slippage,
310
+ transaction: {
311
+ type: "solana",
312
+ serializedTx: txRequest.solanaTransaction.serializedTx,
313
+ chainId: txRequest.chainId,
314
+ },
315
+ instructions: "This is a Solana transaction. Sign and send it using a Solana wallet. " +
316
+ "The serializedTx is hex-encoded (0x-prefixed) — decode with Buffer.from(data.slice(2), 'hex'), " +
317
+ "then deserialize as a VersionedTransaction. IMPORTANT: replace the recentBlockhash with a " +
318
+ "fresh one from getLatestBlockhash() before signing, as the embedded blockhash may be stale. " +
319
+ "After sending, use bridge_status with the trackingId to monitor progress.",
320
+ };
321
+ return {
322
+ content: [{ type: "text", text: JSON.stringify(response, null, 2) }],
323
+ };
324
+ }
325
+ // Simulate the main transaction to verify it won't revert.
326
+ // Skip simulation when an approval tx is pending — the main tx would
327
+ // naturally revert with "transfer amount exceeds allowance" until the
328
+ // user has sent the approval on-chain.
329
+ const warnings = [];
330
+ if (!txRequest.approvalTx) {
331
+ const simulation = await simulateTransaction(txRequest.chainId, {
332
+ to: txRequest.to,
333
+ data: txRequest.data,
334
+ value: txRequest.value,
335
+ });
336
+ if (!simulation.success) {
337
+ return {
338
+ content: [
339
+ {
340
+ type: "text",
341
+ text: JSON.stringify({
342
+ error: "Transaction simulation failed",
343
+ message: simulation.error,
344
+ advice: "The transaction would likely revert on-chain. Please get a fresh quote and try again.",
345
+ provider: txRequest.provider,
346
+ }, null, 2),
347
+ },
348
+ ],
349
+ isError: true,
350
+ };
351
+ }
352
+ if (simulation.warning) {
353
+ warnings.push(simulation.warning);
354
+ }
355
+ if (simulation.estimatedGas) {
356
+ txRequest.gasLimit = txRequest.gasLimit ?? simulation.estimatedGas;
357
+ }
358
+ }
359
+ const response = {
360
+ provider: txRequest.provider,
361
+ trackingId: txRequest.trackingId,
362
+ slippage: params.slippage,
363
+ transaction: {
364
+ to: txRequest.to,
365
+ data: txRequest.data,
366
+ value: txRequest.value,
367
+ chainId: txRequest.chainId,
368
+ ...(txRequest.gasLimit ? { gasLimit: txRequest.gasLimit } : {}),
369
+ },
370
+ instructions: "Sign and send this transaction to initiate the bridge transfer.",
371
+ };
372
+ if (txRequest.approvalTx) {
373
+ response.approvalTransaction = {
374
+ to: txRequest.approvalTx.to,
375
+ data: txRequest.approvalTx.data,
376
+ value: txRequest.approvalTx.value,
377
+ chainId: txRequest.approvalTx.chainId,
378
+ };
379
+ if (txRequest.needsPostApprovalBuild) {
380
+ response.instructions =
381
+ "Two-phase execution needed: (1) Send the approvalTransaction first. (2) AFTER approval confirms, " +
382
+ "call bridge_execute again with the same quoteId — the bridge tx will be re-fetched with the correct nonce.";
383
+ response.needsPostApprovalBuild = true;
384
+ }
385
+ else {
386
+ response.instructions =
387
+ "Two transactions needed: (1) Send the approvalTransaction first to approve token spending (exact amount, not unlimited). (2) Then send the main transaction to initiate the bridge.";
388
+ }
389
+ response.approvalNote =
390
+ "The approval is for the EXACT bridge amount only — not an unlimited approval. " +
391
+ "This is safer but means you'll need a new approval for each bridge transaction.";
392
+ }
393
+ // Warn about native token requirements (e.g. deBridge protocol fees)
394
+ if (txRequest.value && txRequest.value !== "0x0" && txRequest.value !== "0x00") {
395
+ const valueBigInt = BigInt(txRequest.value);
396
+ if (valueBigInt > 0n) {
397
+ const ethAmount = Number(valueBigInt) / 1e18;
398
+ const chainName = getChainName(txRequest.chainId) ?? `chain ${txRequest.chainId}`;
399
+ const nativeSymbol = [56].includes(txRequest.chainId) ? "BNB"
400
+ : [137].includes(txRequest.chainId) ? "MATIC"
401
+ : [43114].includes(txRequest.chainId) ? "AVAX"
402
+ : "ETH";
403
+ warnings.push(`⚠️ This transaction requires ${ethAmount.toFixed(6)} ${nativeSymbol} for protocol fees in addition to the bridge amount. Ensure your wallet on ${chainName} has sufficient ${nativeSymbol} balance.`);
404
+ }
405
+ }
406
+ if (warnings.length > 0) {
407
+ response.warnings = warnings;
408
+ }
409
+ return {
410
+ content: [{ type: "text", text: JSON.stringify(response, null, 2) }],
411
+ };
412
+ }
413
+ catch (err) {
414
+ // Release lock on failure so the user can retry
415
+ executingQuotes.delete(params.quoteId);
416
+ return {
417
+ content: [
418
+ {
419
+ type: "text",
420
+ text: `Failed to build transaction: ${sanitizeError(err)}`,
421
+ },
422
+ ],
423
+ };
424
+ }
425
+ // H-3: Lock stays held on success — quote cannot be re-executed.
426
+ // Lock is only released on error (above) to allow retry.
427
+ });
428
+ }
@@ -0,0 +1,3 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import type { RoutingEngine } from "../routing/engine.js";
3
+ export declare function registerGetChains(server: McpServer, engine: RoutingEngine): void;
@@ -0,0 +1,162 @@
1
+ import { z } from "zod";
2
+ import { isCosmosChain, isSolanaChain } from "../utils/chains.js";
3
+ // Well-known chain ID → canonical name/key (for resolving unnamed "Chain XXXX" entries
4
+ // and for picking the best name/key when merging duplicate entries from different providers)
5
+ const CHAIN_ID_NAMES = {
6
+ 1: { name: "Ethereum", key: "ethereum" },
7
+ 10: { name: "Optimism", key: "optimism" },
8
+ 14: { name: "Flare", key: "flare" },
9
+ 25: { name: "Cronos", key: "cronos" },
10
+ 30: { name: "Rootstock", key: "rootstock" },
11
+ 56: { name: "BNB Chain", key: "bsc" },
12
+ 100: { name: "Gnosis", key: "gnosis" },
13
+ 122: { name: "Fuse", key: "fuse" },
14
+ 130: { name: "Unichain", key: "unichain" },
15
+ 137: { name: "Polygon", key: "polygon" },
16
+ 143: { name: "Monad", key: "monad" },
17
+ 146: { name: "Sonic", key: "sonic" },
18
+ 196: { name: "X Layer", key: "xlayer" },
19
+ 204: { name: "opBNB", key: "opbnb" },
20
+ 232: { name: "Lens", key: "lens" },
21
+ 250: { name: "Fantom", key: "fantom" },
22
+ 252: { name: "Fraxtal", key: "fraxtal" },
23
+ 288: { name: "Boba", key: "boba" },
24
+ 324: { name: "zkSync Era", key: "zksync" },
25
+ 360: { name: "Shape", key: "shape" },
26
+ 480: { name: "World Chain", key: "world-chain" },
27
+ 690: { name: "Redstone", key: "redstone" },
28
+ 999: { name: "HyperEVM", key: "hyperevm" },
29
+ 1088: { name: "Metis", key: "metis" },
30
+ 1101: { name: "Polygon zkEVM", key: "polygon-zkevm" },
31
+ 1135: { name: "Lisk", key: "lisk" },
32
+ 1284: { name: "Moonbeam", key: "moonbeam" },
33
+ 1329: { name: "Sei", key: "sei" },
34
+ 1514: { name: "Hemi", key: "hemi" },
35
+ 1516: { name: "Story", key: "story" },
36
+ 1625: { name: "Gravity", key: "gravity" },
37
+ 1750: { name: "Metal L2", key: "metal" },
38
+ 1868: { name: "Soneium", key: "soneium" },
39
+ 1923: { name: "Swellchain", key: "swellchain" },
40
+ 2020: { name: "Ronin", key: "ronin" },
41
+ 2522: { name: "Shadow", key: "shadow" },
42
+ 2741: { name: "Abstract", key: "abstract" },
43
+ 5000: { name: "Mantle", key: "mantle" },
44
+ 7560: { name: "Cyber", key: "cyber" },
45
+ 7777777: { name: "Zora", key: "zora" },
46
+ 8217: { name: "Kaia", key: "kaia" },
47
+ 8453: { name: "Base", key: "base" },
48
+ 13371: { name: "Immutable zkEVM", key: "immutable-zkevm" },
49
+ 21000000: { name: "Corn", key: "corn" },
50
+ 33139: { name: "ApeChain", key: "apechain" },
51
+ 34443: { name: "Mode", key: "mode" },
52
+ 42161: { name: "Arbitrum", key: "arbitrum" },
53
+ 42170: { name: "Arbitrum Nova", key: "arbitrum-nova" },
54
+ 42220: { name: "Celo", key: "celo" },
55
+ 43114: { name: "Avalanche", key: "avalanche" },
56
+ 50104: { name: "Sophon", key: "sophon" },
57
+ 57073: { name: "Ink", key: "ink" },
58
+ 59144: { name: "Linea", key: "linea" },
59
+ 60808: { name: "Bob", key: "bob" },
60
+ 80094: { name: "Berachain", key: "berachain" },
61
+ 81457: { name: "Blast", key: "blast" },
62
+ 98865: { name: "Plume", key: "plume" },
63
+ 167000: { name: "Taiko", key: "taiko" },
64
+ 534352: { name: "Scroll", key: "scroll" },
65
+ 810180: { name: "zkLink Nova", key: "zklink-nova" },
66
+ 666666666: { name: "Degen", key: "degen" },
67
+ 7225878: { name: "Saakuru", key: "saakuru" },
68
+ 4326: { name: "MegaETH", key: "megaeth" },
69
+ 9745: { name: "Plasma", key: "plasma" },
70
+ 37714555429: { name: "Xai", key: "xai" },
71
+ };
72
+ export function registerGetChains(server, engine) {
73
+ server.tool("bridge_chains", "List supported chains for cross-chain bridging. " +
74
+ "Shows which providers support each chain. " +
75
+ "Chains are deduplicated and grouped by ecosystem: EVM, Cosmos, and Solana. " +
76
+ "Use 'search' parameter to filter by chain name. Use this to discover available routes before calling bridge_get_quote.", {
77
+ search: z.string().optional().describe("Filter chains by name (e.g. 'base', 'arb')"),
78
+ }, async (params) => {
79
+ // Deduplicate by chain ID — different providers return different keys for the same chain
80
+ // (e.g. LI.FI uses "bas", Across uses "chain-8453", both are chain ID 8453 = Base)
81
+ const chainMap = new Map();
82
+ const results = await Promise.allSettled(engine.getAllBackends().map((b) => b.getSupportedChains()));
83
+ for (const result of results) {
84
+ if (result.status !== "fulfilled")
85
+ continue;
86
+ for (const chain of result.value) {
87
+ const existing = chainMap.get(chain.id);
88
+ if (existing) {
89
+ // Merge providers from duplicate entries
90
+ for (const p of chain.providers) {
91
+ if (!existing.providers.includes(p))
92
+ existing.providers.push(p);
93
+ }
94
+ // Pick best name/key: canonical lookup > shorter name > existing
95
+ const canonical = CHAIN_ID_NAMES[chain.id];
96
+ if (canonical) {
97
+ existing.name = canonical.name;
98
+ existing.key = canonical.key;
99
+ }
100
+ else if (chain.name.length < existing.name.length) {
101
+ existing.name = chain.name;
102
+ existing.key = chain.key;
103
+ }
104
+ }
105
+ else {
106
+ // Use canonical name/key if available
107
+ const canonical = CHAIN_ID_NAMES[chain.id];
108
+ chainMap.set(chain.id, {
109
+ ...chain,
110
+ name: canonical?.name ?? chain.name,
111
+ key: canonical?.key ?? chain.key,
112
+ });
113
+ }
114
+ }
115
+ }
116
+ let chains = Array.from(chainMap.values()).sort((a, b) => {
117
+ // Sort: well-known chains first (lower IDs), then alphabetically
118
+ if (a.id < 10000 && b.id >= 10000)
119
+ return -1;
120
+ if (a.id >= 10000 && b.id < 10000)
121
+ return 1;
122
+ return a.name.localeCompare(b.name);
123
+ });
124
+ // Apply search filter if specified
125
+ if (params.search) {
126
+ const searchLower = params.search.toLowerCase();
127
+ chains = chains.filter((c) => c.name.toLowerCase().includes(searchLower) ||
128
+ c.key.toLowerCase().includes(searchLower));
129
+ }
130
+ // Categorize chains by ecosystem
131
+ const cosmosKeys = new Set(["persistence", "cosmoshub", "osmosis", "neutron", "celestia", "injective", "sei", "dydx", "stride", "kujira"]);
132
+ const solanaKeys = new Set(["solana"]);
133
+ const evmChains = chains.filter((c) => !cosmosKeys.has(c.key) && !solanaKeys.has(c.key) && !isCosmosChain(c.id));
134
+ const cosmos = chains.filter((c) => cosmosKeys.has(c.key) || isCosmosChain(c.id));
135
+ const solana = chains.filter((c) => solanaKeys.has(c.key) || isSolanaChain(c.id));
136
+ const formatChain = (c) => ({ id: c.id, name: c.name, key: c.key, providers: c.providers });
137
+ return {
138
+ content: [
139
+ {
140
+ type: "text",
141
+ text: JSON.stringify({
142
+ totalChains: chains.length,
143
+ ecosystems: {
144
+ evm: {
145
+ count: evmChains.length,
146
+ chains: evmChains.map(formatChain),
147
+ },
148
+ cosmos: {
149
+ count: cosmos.length,
150
+ chains: cosmos.map(formatChain),
151
+ },
152
+ solana: {
153
+ count: solana.length,
154
+ chains: solana.map(formatChain),
155
+ },
156
+ },
157
+ }, null, 2),
158
+ },
159
+ ],
160
+ };
161
+ });
162
+ }
@@ -0,0 +1,3 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import type { RoutingEngine } from "../routing/engine.js";
3
+ export declare function registerGetQuote(server: McpServer, engine: RoutingEngine): void;