Skip to main content
Glama
index.js21.3 kB
const { McpServer } = require("@modelcontextprotocol/sdk/server/mcp.js"); const { z } = require("zod"); const ethers = require("ethers"); const { Token, CurrencyAmount, TradeType, Percent, SwapRouter, } = require("@uniswap/sdk-core"); const { AlphaRouter, SwapType } = require("@uniswap/smart-order-router"); // Define minimal ERC20 ABI with decimals function added const ERC20ABI = [ "function balanceOf(address account) external view returns (uint256)", "function approve(address spender, uint256 amount) external returns (bool)", "function symbol() external view returns (string)", "function decimals() external view returns (uint8)", ]; // Define minimal SwapRouter ABI for Uniswap V3 (only exactInput and exactOutput) const SwapRouterABI = [ "function exactInput(tuple(address recipient, uint256 deadline, uint256 amountIn, uint256 amountOutMinimum, address[] path) params) external payable returns (uint256 amountOut)", "function exactOutput(tuple(address recipient, uint256 deadline, uint256 amountOut, uint256 amountInMaximum, address[] path) params) external payable returns (uint256 amountIn)", "function multicall(bytes[] calldata data) external payable returns (bytes[] memory results)", ]; // Define minimal WETH9 ABI for deposit and withdraw const WETHABI = [ "function deposit() external payable", "function withdraw(uint256 wad) external", "function balanceOf(address account) external view returns (uint256)", ]; // Load environment variables and chain configurations require("dotenv").config(); const { getChainConfigs, getChainConfigsFromEnv } = require("./chainConfigs"); // Import utilities from ethers.utils for v5 const { parseUnits, formatUnits } = ethers.utils; // Global config storage for current request let currentConfig = { infuraKey: process.env.INFURA_KEY, walletPrivateKey: process.env.WALLET_PRIVATE_KEY, }; // Function to ensure configuration is available function ensureConfig() { if (!currentConfig.infuraKey) { throw new Error( "INFURA_KEY is required. Please provide it via Smithery config or environment variable." ); } if (!currentConfig.walletPrivateKey) { throw new Error( "WALLET_PRIVATE_KEY is required. Please provide it via Smithery config or environment variable." ); } } // Initialize MCP server const server = new McpServer({ name: "Uniswap Trader MCP", version: "1.0.0", description: "An MCP server for AI agents to automate trading strategies on Uniswap DEX across multiple blockchains", }); // Get provider and router for a specific chain function getChainContext(chainId, infuraKey = null) { // Use provided infuraKey or fall back to current config const apiKey = infuraKey || currentConfig.infuraKey; if (!apiKey) { throw new Error("INFURA_KEY is required but not provided"); } const CHAIN_CONFIGS = getChainConfigs(apiKey); const config = CHAIN_CONFIGS[chainId]; if (!config) { const supportedChains = Object.entries(CHAIN_CONFIGS) .map(([id, { name }]) => `${id} - ${name}`) .join(", "); throw new Error( `Unsupported chainId: ${chainId}. Supported chains: ${supportedChains}` ); } const provider = new ethers.providers.JsonRpcProvider(config.rpcUrl); const router = new AlphaRouter({ chainId, provider }); return { provider, router, config }; } // Create a token instance, fetching decimals for ERC-20 tokens async function createToken( chainId, address, provider, config, symbol = "UNKNOWN", name = "unknownToken" ) { if (!address || address.toLowerCase() === "native") { return new Token(chainId, config.weth, 18, symbol, name); // Native token defaults to 18 decimals } const tokenContract = new ethers.Contract(address, ERC20ABI, provider); const decimals = await tokenContract.decimals(); return new Token( chainId, ethers.utils.getAddress(address), decimals, symbol, name ); } // Check wallet balance, throw error if zero async function checkBalance( provider, wallet, tokenAddress, isNative = false, chainName = "Unknown" ) { if (isNative) { const balance = await provider.getBalance(wallet.address); if (balance.isZero()) { throw new Error( `Zero ${chainName} native token balance. Please deposit funds to ${wallet.address}.` ); } } else { const tokenContract = new ethers.Contract(tokenAddress, ERC20ABI, provider); const balance = await tokenContract.balanceOf(wallet.address); if (balance.isZero()) { const symbol = await tokenContract.symbol(); throw new Error( `Zero ${symbol} balance. Please deposit funds to ${wallet.address}.` ); } } } // Tool: Get price quote with Smart Order Router server.tool( "getPrice", "Get a price quote for a Uniswap swap, supporting multi-hop routes", { chainId: z .number() .default(1) .describe( "Chain ID (1: Ethereum, 10: Optimism, 137: Polygon, 42161: Arbitrum, 42220: Celo, 56: BNB Chain, 43114: Avalanche, 8453: Base)" ), tokenIn: z .string() .describe("Input token address ('NATIVE' for native token like ETH)"), tokenOut: z .string() .describe("Output token address ('NATIVE' for native token like ETH)"), amountIn: z .string() .optional() .describe("Exact input amount (required for exactIn trades)"), amountOut: z .string() .optional() .describe("Exact output amount (required for exactOut trades)"), tradeType: z .enum(["exactIn", "exactOut"]) .default("exactIn") .describe( "Trade type: exactIn requires amountIn, exactOut requires amountOut" ), }, async ({ chainId, tokenIn, tokenOut, amountIn, amountOut, tradeType }) => { try { ensureConfig(); console.log( "[DEBUG] executeSwap: Calling getChainContext with chainId:", chainId ); const { provider, router, config } = getChainContext( chainId, currentConfig.infuraKey ); console.log( "[DEBUG] executeSwap: getChainContext successful, chain name:", config.name ); const tokenA = await createToken(chainId, tokenIn, provider, config); const tokenB = await createToken(chainId, tokenOut, provider, config); if (tradeType === "exactIn" && !amountIn) { throw new Error("amountIn is required for exactIn trades"); } if (tradeType === "exactOut" && !amountOut) { throw new Error("amountOut is required for exactOut trades"); } const amount = tradeType === "exactIn" ? amountIn : amountOut; const decimals = tradeType === "exactIn" ? tokenA.decimals : tokenB.decimals; const amountWei = parseUnits(amount, decimals).toString(); const route = await router.route( CurrencyAmount.fromRawAmount( tradeType === "exactIn" ? tokenA : tokenB, amountWei ), tradeType === "exactIn" ? tokenB : tokenA, tradeType === "exactIn" ? TradeType.EXACT_INPUT : TradeType.EXACT_OUTPUT, { recipient: ethers.constants.AddressZero, slippageTolerance: new Percent(5, 1000), deadline: Math.floor(Date.now() / 1000) + 20 * 60, type: SwapType.SWAP_ROUTER_02, } ); if (!route) throw new Error("No route found"); return { content: [ { type: "text", text: JSON.stringify( { chainId, tradeType, price: route.trade.executionPrice.toSignificant(6), inputAmount: route.trade.inputAmount.toSignificant(6), outputAmount: route.trade.outputAmount.toSignificant(6), minimumReceived: route.trade .minimumAmountOut(new Percent(5, 1000)) .toSignificant(6), maximumInput: route.trade .maximumAmountIn(new Percent(5, 1000)) .toSignificant(6), route: route.trade.swaps.map((swap) => ({ tokenIn: swap.inputAmount.currency.address, tokenOut: swap.outputAmount.currency.address, fee: swap.route.pools[0].fee, })), estimatedGas: route.estimatedGasUsed.toString(), }, null, 2 ), }, ], }; } catch (error) { throw new Error( `Failed to get price: ${error.message}. Check network connection.` ); } } ); // Tool: Execute swap with Smart Order Router server.tool( "executeSwap", "Execute a swap on Uniswap with optimal multi-hop routing", { chainId: z .number() .default(1) .describe( "Chain ID (1: Ethereum, 10: Optimism, 137: Polygon, 42161: Arbitrum, 42220: Celo, 56: BNB Chain, 43114: Avalanche, 8453: Base)" ), tokenIn: z .string() .describe("Input token address ('NATIVE' for native token like ETH)"), tokenOut: z .string() .describe("Output token address ('NATIVE' for native token like ETH)"), amountIn: z .string() .optional() .describe("Exact input amount (required for exactIn trades)"), amountOut: z .string() .optional() .describe("Exact output amount (required for exactOut trades)"), tradeType: z .enum(["exactIn", "exactOut"]) .default("exactIn") .describe( "Trade type: exactIn requires amountIn, exactOut requires amountOut" ), slippageTolerance: z .number() .optional() .default(0.5) .describe("Slippage tolerance in percentage"), deadline: z .number() .optional() .default(20) .describe("Transaction deadline in minutes"), }, async ({ chainId, tokenIn, tokenOut, amountIn, amountOut, tradeType, slippageTolerance, deadline, }) => { try { console.log("[DEBUG] executeSwap: Received params:", { chainId, tokenIn, tokenOut, amountIn, amountOut, tradeType, slippageTolerance, deadline, }); console.log("[DEBUG] executeSwap: Current config at start:", { hasInfuraKey: !!currentConfig.infuraKey, hasWalletKey: !!currentConfig.walletPrivateKey, infuraKeyPrefix: currentConfig.infuraKey ? currentConfig.infuraKey.substring(0, 10) + "..." : "none", }); ensureConfig(); console.log("[DEBUG] executeSwap: ensureConfig passed"); const { provider, router, config } = getChainContext( chainId, currentConfig.infuraKey ); const wallet = new ethers.Wallet( currentConfig.walletPrivateKey, provider ); console.log( "[DEBUG] executeSwap: Wallet created, address:", wallet.address ); const isNativeIn = !tokenIn || tokenIn.toLowerCase() === "native"; const isNativeOut = !tokenOut || tokenOut.toLowerCase() === "native"; console.log("[DEBUG] executeSwap: Creating tokens..."); const tokenA = await createToken( chainId, isNativeIn ? config.weth : tokenIn, provider, config ); console.log( "[DEBUG] executeSwap: tokenA created:", tokenA.symbol, tokenA.address ); const tokenB = await createToken( chainId, isNativeOut ? config.weth : tokenOut, provider, config ); console.log( "[DEBUG] executeSwap: tokenB created:", tokenB.symbol, tokenB.address ); if (tradeType === "exactIn" && !amountIn) { throw new Error("amountIn is required for exactIn trades"); } if (tradeType === "exactOut" && !amountOut) { throw new Error("amountOut is required for exactOut trades"); } const amount = tradeType === "exactIn" ? amountIn : amountOut; const decimals = tradeType === "exactIn" ? tokenA.decimals : tokenB.decimals; const amountWei = parseUnits(amount, decimals).toString(); console.log("[DEBUG] executeSwap: Calculating route..."); const route = await router.route( CurrencyAmount.fromRawAmount( tradeType === "exactIn" ? tokenA : tokenB, amountWei ), tradeType === "exactIn" ? tokenB : tokenA, tradeType === "exactIn" ? TradeType.EXACT_INPUT : TradeType.EXACT_OUTPUT, { recipient: isNativeOut ? wallet.address : config.swapRouter, slippageTolerance: new Percent( Math.floor(slippageTolerance * 100), 10000 ), deadline: Math.floor(Date.now() / 1000) + deadline * 60, type: SwapType.SWAP_ROUTER_02, } ); console.log("[DEBUG] executeSwap: Route calculated:", !!route); if (!route) throw new Error("No route found"); console.log( "[DEBUG] executeSwap: Route found, proceeding to balance check" ); // Check balance before swap console.log("[DEBUG] executeSwap: Checking balance..."); await checkBalance( provider, wallet, isNativeIn ? null : tokenA.address, isNativeIn, config.name ); console.log("[DEBUG] executeSwap: Balance check passed"); const swapRouter = new ethers.Contract( config.swapRouter, SwapRouterABI, wallet ); const wethContract = new ethers.Contract(config.weth, WETHABI, wallet); // Approve token if not native input if (!isNativeIn) { const tokenContract = new ethers.Contract( tokenA.address, ERC20ABI, wallet ); const approvalTx = await tokenContract.approve( config.swapRouter, ethers.constants.MaxUint256 ); await approvalTx.wait(); } let tx; if (isNativeOut && tradeType === "exactOut") { // Execute swap to receive WETH tx = await wallet.sendTransaction({ to: config.swapRouter, data: route.methodParameters.calldata, value: route.methodParameters.value, gasLimit: route.estimatedGasUsed.mul(12).div(10), gasPrice: (await provider.getGasPrice()).mul(2), }); const receipt = await tx.wait(); // Convert WETH to native token const wethAmount = route.trade.outputAmount.quotient; const withdrawTx = await wethContract.withdraw(wethAmount); await withdrawTx.wait(); } else { // Execute swap directly, native input handled by SwapRouter via value tx = await wallet.sendTransaction({ to: config.swapRouter, data: route.methodParameters.calldata, value: isNativeIn && tradeType === "exactIn" ? amountWei : route.methodParameters.value, gasLimit: route.estimatedGasUsed.mul(12).div(10), gasPrice: (await provider.getGasPrice()).mul(2), }); await tx.wait(); } const receipt = await tx.wait(); console.log( "[DEBUG] executeSwap: Swap executed successfully, preparing response" ); return { content: [ { type: "text", text: JSON.stringify( { chainId, txHash: receipt.transactionHash, tradeType, amountIn: route.trade.inputAmount.toSignificant(6), outputAmount: route.trade.outputAmount.toSignificant(6), minimumReceived: route.trade .minimumAmountOut( new Percent(Math.floor(slippageTolerance * 100), 10000) ) .toSignificant(6), maximumInput: route.trade .maximumAmountIn( new Percent(Math.floor(slippageTolerance * 100), 10000) ) .toSignificant(6), fromToken: isNativeIn ? "NATIVE" : tokenIn, toToken: isNativeOut ? "NATIVE" : tokenOut, route: route.trade.swaps.map((swap) => ({ tokenIn: swap.inputAmount.currency.address, tokenOut: swap.outputAmount.currency.address, fee: swap.route.pools[0].fee, })), gasUsed: receipt.gasUsed.toString(), }, null, 2 ), }, ], }; } catch (error) { console.error( "[DEBUG] executeSwap: Error occurred:", error.message, error.stack ); throw new Error( `Swap failed: ${error.message}. Check wallet funds and network connection.` ); } } ); // Prompt: Generate swap suggestion with Smart Order Router server.prompt( "suggestSwap", { amount: z.string().describe("Amount to swap"), token: z .string() .describe("Starting token address ('NATIVE' for native token like ETH)"), tradeType: z .enum(["exactIn", "exactOut"]) .default("exactIn") .describe("Trade type"), }, ({ amount, token, tradeType }) => ({ messages: [ { role: "user", content: { type: "text", text: `Suggest the best token swap for ${amount} at ${token} on Uniswap V3 using smart order routing. Consider liquidity, fees, and optimal multi-hop paths. Trade type: ${tradeType}.`, }, }, ], }) ); // HTTP Server setup for Smithery streamable deployment const express = require("express"); const { randomUUID } = require("crypto"); const { StreamableHTTPServerTransport, } = require("@modelcontextprotocol/sdk/server/streamableHttp.js"); const { isInitializeRequest } = require("@modelcontextprotocol/sdk/types.js"); // Start HTTP server for streamable transport async function startServer() { try { const PORT = process.env.PORT || 3000; const app = express(); app.use(express.json()); // Map to store transports by session ID const transports = {}; // Configuration injection middleware for Smithery const injectConfig = (req, res, next) => { try { console.log("[DEBUG] injectConfig: Processing request URL:", req.url); // Parse configuration from query params if provided (Smithery pattern) const parsedUrl = new URL(req.url, `http://localhost:${PORT}`); const configParam = parsedUrl.searchParams.get("config"); console.log( "[DEBUG] injectConfig: config param present:", !!configParam ); const config = configParam ? JSON.parse(Buffer.from(configParam, "base64").toString()) : {}; console.log( "[DEBUG] injectConfig: Parsed config keys:", Object.keys(config) ); // Update current config for this request if (config.infuraKey) { currentConfig.infuraKey = config.infuraKey; console.log("[DEBUG] injectConfig: Set infuraKey"); } if (config.walletPrivateKey) { currentConfig.walletPrivateKey = config.walletPrivateKey; console.log("[DEBUG] injectConfig: Set walletPrivateKey"); } console.log("[DEBUG] injectConfig: Final currentConfig:", { hasInfuraKey: !!currentConfig.infuraKey, hasWalletKey: !!currentConfig.walletPrivateKey, }); } catch (error) { console.warn("[DEBUG] Config injection failed:", error.message); } next(); }; // Enable CORS for all routes app.use((req, res, next) => { res.setHeader("Access-Control-Allow-Origin", "*"); res.setHeader( "Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS" ); res.setHeader( "Access-Control-Allow-Headers", "Content-Type, mcp-session-id" ); res.setHeader("Access-Control-Expose-Headers", "mcp-session-id"); if (req.method === "OPTIONS") { res.writeHead(200); res.end(); return; } next(); }); // Apply config injection to MCP routes app.use("/mcp", injectConfig); // Handle POST requests for client-to-server communication app.post("/mcp", async (req, res) => { try { // Check for existing session ID const sessionId = req.headers["mcp-session-id"]; let transport; if (sessionId && transports[sessionId]) { // Reuse existing transport transport = transports[sessionId]; } else if (!sessionId && isInitializeRequest(req.body)) { // New initialization request transport = new StreamableHTTPServerTransport({ sessionIdGenerator: () => randomUUID(), onsessioninitialized: (sessionId) => { // Store the transport by session ID transports[sessionId] = transport; }, // DNS rebinding protection disabled for Smithery compatibility enableDnsRebindingProtection: false, }); // Clean up transport when closed transport.onclose = () => { if (transport.sessionId) { delete transports[transport.sessionId]; } }; // Connect MCP server to transport await server.connect(transport); } else { // Invalid request res.status(400).json({ jsonrpc: "2.0", error: { code: -32000, message: "Bad Request: No valid session ID provided", }, id: null, }); return; } // Handle the request using proper MCP transport await transport.handleRequest(req, res, req.body); } catch (error) { console.error("Request handling error:", error); if (!res.headersSent) { res.status(500).json({ jsonrpc: "2.0", error: { code: -32603, message: "Internal server error", }, id: null, }); } } }); // Reusable handler for GET and DELETE requests const handleSessionRequest = async (req, res) => { const sessionId = req.headers["mcp-session-id"]; if (!sessionId || !transports[sessionId]) { res.status(400).send("Invalid or missing session ID"); return; } const transport = transports[sessionId]; await transport.handleRequest(req, res); }; // Handle GET requests for server-to-client notifications via SSE app.get("/mcp", handleSessionRequest); // Handle DELETE requests for session termination app.delete("/mcp", handleSessionRequest); // Health check endpoint (preserved for monitoring) app.get("/health", (req, res) => { res.json({ status: "ok", timestamp: new Date().toISOString(), }); }); // 404 handler app.use((req, res) => { res.status(404).json({ error: "Not found" }); }); app.listen(PORT, () => { console.log(`Uniswap Trader MCP server listening on port ${PORT}`); console.log(`Health check: http://localhost:${PORT}/health`); console.log(`MCP endpoint: http://localhost:${PORT}/mcp`); }); } catch (error) { console.error(`Failed to start server: ${error.message}`); process.exit(1); } } startServer();

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/catwhisperingninja/uniswap-trader-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server