EDUCHAIN Agent Kit

  • src
#!/usr/bin/env node import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { CallToolRequestSchema, ErrorCode, ListResourcesRequestSchema, ListResourceTemplatesRequestSchema, ListToolsRequestSchema, McpError, ReadResourceRequestSchema, } from '@modelcontextprotocol/sdk/types.js'; import { ethers } from 'ethers'; import * as subgraph from './subgraph.js'; import * as blockchain from './blockchain.js'; import * as swap from './swap.js'; import * as external_market from './external_market.js'; class SailFishSubgraphServer { private server: Server; constructor() { this.server = new Server( { name: 'educhain-agent-kit', version: '1.0.0', }, { capabilities: { resources: {}, tools: {}, }, } ); this.setupResourceHandlers(); this.setupToolHandlers(); // Error handling this.server.onerror = (error) => console.error('[MCP Error]', error); process.on('SIGINT', async () => { await this.server.close(); process.exit(0); }); } private setupResourceHandlers() { // List available resources this.server.setRequestHandler(ListResourcesRequestSchema, async () => ({ resources: [ { uri: 'sailfish://overview', name: 'SailFish DEX Overview', mimeType: 'application/json', description: 'Overview of SailFish DEX including TVL, volume, and other metrics', }, ], })); // List resource templates this.server.setRequestHandler( ListResourceTemplatesRequestSchema, async () => ({ resourceTemplates: [ { uriTemplate: 'sailfish://token/{tokenId}', name: 'Token Information', mimeType: 'application/json', description: 'Information about a specific token on SailFish DEX', }, { uriTemplate: 'sailfish://pool/{poolId}', name: 'Pool Information', mimeType: 'application/json', description: 'Information about a specific liquidity pool on SailFish DEX', }, ], }) ); // Handle resource reading this.server.setRequestHandler( ReadResourceRequestSchema, async (request) => { try { const { uri } = request.params; let content = ''; // Handle overview resource if (uri === 'sailfish://overview') { const factory = await subgraph.getFactory(); const ethPrice = await subgraph.getEthPrice(); content = JSON.stringify({ totalValueLockedUSD: factory?.totalValueLockedUSD || '0', totalVolumeUSD: factory?.totalVolumeUSD || '0', txCount: factory?.txCount || '0', poolCount: factory?.poolCount || '0', ethPriceUSD: ethPrice || '0', timestamp: new Date().toISOString(), }, null, 2); } // Handle token resource else if (uri.startsWith('sailfish://token/')) { const tokenId = uri.replace('sailfish://token/', ''); const token = await subgraph.getToken(tokenId); if (!token) { throw new McpError( ErrorCode.InvalidRequest, `Token with ID ${tokenId} not found` ); } const tokenPrice = await subgraph.getTokenPrice(tokenId); content = JSON.stringify({ ...token, priceUSD: tokenPrice || 'Unknown', timestamp: new Date().toISOString(), }, null, 2); } // Handle pool resource else if (uri.startsWith('sailfish://pool/')) { const poolId = uri.replace('sailfish://pool/', ''); const pool = await subgraph.getPool(poolId); if (!pool) { throw new McpError( ErrorCode.InvalidRequest, `Pool with ID ${poolId} not found` ); } content = JSON.stringify({ ...pool, timestamp: new Date().toISOString(), }, null, 2); } else { throw new McpError( ErrorCode.InvalidRequest, `Invalid URI format: ${uri}` ); } return { contents: [ { uri: request.params.uri, mimeType: 'application/json', text: content, }, ], }; } catch (error) { if (error instanceof McpError) { throw error; } console.error('Error handling resource request:', error); throw new McpError( ErrorCode.InternalError, `Failed to fetch data: ${(error as Error).message}` ); } } ); } private setupToolHandlers() { // List available tools this.server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: [ { name: 'get_token_price', description: 'Get the current price of a token on SailFish DEX', inputSchema: { type: 'object', properties: { tokenId: { type: 'string', description: 'Token address', }, }, required: ['tokenId'], }, }, { name: 'get_token_info', description: 'Get detailed information about a token on SailFish DEX', inputSchema: { type: 'object', properties: { tokenId: { type: 'string', description: 'Token address', }, }, required: ['tokenId'], }, }, { name: 'get_pool_info', description: 'Get detailed information about a liquidity pool on SailFish DEX', inputSchema: { type: 'object', properties: { poolId: { type: 'string', description: 'Pool address', }, }, required: ['poolId'], }, }, { name: 'get_top_tokens', description: 'Get a list of top tokens by TVL on SailFish DEX', inputSchema: { type: 'object', properties: { count: { type: 'number', description: 'Number of tokens to return (default: 10)', }, }, required: [], }, }, { name: 'get_top_pools', description: 'Get a list of top liquidity pools by TVL on SailFish DEX', inputSchema: { type: 'object', properties: { count: { type: 'number', description: 'Number of pools to return (default: 10)', }, }, required: [], }, }, { name: 'get_total_tvl', description: 'Get the total value locked (TVL) in SailFish DEX', inputSchema: { type: 'object', properties: {}, required: [], }, }, { name: 'get_24h_volume', description: 'Get the 24-hour trading volume on SailFish DEX', inputSchema: { type: 'object', properties: {}, required: [], }, }, { name: 'get_token_historical_data', description: 'Get historical data for a token on SailFish DEX', inputSchema: { type: 'object', properties: { tokenId: { type: 'string', description: 'Token address', }, days: { type: 'number', description: 'Number of days of data to return (default: 7)', }, }, required: ['tokenId'], }, }, { name: 'get_pool_historical_data', description: 'Get historical data for a liquidity pool on SailFish DEX', inputSchema: { type: 'object', properties: { poolId: { type: 'string', description: 'Pool address', }, days: { type: 'number', description: 'Number of days of data to return (default: 7)', }, }, required: ['poolId'], }, }, { name: 'get_edu_balance', description: 'Get the EDU balance of a wallet address', inputSchema: { type: 'object', properties: { walletAddress: { type: 'string', description: 'Wallet address to check', }, }, required: ['walletAddress'], }, }, { name: 'get_token_balance', description: 'Get the token balance of a wallet address with USD value using SailFish as price oracle', inputSchema: { type: 'object', properties: { tokenAddress: { type: 'string', description: 'Token contract address', }, walletAddress: { type: 'string', description: 'Wallet address to check', }, }, required: ['tokenAddress', 'walletAddress'], }, }, { name: 'get_multiple_token_balances', description: 'Get multiple token balances for a wallet address with USD values using SailFish as price oracle', inputSchema: { type: 'object', properties: { tokenAddresses: { type: 'array', items: { type: 'string', }, description: 'List of token contract addresses', }, walletAddress: { type: 'string', description: 'Wallet address to check', }, }, required: ['tokenAddresses', 'walletAddress'], }, }, { name: 'get_nft_balance', description: 'Get the NFT balance of a wallet address for a specific NFT collection', inputSchema: { type: 'object', properties: { nftAddress: { type: 'string', description: 'NFT contract address', }, walletAddress: { type: 'string', description: 'Wallet address to check', }, fetchTokenIds: { type: 'boolean', description: 'Whether to fetch token IDs (default: true)', }, }, required: ['nftAddress', 'walletAddress'], }, }, { name: 'get_wallet_overview', description: 'Get an overview of a wallet including EDU, tokens, and NFTs', inputSchema: { type: 'object', properties: { walletAddress: { type: 'string', description: 'Wallet address to check', }, tokenAddresses: { type: 'array', items: { type: 'string', }, description: 'List of token contract addresses to check', }, nftAddresses: { type: 'array', items: { type: 'string', }, description: 'List of NFT contract addresses to check', }, }, required: ['walletAddress'], }, }, { name: 'set_rpc_url', description: 'Set the RPC URL for blockchain interactions', inputSchema: { type: 'object', properties: { url: { type: 'string', description: 'RPC URL to use for blockchain interactions', }, }, required: ['url'], }, }, { name: 'get_rpc_url', description: 'Get the current RPC URL used for blockchain interactions', inputSchema: { type: 'object', properties: {}, required: [], }, }, { name: 'send_edu', description: 'Send EDU native token to another wallet address', inputSchema: { type: 'object', properties: { privateKey: { type: 'string', description: 'Private key of the sender wallet', }, toAddress: { type: 'string', description: 'Recipient wallet address', }, amount: { type: 'string', description: 'Amount of EDU to send', }, }, required: ['privateKey', 'toAddress', 'amount'], }, }, { name: 'get_wallet_address_from_private_key', description: 'Get wallet address from private key with proper checksum formatting', inputSchema: { type: 'object', properties: { privateKey: { type: 'string', description: 'Private key of the wallet', }, }, required: ['privateKey'], }, }, { name: 'send_erc20_token', description: 'Send ERC20 token to another wallet address', inputSchema: { type: 'object', properties: { privateKey: { type: 'string', description: 'Private key of the sender wallet', }, tokenAddress: { type: 'string', description: 'Token contract address', }, toAddress: { type: 'string', description: 'Recipient wallet address', }, amount: { type: 'string', description: 'Amount of tokens to send', }, confirm: { type: 'boolean', description: 'Confirm the transaction after verifying wallet address (default: true)', }, }, required: ['privateKey', 'tokenAddress', 'toAddress', 'amount'], }, }, { name: 'get_swap_quote', description: 'Get a quote for swapping tokens on SailFish DEX', inputSchema: { type: 'object', properties: { tokenIn: { type: 'string', description: 'Address of the input token', }, tokenOut: { type: 'string', description: 'Address of the output token', }, amountIn: { type: 'string', description: 'Amount of input token to swap', }, fee: { type: 'number', description: 'Fee tier (100=0.01%, 500=0.05%, 3000=0.3%, 10000=1%)', }, }, required: ['tokenIn', 'tokenOut', 'amountIn'], }, }, { name: 'swap_tokens', description: 'Swap tokens on SailFish DEX (token to token)', inputSchema: { type: 'object', properties: { privateKey: { type: 'string', description: 'Private key of the sender wallet', }, tokenIn: { type: 'string', description: 'Address of the input token', }, tokenOut: { type: 'string', description: 'Address of the output token', }, amountIn: { type: 'string', description: 'Amount of input token to swap', }, slippagePercentage: { type: 'number', description: 'Slippage tolerance percentage (default: 0.5)', }, fee: { type: 'number', description: 'Fee tier (100=0.01%, 500=0.05%, 3000=0.3%, 10000=1%)', }, }, required: ['privateKey', 'tokenIn', 'tokenOut', 'amountIn'], }, }, { name: 'swap_edu_for_tokens', description: 'Swap EDU for tokens on SailFish DEX', inputSchema: { type: 'object', properties: { privateKey: { type: 'string', description: 'Private key of the sender wallet', }, tokenOut: { type: 'string', description: 'Address of the output token', }, amountIn: { type: 'string', description: 'Amount of EDU to swap', }, slippagePercentage: { type: 'number', description: 'Slippage tolerance percentage (default: 0.5)', }, fee: { type: 'number', description: 'Fee tier (100=0.01%, 500=0.05%, 3000=0.3%, 10000=1%)', }, }, required: ['privateKey', 'tokenOut', 'amountIn'], }, }, { name: 'swap_tokens_for_edu', description: 'Swap tokens for EDU on SailFish DEX', inputSchema: { type: 'object', properties: { privateKey: { type: 'string', description: 'Private key of the sender wallet', }, tokenIn: { type: 'string', description: 'Address of the input token', }, amountIn: { type: 'string', description: 'Amount of tokens to swap', }, slippagePercentage: { type: 'number', description: 'Slippage tolerance percentage (default: 0.5)', }, fee: { type: 'number', description: 'Fee tier (100=0.01%, 500=0.05%, 3000=0.3%, 10000=1%)', }, }, required: ['privateKey', 'tokenIn', 'amountIn'], }, }, { name: 'get_external_market_data', description: 'Get external market data for EDU from centralized exchanges', inputSchema: { type: 'object', properties: {}, required: [], }, }, { name: 'check_arbitrage_opportunities', description: 'Check for arbitrage opportunities between centralized exchanges and SailFish DEX', inputSchema: { type: 'object', properties: { threshold: { type: 'number', description: 'Minimum price difference percentage to consider as an arbitrage opportunity (default: 1.0)', }, }, required: [], }, }, { name: 'update_external_market_config', description: 'Update the configuration for external market data API', inputSchema: { type: 'object', properties: { apiUrl: { type: 'string', description: 'API URL for external market data', }, apiKey: { type: 'string', description: 'API key for external market data (if required)', }, symbols: { type: 'object', properties: { EDU: { type: 'string', description: 'Symbol for EDU token on the external API', }, USD: { type: 'string', description: 'Symbol for USD on the external API', }, }, description: 'Symbol mappings for the external API', }, }, required: [], }, }, { name: 'get_external_market_config', description: 'Get the current configuration for external market data API', inputSchema: { type: 'object', properties: {}, required: [], }, }, { name: 'wrap_edu', description: 'Wrap EDU to WEDU (Wrapped EDU)', inputSchema: { type: 'object', properties: { privateKey: { type: 'string', description: 'Private key of the wallet', }, amount: { type: 'string', description: 'Amount of EDU to wrap', }, }, required: ['privateKey', 'amount'], }, }, { name: 'unwrap_wedu', description: 'Unwrap WEDU (Wrapped EDU) to EDU', inputSchema: { type: 'object', properties: { privateKey: { type: 'string', description: 'Private key of the wallet', }, amount: { type: 'string', description: 'Amount of WEDU to unwrap', }, }, required: ['privateKey', 'amount'], }, }, ], })); // Handle tool calls this.server.setRequestHandler(CallToolRequestSchema, async (request) => { try { const { name, arguments: args = {} } = request.params; switch (name) { case 'get_token_price': { if (!args.tokenId || typeof args.tokenId !== 'string') { throw new McpError(ErrorCode.InvalidParams, 'Token ID is required'); } const price = await subgraph.getTokenPrice(args.tokenId); return { content: [ { type: 'text', text: JSON.stringify({ price: price || 'Unknown' }, null, 2), }, ], }; } case 'get_token_info': { if (!args.tokenId || typeof args.tokenId !== 'string') { throw new McpError(ErrorCode.InvalidParams, 'Token ID is required'); } const token = await subgraph.getToken(args.tokenId); if (!token) { throw new McpError(ErrorCode.InvalidRequest, `Token with ID ${args.tokenId} not found`); } const price = await subgraph.getTokenPrice(args.tokenId); return { content: [ { type: 'text', text: JSON.stringify({ ...token, priceUSD: price || 'Unknown' }, null, 2), }, ], }; } case 'get_pool_info': { if (!args.poolId || typeof args.poolId !== 'string') { throw new McpError(ErrorCode.InvalidParams, 'Pool ID is required'); } const pool = await subgraph.getPool(args.poolId); if (!pool) { throw new McpError(ErrorCode.InvalidRequest, `Pool with ID ${args.poolId} not found`); } return { content: [ { type: 'text', text: JSON.stringify(pool, null, 2), }, ], }; } case 'get_top_tokens': { const count = typeof args.count === 'number' ? args.count : 10; const tokens = await subgraph.getTopTokens(count); return { content: [ { type: 'text', text: JSON.stringify(tokens, null, 2), }, ], }; } case 'get_top_pools': { const count = typeof args.count === 'number' ? args.count : 10; const pools = await subgraph.getTopPools(count); return { content: [ { type: 'text', text: JSON.stringify(pools, null, 2), }, ], }; } case 'get_total_tvl': { const tvl = await subgraph.getTotalTVL(); return { content: [ { type: 'text', text: JSON.stringify({ totalValueLockedUSD: tvl }, null, 2), }, ], }; } case 'get_24h_volume': { const volume = await subgraph.get24HVolume(); return { content: [ { type: 'text', text: JSON.stringify({ volumeUSD: volume }, null, 2), }, ], }; } case 'get_token_historical_data': { if (!args.tokenId || typeof args.tokenId !== 'string') { throw new McpError(ErrorCode.InvalidParams, 'Token ID is required'); } const days = typeof args.days === 'number' ? args.days : 7; const data = await subgraph.getTokenDayData(args.tokenId, days); return { content: [ { type: 'text', text: JSON.stringify(data, null, 2), }, ], }; } case 'get_pool_historical_data': { if (!args.poolId || typeof args.poolId !== 'string') { throw new McpError(ErrorCode.InvalidParams, 'Pool ID is required'); } const days = typeof args.days === 'number' ? args.days : 7; const data = await subgraph.getPoolDayData(args.poolId, days); return { content: [ { type: 'text', text: JSON.stringify(data, null, 2), }, ], }; } case 'get_edu_balance': { if (!args.walletAddress || typeof args.walletAddress !== 'string') { throw new McpError(ErrorCode.InvalidParams, 'Wallet address is required'); } const balance = await blockchain.getEduBalance(args.walletAddress); return { content: [ { type: 'text', text: JSON.stringify(balance, null, 2), }, ], }; } case 'get_token_balance': { if (!args.tokenAddress || typeof args.tokenAddress !== 'string') { throw new McpError(ErrorCode.InvalidParams, 'Token address is required'); } if (!args.walletAddress || typeof args.walletAddress !== 'string') { throw new McpError(ErrorCode.InvalidParams, 'Wallet address is required'); } const balance = await blockchain.getTokenBalance(args.tokenAddress, args.walletAddress); return { content: [ { type: 'text', text: JSON.stringify(balance, null, 2), }, ], }; } case 'get_multiple_token_balances': { if (!args.tokenAddresses || !Array.isArray(args.tokenAddresses)) { throw new McpError(ErrorCode.InvalidParams, 'Token addresses array is required'); } if (!args.walletAddress || typeof args.walletAddress !== 'string') { throw new McpError(ErrorCode.InvalidParams, 'Wallet address is required'); } const balances = await blockchain.getMultipleTokenBalances(args.tokenAddresses, args.walletAddress); return { content: [ { type: 'text', text: JSON.stringify(balances, null, 2), }, ], }; } case 'get_nft_balance': { if (!args.nftAddress || typeof args.nftAddress !== 'string') { throw new McpError(ErrorCode.InvalidParams, 'NFT address is required'); } if (!args.walletAddress || typeof args.walletAddress !== 'string') { throw new McpError(ErrorCode.InvalidParams, 'Wallet address is required'); } const fetchTokenIds = args.fetchTokenIds !== false; const balance = await blockchain.getERC721Balance(args.nftAddress, args.walletAddress, fetchTokenIds); return { content: [ { type: 'text', text: JSON.stringify(balance, null, 2), }, ], }; } case 'get_wallet_overview': { if (!args.walletAddress || typeof args.walletAddress !== 'string') { throw new McpError(ErrorCode.InvalidParams, 'Wallet address is required'); } const tokenAddresses = Array.isArray(args.tokenAddresses) ? args.tokenAddresses : []; const nftAddresses = Array.isArray(args.nftAddresses) ? args.nftAddresses : []; const overview = await blockchain.getWalletOverview(args.walletAddress, tokenAddresses, nftAddresses); return { content: [ { type: 'text', text: JSON.stringify(overview, null, 2), }, ], }; } case 'set_rpc_url': { if (!args.url || typeof args.url !== 'string') { throw new McpError(ErrorCode.InvalidParams, 'RPC URL is required'); } blockchain.setRpcUrl(args.url); return { content: [ { type: 'text', text: JSON.stringify({ success: true, rpcUrl: args.url }, null, 2), }, ], }; } case 'get_rpc_url': { const rpcUrl = blockchain.getRpcUrl(); return { content: [ { type: 'text', text: JSON.stringify({ rpcUrl }, null, 2), }, ], }; } case 'get_wallet_address_from_private_key': { if (!args.privateKey || typeof args.privateKey !== 'string') { throw new McpError(ErrorCode.InvalidParams, 'Private key is required'); } const walletAddress = blockchain.getWalletAddressFromPrivateKey(args.privateKey); return { content: [ { type: 'text', text: JSON.stringify({ walletAddress }, null, 2), }, ], }; } case 'send_edu': { if (!args.privateKey || typeof args.privateKey !== 'string') { throw new McpError(ErrorCode.InvalidParams, 'Private key is required'); } if (!args.toAddress || typeof args.toAddress !== 'string') { throw new McpError(ErrorCode.InvalidParams, 'Recipient address is required'); } if (!args.amount || typeof args.amount !== 'string') { throw new McpError(ErrorCode.InvalidParams, 'Amount is required'); } // Get wallet address from private key for information const fromAddress = blockchain.getWalletAddressFromPrivateKey(args.privateKey); // Proceed with the transaction const result = await blockchain.sendEdu(args.privateKey, args.toAddress, args.amount); // Add from address to the result for better context const enhancedResult = { ...result, fromAddress }; return { content: [ { type: 'text', text: JSON.stringify(enhancedResult, null, 2), }, ], }; } case 'send_erc20_token': { if (!args.privateKey || typeof args.privateKey !== 'string') { throw new McpError(ErrorCode.InvalidParams, 'Private key is required'); } if (!args.tokenAddress || typeof args.tokenAddress !== 'string') { throw new McpError(ErrorCode.InvalidParams, 'Token address is required'); } if (!args.toAddress || typeof args.toAddress !== 'string') { throw new McpError(ErrorCode.InvalidParams, 'Recipient address is required'); } if (!args.amount || typeof args.amount !== 'string') { throw new McpError(ErrorCode.InvalidParams, 'Amount is required'); } // Get wallet address from private key for information const fromAddress = blockchain.getWalletAddressFromPrivateKey(args.privateKey); // Try to get token details for better context let tokenSymbol = 'Unknown'; try { const provider = blockchain.getProvider(); const tokenContract = new ethers.Contract(args.tokenAddress, ['function symbol() view returns (string)'], provider); tokenSymbol = await tokenContract.symbol(); } catch (error) { console.error('Error fetching token symbol:', error); } // Proceed with the transaction const result = await blockchain.sendErc20Token(args.privateKey, args.tokenAddress, args.toAddress, args.amount); // Add from address and token symbol to the result for better context const enhancedResult = { ...result, fromAddress, tokenSymbol }; return { content: [ { type: 'text', text: JSON.stringify(enhancedResult, null, 2), }, ], }; } case 'get_swap_quote': { if (!args.tokenIn || typeof args.tokenIn !== 'string') { throw new McpError(ErrorCode.InvalidParams, 'Input token address is required'); } if (!args.tokenOut || typeof args.tokenOut !== 'string') { throw new McpError(ErrorCode.InvalidParams, 'Output token address is required'); } if (!args.amountIn || typeof args.amountIn !== 'string') { throw new McpError(ErrorCode.InvalidParams, 'Input amount is required'); } const slippagePercentage = typeof args.slippagePercentage === 'number' ? args.slippagePercentage : 0.5; const quote = await swap.getSwapQuote(args.tokenIn, args.tokenOut, args.amountIn, slippagePercentage); return { content: [ { type: 'text', text: JSON.stringify({ inputToken: { address: args.tokenIn, symbol: quote.tokenInSymbol, decimals: quote.tokenInDecimals, amount: args.amountIn, rawAmount: ethers.parseUnits(args.amountIn, quote.tokenInDecimals).toString() }, outputToken: { address: args.tokenOut, symbol: quote.tokenOutSymbol, decimals: quote.tokenOutDecimals, amount: quote.formattedAmountOut, minimumAmount: quote.formattedMinimumAmountOut, rawAmount: quote.amountOut, rawMinimumAmount: quote.minimumAmountOut }, exchangeRate: (Number(quote.formattedAmountOut) / Number(args.amountIn)).toString(), priceImpact: quote.priceImpact.toFixed(2), routeType: quote.route.type, slippage: slippagePercentage.toString(), note: "Amounts are formatted using the token's decimal places. Raw amounts are in wei units." }, null, 2), }, ], }; } case 'swap_tokens': { if (!args.privateKey || typeof args.privateKey !== 'string') { throw new McpError(ErrorCode.InvalidParams, 'Private key is required'); } if (!args.tokenIn || typeof args.tokenIn !== 'string') { throw new McpError(ErrorCode.InvalidParams, 'Input token address is required'); } if (!args.tokenOut || typeof args.tokenOut !== 'string') { throw new McpError(ErrorCode.InvalidParams, 'Output token address is required'); } if (!args.amountIn || typeof args.amountIn !== 'string') { throw new McpError(ErrorCode.InvalidParams, 'Input amount is required'); } const slippagePercentage = typeof args.slippagePercentage === 'number' ? args.slippagePercentage : 0.5; const fee = typeof args.fee === 'number' ? args.fee : 3000; // Default to 0.3% const result = await swap.swapExactTokensForTokens( args.privateKey, args.tokenIn, args.tokenOut, args.amountIn, slippagePercentage ); return { content: [ { type: 'text', text: JSON.stringify(result, null, 2), }, ], }; } case 'swap_edu_for_tokens': { if (!args.privateKey || typeof args.privateKey !== 'string') { throw new McpError(ErrorCode.InvalidParams, 'Private key is required'); } if (!args.tokenOut || typeof args.tokenOut !== 'string') { throw new McpError(ErrorCode.InvalidParams, 'Output token address is required'); } if (!args.amountIn || typeof args.amountIn !== 'string') { throw new McpError(ErrorCode.InvalidParams, 'Input amount is required'); } const slippagePercentage = typeof args.slippagePercentage === 'number' ? args.slippagePercentage : 0.5; const fee = typeof args.fee === 'number' ? args.fee : 3000; // Default to 0.3% const result = await swap.swapExactEDUForTokens( args.privateKey, args.tokenOut, args.amountIn, slippagePercentage ); return { content: [ { type: 'text', text: JSON.stringify(result, null, 2), }, ], }; } case 'swap_tokens_for_edu': { if (!args.privateKey || typeof args.privateKey !== 'string') { throw new McpError(ErrorCode.InvalidParams, 'Private key is required'); } if (!args.tokenIn || typeof args.tokenIn !== 'string') { throw new McpError(ErrorCode.InvalidParams, 'Input token address is required'); } if (!args.amountIn || typeof args.amountIn !== 'string') { throw new McpError(ErrorCode.InvalidParams, 'Input amount is required'); } const slippagePercentage = typeof args.slippagePercentage === 'number' ? args.slippagePercentage : 0.5; const fee = typeof args.fee === 'number' ? args.fee : 3000; // Default to 0.3% const result = await swap.swapExactTokensForEDU( args.privateKey, args.tokenIn, args.amountIn, slippagePercentage ); return { content: [ { type: 'text', text: JSON.stringify(result, null, 2), }, ], }; } case 'get_external_market_data': { try { const data = await external_market.getExternalMarketData(); return { content: [ { type: 'text', text: JSON.stringify(data, null, 2), }, ], }; } catch (error) { console.error('Error getting external market data:', error); return { content: [ { type: 'text', text: JSON.stringify({ error: 'Failed to fetch external market data', message: (error as Error).message, note: 'You may need to update the external market API configuration' }, null, 2), }, ], isError: true, }; } } case 'check_arbitrage_opportunities': { try { const threshold = typeof args.threshold === 'number' ? args.threshold : 1.0; const opportunities = await external_market.checkArbitrageOpportunities(threshold); return { content: [ { type: 'text', text: JSON.stringify(opportunities, null, 2), }, ], }; } catch (error) { console.error('Error checking arbitrage opportunities:', error); return { content: [ { type: 'text', text: JSON.stringify({ error: 'Failed to check arbitrage opportunities', message: (error as Error).message, note: 'You may need to update the external market API configuration' }, null, 2), }, ], isError: true, }; } } case 'update_external_market_config': { try { const newConfig: any = {}; if (typeof args.apiUrl === 'string') { newConfig.apiUrl = args.apiUrl; } if (typeof args.apiKey === 'string') { newConfig.apiKey = args.apiKey; } if (typeof args.symbols === 'object' && args.symbols !== null) { newConfig.symbols = {} as { EDU: string; USD: string }; if (typeof (args.symbols as any).EDU === 'string') { newConfig.symbols.EDU = (args.symbols as any).EDU; } if (typeof (args.symbols as any).USD === 'string') { newConfig.symbols.USD = (args.symbols as any).USD; } } external_market.updateConfig(newConfig); const currentConfig = external_market.getConfig(); return { content: [ { type: 'text', text: JSON.stringify({ success: true, message: 'External market API configuration updated', config: currentConfig }, null, 2), }, ], }; } catch (error) { console.error('Error updating external market config:', error); return { content: [ { type: 'text', text: JSON.stringify({ error: 'Failed to update external market API configuration', message: (error as Error).message }, null, 2), }, ], isError: true, }; } } case 'get_external_market_config': { try { const config = external_market.getConfig(); return { content: [ { type: 'text', text: JSON.stringify(config, null, 2), }, ], }; } catch (error) { console.error('Error getting external market config:', error); return { content: [ { type: 'text', text: JSON.stringify({ error: 'Failed to get external market API configuration', message: (error as Error).message }, null, 2), }, ], isError: true, }; } } case 'wrap_edu': { if (!args.privateKey || typeof args.privateKey !== 'string') { throw new McpError(ErrorCode.InvalidParams, 'Private key is required'); } if (!args.amount || typeof args.amount !== 'string') { throw new McpError(ErrorCode.InvalidParams, 'Amount is required'); } try { const result = await swap.wrapEDU(args.privateKey, args.amount); return { content: [ { type: 'text', text: JSON.stringify({ ...result, message: `Successfully wrapped ${args.amount} EDU to WEDU`, note: "WEDU (Wrapped EDU) is required for interacting with SailFish DEX. You can unwrap it back to EDU at any time." }, null, 2), }, ], }; } catch (error) { console.error('Error wrapping EDU to WEDU:', error); return { content: [ { type: 'text', text: JSON.stringify({ error: 'Failed to wrap EDU to WEDU', message: (error as Error).message }, null, 2), }, ], isError: true, }; } } case 'unwrap_wedu': { if (!args.privateKey || typeof args.privateKey !== 'string') { throw new McpError(ErrorCode.InvalidParams, 'Private key is required'); } if (!args.amount || typeof args.amount !== 'string') { throw new McpError(ErrorCode.InvalidParams, 'Amount is required'); } try { const result = await swap.unwrapWEDU(args.privateKey, args.amount); return { content: [ { type: 'text', text: JSON.stringify({ ...result, message: `Successfully unwrapped ${args.amount} WEDU to EDU`, }, null, 2), }, ], }; } catch (error) { console.error('Error unwrapping WEDU to EDU:', error); return { content: [ { type: 'text', text: JSON.stringify({ error: 'Failed to unwrap WEDU to EDU', message: (error as Error).message }, null, 2), }, ], isError: true, }; } } default: throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`); } } catch (error) { if (error instanceof McpError) { throw error; } console.error('Error handling tool call:', error); return { content: [ { type: 'text', text: `Error: ${(error as Error).message}`, }, ], isError: true, }; } }); } async run() { const transport = new StdioServerTransport(); await this.server.connect(transport); console.error('EDUCHAIN Agent Kit running on stdio'); } } const server = new SailFishSubgraphServer(); server.run().catch(console.error);