Web3 MCP Server

import { JsonRpcProvider, formatEther, formatUnits, Contract, Wallet, parseUnits, MaxUint256 } from 'ethers'; import { z } from "zod"; import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; // Network configurations interface NetworkConfig { name: string; rpc: string; chainId: number; currencySymbol: string; explorer: string; } const NETWORKS: { [key: string]: NetworkConfig } = { ethereum: { name: "Ethereum", rpc: process.env.ETH_RPC_URL || "https://eth-mainnet.g.alchemy.com/v2/demo", chainId: 1, currencySymbol: "ETH", explorer: "https://etherscan.io" }, base: { name: "Base", rpc: process.env.BASE_RPC_URL || "https://mainnet.base.org", chainId: 8453, currencySymbol: "ETH", explorer: "https://basescan.org" }, arbitrum: { name: "Arbitrum", rpc: process.env.ARBITRUM_RPC_URL || "https://arb1.arbitrum.io/rpc", chainId: 42161, currencySymbol: "ETH", explorer: "https://arbiscan.io" }, optimism: { name: "Optimism", rpc: process.env.OPTIMISM_RPC_URL || "https://mainnet.optimism.io", chainId: 10, currencySymbol: "ETH", explorer: "https://optimistic.etherscan.io" }, bsc: { name: "BNB Smart Chain", rpc: process.env.BSC_RPC_URL || "https://bsc-dataseed.binance.org", chainId: 56, currencySymbol: "BNB", explorer: "https://bscscan.com" }, polygon: { name: "Polygon", rpc: process.env.POLYGON_RPC_URL || "https://polygon-rpc.com", chainId: 137, currencySymbol: "MATIC", explorer: "https://polygonscan.com" }, avalanche: { name: "Avalanche", rpc: process.env.AVALANCHE_RPC_URL || "https://api.avax.network/ext/bc/C/rpc", chainId: 43114, currencySymbol: "AVAX", explorer: "https://snowtrace.io" }, berachain: { name: "Berachain", rpc: process.env.BERACHAIN_RPC_URL || "https://rpc.berachain.com", chainId: 80094, currencySymbol: "BERA", explorer: "https://berascan.com" }, sonic: { name: "Sonic", rpc: process.env.SONIC_RPC_URL || "https://rpc.soniclabs.com/", chainId: 2024, currencySymbol: "SONIC", explorer: "https://explorer.sonic.ooo" } }; // Initialize providers for each network const providers: { [key: string]: JsonRpcProvider } = {}; for (const [network, config] of Object.entries(NETWORKS)) { providers[network] = new JsonRpcProvider(config.rpc); } // ERC-20 minimal ABI const ERC20_ABI = [ "function balanceOf(address owner) view returns (uint256)", "function decimals() view returns (uint8)", "function symbol() view returns (string)", "function transfer(address to, uint256 amount) returns (bool)", "function approve(address spender, uint256 amount) returns (bool)" ]; export function registerEvmTools(server: McpServer) { // Get native token balance for any EVM network server.tool( "getEvmBalance", "Get native token balance for an EVM address on any supported network", { address: z.string().describe("EVM account address"), network: z.string().describe("Network name (ethereum, base, arbitrum, optimism, bsc, polygon, avalanche, berachain, sonic)"), }, async ({ address, network }) => { try { if (!NETWORKS[network]) { return { content: [ { type: "text", text: `Unsupported network: ${network}. Supported networks are: ${Object.keys(NETWORKS).join(", ")}`, }, ], }; } const provider = providers[network]; const balance = await provider.getBalance(address); const formattedBalance = formatEther(balance); const networkConfig = NETWORKS[network]; return { content: [ { type: "text", text: `Balance on ${networkConfig.name}:\n${formattedBalance} ${networkConfig.currencySymbol}`, }, ], }; } catch (err) { const error = err as Error; return { content: [ { type: "text", text: `Failed to retrieve balance: ${error.message}`, }, ], }; } } ); // Get ERC-20 token balance for any EVM network server.tool( "getEvmTokenBalance", "Get ERC-20 token balance for an address on any supported EVM network", { address: z.string().describe("EVM account address"), tokenAddress: z.string().describe("ERC-20 token contract address"), network: z.string().describe("Network name (ethereum, base, arbitrum, optimism, bsc, polygon, avalanche, berachain, sonic)"), }, async ({ address, tokenAddress, network }) => { try { if (!NETWORKS[network]) { return { content: [ { type: "text", text: `Unsupported network: ${network}. Supported networks are: ${Object.keys(NETWORKS).join(", ")}`, }, ], }; } const provider = providers[network]; const contract = new Contract(tokenAddress, ERC20_ABI, provider); const [balance, decimals, symbol] = await Promise.all([ contract.balanceOf(address), contract.decimals(), contract.symbol() ]); const formattedBalance = formatUnits(balance, decimals); const networkConfig = NETWORKS[network]; return { content: [ { type: "text", text: `Token Balance on ${networkConfig.name}:\n${formattedBalance} ${symbol} (${tokenAddress})\nExplorer: ${networkConfig.explorer}/token/${tokenAddress}` }, ], }; } catch (err) { const error = err as Error; return { content: [ { type: "text", text: `Failed to retrieve token balance: ${error.message}`, }, ], }; } } ); // Get gas price for any EVM network server.tool( "getGasPrice", "Get current gas price for any supported EVM network", { network: z.string().describe("Network name (ethereum, base, arbitrum, optimism, bsc, polygon, avalanche, berachain, sonic)"), }, async ({ network }) => { try { if (!NETWORKS[network]) { return { content: [ { type: "text", text: `Unsupported network: ${network}. Supported networks are: ${Object.keys(NETWORKS).join(", ")}`, }, ], }; } const provider = providers[network]; const feeData = await provider.getFeeData(); const gasPrice = formatUnits(feeData.gasPrice || 0, 'gwei'); const maxFeePerGas = feeData.maxFeePerGas ? formatUnits(feeData.maxFeePerGas, 'gwei') : null; const maxPriorityFeePerGas = feeData.maxPriorityFeePerGas ? formatUnits(feeData.maxPriorityFeePerGas, 'gwei') : null; const networkConfig = NETWORKS[network]; let response = `Gas Prices on ${networkConfig.name}:\nGas Price: ${gasPrice} Gwei`; if (maxFeePerGas) { response += `\nMax Fee: ${maxFeePerGas} Gwei`; } if (maxPriorityFeePerGas) { response += `\nMax Priority Fee: ${maxPriorityFeePerGas} Gwei`; } return { content: [ { type: "text", text: response, }, ], }; } catch (err) { const error = err as Error; return { content: [ { type: "text", text: `Failed to retrieve gas price: ${error.message}`, }, ], }; } } ); // Send native tokens on any EVM network server.tool( "sendEvmTransaction", "Send native tokens on any supported EVM network (using private key from .env)", { toAddress: z.string().describe("Recipient's address"), amount: z.string().describe("Amount to send (in native tokens)"), network: z.string().describe("Network name (ethereum, base, arbitrum, optimism, bsc, polygon, avalanche, berachain, sonic)"), }, async ({ toAddress, amount, network }) => { try { if (!NETWORKS[network]) { return { content: [ { type: "text", text: `Unsupported network: ${network}. Supported networks are: ${Object.keys(NETWORKS).join(", ")}` } ] }; } const provider = providers[network]; const networkConfig = NETWORKS[network]; // Get private key from environment variables if (!process.env.ETH_PRIVATE_KEY) { throw new Error('ETH_PRIVATE_KEY not found in environment variables'); } // Create wallet from private key const wallet = new Wallet(process.env.ETH_PRIVATE_KEY, provider); const fromAddress = wallet.address; // Get current gas price and nonce const [gasPrice, nonce] = await Promise.all([ provider.getFeeData(), provider.getTransactionCount(fromAddress) ]); // Prepare transaction const tx = { to: toAddress, value: parseUnits(amount), nonce: nonce, maxPriorityFeePerGas: gasPrice.maxPriorityFeePerGas, maxFeePerGas: gasPrice.maxFeePerGas, gasLimit: 21000, // Standard ETH transfer chainId: networkConfig.chainId }; // Sign and send transaction const txResponse = await wallet.sendTransaction(tx); return { content: [ { type: "text", text: `Transaction sent on ${networkConfig.name}!\nFrom: ${fromAddress}\nTo: ${toAddress}\nAmount: ${amount} ${networkConfig.currencySymbol}\nTransaction Hash: ${txResponse.hash}\nExplorer Link: ${networkConfig.explorer}/tx/${txResponse.hash}` } ] }; } catch (err) { const error = err as Error; return { content: [ { type: "text", text: `Failed to send transaction: ${error.message}` } ] }; } } ); // Send ERC-20 tokens on any EVM network server.tool( "sendEvmToken", "Send ERC-20 tokens on any supported EVM network (using private key from .env)", { toAddress: z.string().describe("Recipient's address"), tokenAddress: z.string().describe("Token contract address"), amount: z.string().describe("Amount to send (in token units)"), network: z.string().describe("Network name (ethereum, base, arbitrum, optimism, bsc, polygon, avalanche, berachain)"), }, async ({ toAddress, tokenAddress, amount, network }) => { try { if (!NETWORKS[network]) { return { content: [ { type: "text", text: `Unsupported network: ${network}. Supported networks are: ${Object.keys(NETWORKS).join(", ")}` } ] }; } const provider = providers[network]; const networkConfig = NETWORKS[network]; // Get private key from environment variables if (!process.env.ETH_PRIVATE_KEY) { throw new Error('ETH_PRIVATE_KEY not found in environment variables'); } // Create wallet from private key const wallet = new Wallet(process.env.ETH_PRIVATE_KEY, provider); const fromAddress = wallet.address; // Create contract instance const contract = new Contract(tokenAddress, ERC20_ABI, wallet); // Get token details const [decimals, symbol] = await Promise.all([ contract.decimals(), contract.symbol() ]); // Convert amount to token units const amountInTokenUnits = parseUnits(amount, decimals); // Send transaction const txResponse = await contract.transfer(toAddress, amountInTokenUnits, { gasLimit: 100000 // Estimated gas limit for token transfers }); return { content: [ { type: "text", text: `Token transfer sent on ${networkConfig.name}!\nFrom: ${fromAddress}\nTo: ${toAddress}\nAmount: ${amount} ${symbol}\nToken Address: ${tokenAddress}\nTransaction Hash: ${txResponse.hash}\nExplorer Link: ${networkConfig.explorer}/tx/${txResponse.hash}` } ] }; } catch (err) { const error = err as Error; return { content: [ { type: "text", text: `Failed to send token transfer: ${error.message}` } ] }; } } ); // Approve ERC-20 token spending server.tool( "approveEvmToken", "Approve ERC-20 token spending on any supported EVM network (using private key from .env)", { spenderAddress: z.string().describe("Address to approve for spending"), tokenAddress: z.string().describe("Token contract address"), amount: z.string().optional().describe("Amount to approve (in token units, defaults to unlimited)"), network: z.string().describe("Network name (ethereum, base, arbitrum, optimism, bsc, polygon, avalanche, berachain)"), }, async ({ spenderAddress, tokenAddress, amount, network }) => { try { if (!NETWORKS[network]) { return { content: [ { type: "text", text: `Unsupported network: ${network}. Supported networks are: ${Object.keys(NETWORKS).join(", ")}` } ] }; } const provider = providers[network]; const networkConfig = NETWORKS[network]; // Get private key from environment variables if (!process.env.ETH_PRIVATE_KEY) { throw new Error('ETH_PRIVATE_KEY not found in environment variables'); } // Create wallet from private key const wallet = new Wallet(process.env.ETH_PRIVATE_KEY, provider); // Create contract instance const contract = new Contract(tokenAddress, ERC20_ABI, wallet); // Get token details const [decimals, symbol] = await Promise.all([ contract.decimals(), contract.symbol() ]); // Calculate approval amount const approvalAmount = amount ? parseUnits(amount, decimals) : MaxUint256; // Send approval transaction const txResponse = await contract.approve(spenderAddress, approvalAmount, { gasLimit: 60000 // Estimated gas limit for approvals }); const formattedAmount = amount || "unlimited"; return { content: [ { type: "text", text: `Token approval sent on ${networkConfig.name}!\nToken: ${symbol} (${tokenAddress})\nSpender: ${spenderAddress}\nAmount: ${formattedAmount}\nTransaction Hash: ${txResponse.hash}\nExplorer Link: ${networkConfig.explorer}/tx/${txResponse.hash}` } ] }; } catch (err) { const error = err as Error; return { content: [ { type: "text", text: `Failed to approve token spending: ${error.message}` } ] }; } } ); }