index.js•21.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();