@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,204 @@
1
+ import { z } from "zod";
2
+ const HELP_CONTENT = {
3
+ overview: [
4
+ "BridgeKitty is a cross-chain bridge aggregator MCP server connecting EVM chains, Solana, and Cosmos chains (including Persistence and Cosmoshub).",
5
+ "",
6
+ "It provides 6 bridge backends:",
7
+ " - LI.FI: Aggregator covering 30+ bridges with the widest chain/token coverage.",
8
+ " - Squid Router (Skip Protocol): 120+ chains including 62+ Cosmos/IBC chains.",
9
+ " - deBridge DLN: Fast intent-based bridging with Solana support.",
10
+ " - Across: Fastest fills (~6 seconds average).",
11
+ " - Relay: Gas-optimized bridging for cost-sensitive transfers.",
12
+ " - Persistence Interop: Direct BTCB<>cbBTC bridging with XPRT farming rewards.",
13
+ "",
14
+ "Available tool categories:",
15
+ " Bridging: bridge_get_quote, bridge_execute, bridge_status, bridge_quote_multi",
16
+ " Discovery: bridge_chains, bridge_tokens",
17
+ " Wallet: wallet_status, wallet_setup, wallet_import, wallet_balance",
18
+ " XPRT Farming: xprt_farm_prepare, xprt_farm_start, xprt_farm_status, xprt_farm_boost, xprt_rewards_check",
19
+ " XPRT Staking: xprt_stake, xprt_unstake, xprt_claim_rewards",
20
+ " Onboarding: xprt_onboard",
21
+ "",
22
+ "Use bridgekitty_help with a specific topic for detailed guidance: 'bridging', 'farming', 'wallet', or 'troubleshooting'.",
23
+ ].join("\n"),
24
+ bridging: [
25
+ "Cross-Chain Bridging Guide",
26
+ "=========================",
27
+ "",
28
+ "Recommended flow:",
29
+ " 1. bridge_get_quote - Compare quotes across all backends for your route.",
30
+ " 2. bridge_execute - Get unsigned transaction data for the best quote.",
31
+ " 3. bridge_status - Track your bridge transfer progress until completion.",
32
+ "",
33
+ "Parameters for bridge_get_quote:",
34
+ " - fromChain / toChain: Chain name (e.g. 'base', 'arbitrum') or chain ID (e.g. '8453').",
35
+ " - fromToken / toToken: Token symbol (e.g. 'USDC', 'ETH') or contract address (0x...).",
36
+ " - amount: Human-readable amount (e.g. '100' for 100 USDC).",
37
+ " - fromAddress: Your wallet address (0x...).",
38
+ " - preference: 'cheapest' or 'fastest' (default: 'fastest').",
39
+ "",
40
+ "For multi-hop routes (e.g. ETH on Base to XPRT on Persistence):",
41
+ " Use bridge_quote_multi which automatically finds optimal intermediate hops.",
42
+ "",
43
+ "Common pitfalls:",
44
+ " - Quotes expire quickly (30-60s). Always get a fresh quote before executing.",
45
+ " - Some routes require token approval before the bridge tx. bridge_execute will",
46
+ " return both an approvalTransaction and the main transaction when needed.",
47
+ " - Ensure you have enough native gas token on the source chain.",
48
+ " - Token symbols are resolved against a verified registry. If a symbol is not found,",
49
+ " use the raw contract address instead.",
50
+ "",
51
+ "Example: Bridge 100 USDC from Base to Arbitrum:",
52
+ " 1. bridge_get_quote(fromChain='base', toChain='arbitrum', fromToken='USDC',",
53
+ " toToken='USDC', amount='100', fromAddress='0x...')",
54
+ " 2. bridge_execute(quoteId='<from step 1>')",
55
+ " 3. Sign and send the returned transaction(s)",
56
+ " 4. bridge_status(trackingId='<from step 2>')",
57
+ ].join("\n"),
58
+ farming: [
59
+ "XPRT Farming Guide",
60
+ "===================",
61
+ "",
62
+ "Earn XPRT rewards by bridging BTC variants (cbBTC <> BTCB) via Persistence Interop.",
63
+ "",
64
+ "Recommended flow:",
65
+ " 1. xprt_farm_prepare - Convert ETH or other tokens to cbBTC and bridge gas to BSC.",
66
+ " 2. xprt_farm_start - Run automated BTC round-trip swaps to accumulate volume.",
67
+ " 3. xprt_farm_status - Check wallet link status, balances, and current epoch info.",
68
+ " 4. xprt_rewards_check - View earned XPRT rewards, pending amounts, and multiplier tier.",
69
+ "",
70
+ "Multiplier Tiers:",
71
+ " - Explorer (1x): Default, no staking required",
72
+ " - Voyager (2x): Stake ≥ 10,000 XPRT",
73
+ " - Pioneer (5x): Stake ≥ 1,000,000 XPRT",
74
+ "",
75
+ "Staking tools to boost multiplier:",
76
+ " - xprt_stake: Delegate existing liquid XPRT to a validator",
77
+ " - xprt_unstake: Initiate unbonding of staked XPRT (21-day period)",
78
+ " - xprt_claim_rewards: Claim pending staking rewards",
79
+ " - xprt_farm_boost: Buy and auto-stake XPRT from ETH or other tokens",
80
+ "",
81
+ "Use xprt_stake to delegate XPRT and increase your multiplier.",
82
+ "",
83
+ "Quick start with xprt_onboard:",
84
+ " Run xprt_onboard for a personalized action plan based on your current wallet state.",
85
+ "",
86
+ "Key details:",
87
+ " - Minimum per-leg amount: 0.00005 BTC (~$5 at current prices).",
88
+ " - Maximum per-leg amount: 0.001 BTC (~$100 at current prices).",
89
+ " - Rewards are distributed daily as airdrops (estimated, not guaranteed).",
90
+ " - You need gas on both Base (ETH) and BSC (BNB) for round-trip farming.",
91
+ " - Link your Persistence address to receive XPRT rewards.",
92
+ "",
93
+ "Common pitfalls:",
94
+ " - Running out of gas on BSC (BNB) is the most common issue. xprt_farm_prepare",
95
+ " automatically bridges some ETH to BNB for gas.",
96
+ " - If rounds time out, the solver may be congested. Increase fillTimeout or try later.",
97
+ " - Loss protection: xprt_farm_start has maxLossBps (default 200 = 2%) and maxFailures",
98
+ " (default 3) to prevent excessive losses.",
99
+ ].join("\n"),
100
+ wallet: [
101
+ "Wallet Management Guide",
102
+ "=======================",
103
+ "",
104
+ "Recommended flow:",
105
+ " 1. wallet_status - Check if a wallet is already configured.",
106
+ " 2. wallet_setup - Generate new wallets for all chains (EVM + Cosmos + Solana).",
107
+ " OR wallet_import - Import existing mnemonic or private key.",
108
+ " 3. wallet_balance - Check balances across all configured chains.",
109
+ "",
110
+ "Key details:",
111
+ " - Keys are stored in ~/.bridgekitty/.env (or BRIDGEKITTY_HOME env var).",
112
+ " - A single mnemonic derives wallets for all chains:",
113
+ " - EVM: BIP-44 path m/44'/60'/0'/0/0 (same address on all EVM chains).",
114
+ " - Cosmos: persistence prefix for Persistence chain.",
115
+ " - Solana: BIP-44 path m/44'/501'/0'/0'.",
116
+ " - wallet_setup refuses to overwrite existing keys (safety feature).",
117
+ " - wallet_import with overwrite=true will replace existing keys (back up first!).",
118
+ "",
119
+ "Security notes:",
120
+ " - Keys are loaded into an in-memory store at startup and cleared from process.env.",
121
+ " - The .env file should have 0600 permissions (owner read/write only).",
122
+ " - Never share your mnemonic or private key.",
123
+ " - Back up your .env file immediately after wallet creation.",
124
+ "",
125
+ "Common pitfalls:",
126
+ " - Forgetting to back up keys before overwriting.",
127
+ " - Running wallet_setup when keys already exist (it will refuse -- use wallet_import).",
128
+ " - Importing only a privateKey gives EVM-only access. Use a mnemonic for full multi-chain support.",
129
+ ].join("\n"),
130
+ troubleshooting: [
131
+ "Troubleshooting Guide",
132
+ "=====================",
133
+ "",
134
+ "Rate Limits:",
135
+ " - bridge_get_quote is limited to 10 requests per route per minute.",
136
+ " - If you hit rate limits, wait 60 seconds before retrying the same route.",
137
+ " - Different routes have independent rate limits.",
138
+ "",
139
+ "RPC Timeouts:",
140
+ " - BridgeKitty uses multiple RPCs per chain with automatic failover.",
141
+ " - If you see RPC timeout errors, the chain may be congested. Retry after a few minutes.",
142
+ " - wallet_balance may show 'error' for individual chains while others succeed.",
143
+ "",
144
+ "Token Not Found:",
145
+ " - Token symbols are resolved against a curated verified registry.",
146
+ " - If a symbol is not found, use the raw contract address (0x...) instead.",
147
+ " - Common aliases: 'ETH' works on all L2s, 'BNB' on BSC, 'MATIC'/'POL' on Polygon.",
148
+ "",
149
+ "No Routes Found:",
150
+ " - Some token pairs or chain combinations may not be supported by any backend.",
151
+ " - Try bridge_quote_multi for routes that need intermediate hops.",
152
+ " - Check bridge_chains and bridge_tokens to verify chain/token support.",
153
+ "",
154
+ "Transaction Simulation Failed:",
155
+ " - The bridge transaction would revert on-chain. Common causes:",
156
+ " - Insufficient token balance or allowance.",
157
+ " - Quote expired (get a fresh quote).",
158
+ " - Insufficient gas (native token balance too low).",
159
+ " - Always get a fresh quote and try again.",
160
+ "",
161
+ "XPRT Farming Issues:",
162
+ " - 'Insufficient BTC balance': Run xprt_farm_prepare or send BTC to your wallet.",
163
+ " - 'Balance below minimum': You need at least 0.00005 BTC per leg.",
164
+ " - Timeouts during farming: Increase fillTimeout parameter (default 180s).",
165
+ " - High loss: Adjust maxLossBps or reduce round count.",
166
+ "",
167
+ "Circuit Breaker:",
168
+ " - Backends that fail repeatedly are temporarily disabled (circuit broken).",
169
+ " - They automatically recover after a cooldown period.",
170
+ " - If all backends are down, wait a few minutes and retry.",
171
+ ].join("\n"),
172
+ };
173
+ export function registerHelpTool(server) {
174
+ server.tool("bridgekitty_help", "Get help and guidance on using BridgeKitty. Topics: overview (default), bridging, farming, wallet, troubleshooting. " +
175
+ "Returns structured guides with recommended tool sequences and common pitfalls.", {
176
+ topic: z
177
+ .enum(["overview", "bridging", "farming", "wallet", "troubleshooting"])
178
+ .optional()
179
+ .default("overview")
180
+ .describe("Help topic: 'overview', 'bridging', 'farming', 'wallet', or 'troubleshooting'"),
181
+ }, async (params) => {
182
+ const topic = params.topic ?? "overview";
183
+ const content = HELP_CONTENT[topic];
184
+ if (!content) {
185
+ return {
186
+ content: [{
187
+ type: "text",
188
+ text: `Unknown topic: ${topic}. Available topics: overview, bridging, farming, wallet, troubleshooting.`,
189
+ }],
190
+ isError: true,
191
+ };
192
+ }
193
+ return {
194
+ content: [{
195
+ type: "text",
196
+ text: JSON.stringify({
197
+ topic,
198
+ guide: content,
199
+ availableTopics: ["overview", "bridging", "farming", "wallet", "troubleshooting"],
200
+ }, null, 2),
201
+ }],
202
+ };
203
+ });
204
+ }
@@ -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 registerMultiQuote(server: McpServer, engine: RoutingEngine): void;
@@ -0,0 +1,310 @@
1
+ import { z } from "zod";
2
+ import { resolveChainId, getChainName } from "../utils/chains.js";
3
+ import { resolveToken } from "../utils/token-registry.js";
4
+ import { parseTokenAmount } from "../utils/tokens.js";
5
+ // Common intermediary tokens used for multi-hop routing
6
+ const INTERMEDIARIES = [
7
+ { symbol: "USDC", label: "USDC" },
8
+ { symbol: "ETH", label: "ETH" },
9
+ { symbol: "CBBTC", label: "cbBTC" },
10
+ { symbol: "WBTC", label: "WBTC" },
11
+ ];
12
+ export function registerMultiQuote(server, engine) {
13
+ server.tool("bridge_quote_multi", "Get quotes for multi-hop routes across supported EVM chains. Resolves optimal path internally " +
14
+ "-- e.g., ETH on Base to USDC on Arbitrum may route through WBTC. Returns the full route " +
15
+ "as ordered hops with per-hop detail. " +
16
+ "Note: For Cosmos destinations (Persistence, Cosmos Hub), use xprt_farm_boost or bridge_get_quote with Squid Router.", {
17
+ fromChain: z
18
+ .string()
19
+ .describe("Source chain (e.g. 'base', 'ethereum', or chain ID like '8453')"),
20
+ fromToken: z
21
+ .string()
22
+ .describe("Token to send -- symbol (e.g. 'USDC', 'ETH') or contract address (0x...)"),
23
+ toChain: z
24
+ .string()
25
+ .describe("Destination chain (e.g. 'arbitrum', 'bsc', or chain ID)"),
26
+ toToken: z
27
+ .string()
28
+ .describe("Token to receive -- symbol (e.g. 'USDC', 'ETH') or contract address (0x...)"),
29
+ amount: z
30
+ .string()
31
+ .describe("Amount in human-readable units (e.g. '100' for 100 USDC)"),
32
+ fromAddress: z
33
+ .string()
34
+ .describe("Sender wallet address (0x...)"),
35
+ optimize: z
36
+ .enum(["cheapest", "fastest"])
37
+ .default("cheapest")
38
+ .describe("Optimize for lowest cost or fastest delivery"),
39
+ }, async (params) => {
40
+ // Resolve chains
41
+ const fromChainId = resolveChainId(params.fromChain);
42
+ const toChainId = resolveChainId(params.toChain);
43
+ if (!fromChainId) {
44
+ return {
45
+ content: [{
46
+ type: "text",
47
+ text: `Unknown source chain: ${params.fromChain}. Use chain name (e.g. 'base') or ID (e.g. '8453').`,
48
+ }],
49
+ isError: true,
50
+ };
51
+ }
52
+ if (!toChainId) {
53
+ return {
54
+ content: [{
55
+ type: "text",
56
+ text: `Unknown destination chain: ${params.toChain}. Use chain name (e.g. 'arbitrum') or ID.`,
57
+ }],
58
+ isError: true,
59
+ };
60
+ }
61
+ // Resolve tokens
62
+ const fromTokenResult = resolveToken(params.fromToken, fromChainId);
63
+ if (!fromTokenResult.ok) {
64
+ return {
65
+ content: [{
66
+ type: "text",
67
+ text: JSON.stringify({
68
+ error: "Token resolution failed",
69
+ token: params.fromToken,
70
+ chain: getChainName(fromChainId),
71
+ message: fromTokenResult.error,
72
+ }, null, 2),
73
+ }],
74
+ isError: true,
75
+ };
76
+ }
77
+ const toTokenResult = resolveToken(params.toToken, toChainId);
78
+ if (!toTokenResult.ok) {
79
+ return {
80
+ content: [{
81
+ type: "text",
82
+ text: JSON.stringify({
83
+ error: "Token resolution failed",
84
+ token: params.toToken,
85
+ chain: getChainName(toChainId),
86
+ message: toTokenResult.error,
87
+ }, null, 2),
88
+ }],
89
+ isError: true,
90
+ };
91
+ }
92
+ const fromTokenAddress = fromTokenResult.address;
93
+ const toTokenAddress = toTokenResult.address;
94
+ const fromDecimals = fromTokenResult.decimals;
95
+ const toDecimals = toTokenResult.decimals;
96
+ const fromSymbol = fromTokenResult.symbol;
97
+ const toSymbol = toTokenResult.symbol;
98
+ // Validate amount
99
+ const amountTrimmed = params.amount.trim();
100
+ if (!amountTrimmed || !/^\d+\.?\d*$/.test(amountTrimmed)) {
101
+ return {
102
+ content: [{
103
+ type: "text",
104
+ text: JSON.stringify({
105
+ error: "Invalid amount",
106
+ message: `Amount must be a positive number. Got: "${params.amount}"`,
107
+ }),
108
+ }],
109
+ isError: true,
110
+ };
111
+ }
112
+ const amountRaw = parseTokenAmount(amountTrimmed, fromDecimals);
113
+ const routes = [];
114
+ // ── Strategy 1: Try direct route ────────────────────────────────────────
115
+ try {
116
+ const directQuotes = await engine.getQuotes({
117
+ fromChainId,
118
+ toChainId,
119
+ fromTokenAddress,
120
+ toTokenAddress,
121
+ amountRaw,
122
+ fromAddress: params.fromAddress,
123
+ preference: params.optimize,
124
+ fromTokenDecimals: fromDecimals,
125
+ toTokenDecimals: toDecimals,
126
+ });
127
+ for (const q of directQuotes.slice(0, 2)) {
128
+ routes.push({
129
+ hops: [{
130
+ hopNumber: 1,
131
+ provider: q.provider,
132
+ fromChain: getChainName(fromChainId),
133
+ toChain: getChainName(toChainId),
134
+ fromToken: fromSymbol,
135
+ toToken: toSymbol,
136
+ estimatedOutput: q.minOutputAmount,
137
+ estimatedFeeUsd: q.estimatedFeeUsd,
138
+ estimatedTimeSeconds: q.estimatedTimeSeconds,
139
+ }],
140
+ estimatedOutput: `${q.minOutputAmount} ${toSymbol}`,
141
+ totalFeeUsd: q.estimatedFeeUsd,
142
+ estimatedTotalTimeSeconds: `${q.estimatedTimeSeconds}s`,
143
+ routeLabel: `Direct: ${fromSymbol} -> ${toSymbol} via ${q.provider}`,
144
+ });
145
+ }
146
+ }
147
+ catch {
148
+ // Direct route failed -- continue to multi-hop
149
+ }
150
+ // ── Strategy 2: Try 2-hop routes via intermediaries ─────────────────────
151
+ if (routes.length < 3) {
152
+ const intermediaryPromises = INTERMEDIARIES
153
+ .filter((mid) => {
154
+ // Skip if intermediary is the same as source or destination token
155
+ return mid.symbol.toUpperCase() !== fromSymbol.toUpperCase() &&
156
+ mid.symbol.toUpperCase() !== toSymbol.toUpperCase();
157
+ })
158
+ .map(async (mid) => {
159
+ try {
160
+ // Resolve intermediary on source chain (for hop 1 destination)
161
+ const midOnSourceResult = resolveToken(mid.symbol, fromChainId);
162
+ if (!midOnSourceResult.ok)
163
+ return null;
164
+ // Resolve intermediary on destination chain (for hop 2 source)
165
+ const midOnDestResult = resolveToken(mid.symbol, toChainId);
166
+ if (!midOnDestResult.ok)
167
+ return null;
168
+ // Hop 1: fromToken on fromChain -> intermediary on fromChain (same-chain swap)
169
+ // Then cross-chain: intermediary fromChain -> intermediary toChain
170
+ // For simplicity, try direct cross-chain: fromToken -> intermediary on toChain
171
+ // Then intermediary -> toToken on toChain
172
+ // Hop 1: fromToken -> midToken cross-chain (fromChain -> toChain)
173
+ let hop1Quotes;
174
+ try {
175
+ hop1Quotes = await engine.getQuotes({
176
+ fromChainId,
177
+ toChainId,
178
+ fromTokenAddress,
179
+ toTokenAddress: midOnDestResult.address,
180
+ amountRaw,
181
+ fromAddress: params.fromAddress,
182
+ preference: params.optimize,
183
+ fromTokenDecimals: fromDecimals,
184
+ toTokenDecimals: midOnDestResult.decimals,
185
+ });
186
+ }
187
+ catch {
188
+ return null;
189
+ }
190
+ if (!hop1Quotes || hop1Quotes.length === 0)
191
+ return null;
192
+ const hop1 = hop1Quotes[0];
193
+ // Hop 2: midToken -> toToken on toChain (same-chain swap)
194
+ const hop1OutputRaw = hop1.minOutputAmountRaw;
195
+ let hop2Quotes;
196
+ try {
197
+ hop2Quotes = await engine.getQuotes({
198
+ fromChainId: toChainId,
199
+ toChainId: toChainId,
200
+ fromTokenAddress: midOnDestResult.address,
201
+ toTokenAddress,
202
+ amountRaw: hop1OutputRaw,
203
+ fromAddress: params.fromAddress,
204
+ preference: params.optimize,
205
+ fromTokenDecimals: midOnDestResult.decimals,
206
+ toTokenDecimals: toDecimals,
207
+ });
208
+ }
209
+ catch {
210
+ // Same-chain swap may not be supported by cross-chain engine
211
+ return null;
212
+ }
213
+ if (!hop2Quotes || hop2Quotes.length === 0)
214
+ return null;
215
+ const hop2 = hop2Quotes[0];
216
+ const totalTimeSec = hop1.estimatedTimeSeconds + hop2.estimatedTimeSeconds;
217
+ const fee1 = hop1.estimatedFeeUsd;
218
+ const fee2 = hop2.estimatedFeeUsd;
219
+ const totalFeeUsd = (fee1 !== null && fee2 !== null) ? fee1 + fee2 : null;
220
+ return {
221
+ hops: [
222
+ {
223
+ hopNumber: 1,
224
+ provider: hop1.provider,
225
+ fromChain: getChainName(fromChainId),
226
+ toChain: getChainName(toChainId),
227
+ fromToken: fromSymbol,
228
+ toToken: mid.label,
229
+ estimatedOutput: hop1.minOutputAmount,
230
+ estimatedFeeUsd: hop1.estimatedFeeUsd,
231
+ estimatedTimeSeconds: hop1.estimatedTimeSeconds,
232
+ },
233
+ {
234
+ hopNumber: 2,
235
+ provider: hop2.provider,
236
+ fromChain: getChainName(toChainId),
237
+ toChain: getChainName(toChainId),
238
+ fromToken: mid.label,
239
+ toToken: toSymbol,
240
+ estimatedOutput: hop2.minOutputAmount,
241
+ estimatedFeeUsd: hop2.estimatedFeeUsd,
242
+ estimatedTimeSeconds: hop2.estimatedTimeSeconds,
243
+ },
244
+ ],
245
+ estimatedOutput: `${hop2.minOutputAmount} ${toSymbol}`,
246
+ totalFeeUsd,
247
+ estimatedTotalTimeSeconds: `${totalTimeSec}s`,
248
+ routeLabel: `${fromSymbol} -> ${mid.label} -> ${toSymbol} via ${hop1.provider} + ${hop2.provider}`,
249
+ };
250
+ }
251
+ catch {
252
+ return null;
253
+ }
254
+ });
255
+ const intermediaryResults = await Promise.allSettled(intermediaryPromises);
256
+ for (const result of intermediaryResults) {
257
+ if (result.status === "fulfilled" && result.value) {
258
+ routes.push(result.value);
259
+ }
260
+ }
261
+ }
262
+ if (routes.length === 0) {
263
+ return {
264
+ content: [{
265
+ type: "text",
266
+ text: JSON.stringify({
267
+ error: "No routes found",
268
+ message: `No direct or multi-hop routes found for ${params.amount} ${fromSymbol} from ${getChainName(fromChainId)} to ${toSymbol} on ${getChainName(toChainId)}. This route may not be supported.`,
269
+ suggestions: [
270
+ "Try different token pairs (e.g. bridge to USDC first, then swap).",
271
+ "Check bridge_chains and bridge_tokens for supported chains/tokens.",
272
+ "Use bridge_get_quote for direct single-hop routes.",
273
+ ],
274
+ }, null, 2),
275
+ }],
276
+ };
277
+ }
278
+ // Sort routes by optimization preference
279
+ if (params.optimize === "cheapest") {
280
+ routes.sort((a, b) => {
281
+ // Parse output amounts for comparison (higher is better)
282
+ const outA = parseFloat(a.estimatedOutput) || 0;
283
+ const outB = parseFloat(b.estimatedOutput) || 0;
284
+ return outB - outA;
285
+ });
286
+ }
287
+ else {
288
+ routes.sort((a, b) => {
289
+ const timeA = parseInt(a.estimatedTotalTimeSeconds) || 0;
290
+ const timeB = parseInt(b.estimatedTotalTimeSeconds) || 0;
291
+ return timeA - timeB;
292
+ });
293
+ }
294
+ // Limit to top 3
295
+ const topRoutes = routes.slice(0, 3);
296
+ const response = {
297
+ routes: topRoutes,
298
+ totalRoutesFound: routes.length,
299
+ optimizedFor: params.optimize,
300
+ summary: `Found ${routes.length} route(s) for ${params.amount} ${fromSymbol} (${getChainName(fromChainId)}) -> ${toSymbol} (${getChainName(toChainId)}). Best: ${topRoutes[0].routeLabel}, output: ${topRoutes[0].estimatedOutput}.`,
301
+ note: "Multi-hop routes require executing each hop sequentially. Use bridge_get_quote + bridge_execute for each hop.",
302
+ };
303
+ return {
304
+ content: [{
305
+ type: "text",
306
+ text: JSON.stringify(response, null, 2),
307
+ }],
308
+ };
309
+ });
310
+ }
@@ -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 registerOnboardTool(server: McpServer, engine: RoutingEngine): void;