uniswap-trader-mcp
by kukapay
Verified
const { McpServer } = require("@modelcontextprotocol/sdk/server/mcp.js");
const { StdioServerTransport } = require("@modelcontextprotocol/sdk/server/stdio.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 CHAIN_CONFIGS = require('./chainConfigs');
// Import utilities from ethers.utils for v5
const { parseUnits, formatUnits } = ethers.utils;
const WALLET_PRIVATE_KEY = process.env.WALLET_PRIVATE_KEY;
if (!WALLET_PRIVATE_KEY) {
throw new Error("WALLET_PRIVATE_KEY environment variable is required");
}
// 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) {
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, symbol = "UNKNOWN", name = "Unknown Token") {
const config = CHAIN_CONFIGS[chainId];
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();
console.log('=>', 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) {
if (isNative) {
const balance = await provider.getBalance(wallet.address);
if (balance.isZero()) {
throw new Error(`Zero ${CHAIN_CONFIGS[provider.network.chainId].name} 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 {
const { provider, router, config } = getChainContext(chainId);
const tokenA = await createToken(chainId, tokenIn, provider);
const tokenB = await createToken(chainId, tokenOut, provider);
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 {
const { provider, router, config } = getChainContext(chainId);
const wallet = new ethers.Wallet(WALLET_PRIVATE_KEY, provider);
const isNativeIn = !tokenIn || tokenIn.toLowerCase() === "native";
const isNativeOut = !tokenOut || tokenOut.toLowerCase() === "native";
const tokenA = await createToken(chainId, isNativeIn ? config.weth : tokenIn, provider);
const tokenB = await createToken(chainId, isNativeOut ? config.weth : tokenOut, provider);
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: 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,
}
);
if (!route) throw new Error("No route found");
// Check balance before swap
await checkBalance(provider, wallet, isNativeIn ? null : tokenA.address, isNativeIn);
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();
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) {
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}.`
}
}]
})
);
// Start the server without Infura check
async function startServer() {
try {
const transport = new StdioServerTransport();
await server.connect(transport);
} catch (error) {
console.error(`Failed to start server: ${error.message}`);
process.exit(1);
}
}
startServer();