Skip to main content
Glama
clumsynonono

Aave Liquidation MCP Server

by clumsynonono
index.ts15.2 kB
#!/usr/bin/env node /** * Aave Liquidation MCP Server * Provides tools for analyzing Aave V3 liquidation opportunities on Ethereum mainnet */ import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { CallToolRequestSchema, ListToolsRequestSchema, ErrorCode, McpError, } from '@modelcontextprotocol/sdk/types.js'; import dotenv from 'dotenv'; import { ethers } from 'ethers'; import { AaveClient } from './aave-client.js'; // Load environment variables dotenv.config(); // Validate required environment variables const RPC_URL = process.env.ETHEREUM_RPC_URL; if (!RPC_URL) { throw new Error('ETHEREUM_RPC_URL environment variable is required'); } // Initialize Aave client const aaveClient = new AaveClient(RPC_URL); // Initialize MCP server const server = new Server( { name: 'aave-liquidation-mcp', version: '1.0.0', }, { capabilities: { tools: {}, }, } ); /** * List available tools */ server.setRequestHandler(ListToolsRequestSchema, async () => { return { tools: [ { name: 'get_user_health', description: 'Get health factor and account data for a specific Ethereum address on Aave V3. Returns collateral, debt, and liquidation status.', inputSchema: { type: 'object', properties: { address: { type: 'string', description: 'Ethereum address to check (must be a valid address)', }, }, required: ['address'], }, }, { name: 'analyze_liquidation', description: 'Analyze a user position for liquidation opportunity. Returns detailed information including collateral assets, debt assets, risk level, and potential profit.', inputSchema: { type: 'object', properties: { address: { type: 'string', description: 'Ethereum address to analyze (must be a valid address)', }, }, required: ['address'], }, }, { name: 'get_user_positions', description: 'Get detailed breakdown of a user collateral and debt positions across all Aave V3 assets.', inputSchema: { type: 'object', properties: { address: { type: 'string', description: 'Ethereum address to query', }, }, required: ['address'], }, }, { name: 'get_aave_reserves', description: 'Get list of all available reserves (assets) in Aave V3 protocol with their configuration.', inputSchema: { type: 'object', properties: {}, }, }, { name: 'get_asset_price', description: 'Get current price for a specific asset from Aave oracle.', inputSchema: { type: 'object', properties: { assetAddress: { type: 'string', description: 'Token contract address', }, }, required: ['assetAddress'], }, }, { name: 'get_protocol_status', description: 'Get general Aave V3 protocol status including current block number.', inputSchema: { type: 'object', properties: {}, }, }, { name: 'batch_check_addresses', description: 'Batch check multiple Ethereum addresses for liquidation opportunities. Returns a summary of all addresses with their health status.', inputSchema: { type: 'object', properties: { addresses: { type: 'array', items: { type: 'string', }, description: 'Array of Ethereum addresses to check (max 20 addresses)', }, }, required: ['addresses'], }, }, { name: 'validate_address', description: 'Validate if a string is a valid Ethereum address format.', inputSchema: { type: 'object', properties: { address: { type: 'string', description: 'Address string to validate', }, }, required: ['address'], }, }, ], }; }); /** * Handle tool execution */ server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; try { switch (name) { case 'get_user_health': { const address = args?.address as string; if (!address || typeof address !== 'string') { throw new McpError( ErrorCode.InvalidParams, 'address parameter is required and must be a string' ); } if (!aaveClient.isValidAddress(address)) { throw new McpError( ErrorCode.InvalidParams, 'Invalid Ethereum address format' ); } const accountData = await aaveClient.getUserAccountData(address); return { content: [ { type: 'text', text: JSON.stringify( { address: accountData.address, healthFactor: accountData.healthFactorFormatted, totalCollateralUSD: parseFloat( ethers.formatUnits(accountData.totalCollateralBase, 8) ).toFixed(2), totalDebtUSD: parseFloat( ethers.formatUnits(accountData.totalDebtBase, 8) ).toFixed(2), availableBorrowsUSD: parseFloat( ethers.formatUnits(accountData.availableBorrowsBase, 8) ).toFixed(2), liquidationThreshold: parseFloat( (Number(accountData.currentLiquidationThreshold) / 10000).toFixed(4) ), ltv: parseFloat((Number(accountData.ltv) / 10000).toFixed(4)), isLiquidatable: accountData.isLiquidatable, isAtRisk: accountData.isAtRisk, status: accountData.isLiquidatable ? 'LIQUIDATABLE' : accountData.isAtRisk ? 'AT_RISK' : 'HEALTHY', }, null, 2 ), }, ], }; } case 'analyze_liquidation': { const address = args?.address as string; if (!address || typeof address !== 'string') { throw new McpError( ErrorCode.InvalidParams, 'address parameter is required and must be a string' ); } if (!aaveClient.isValidAddress(address)) { throw new McpError( ErrorCode.InvalidParams, 'Invalid Ethereum address format' ); } const opportunity = await aaveClient.analyzeLiquidationOpportunity(address); if (!opportunity) { return { content: [ { type: 'text', text: JSON.stringify( { message: 'No liquidation opportunity found. Position is healthy.', address, }, null, 2 ), }, ], }; } return { content: [ { type: 'text', text: JSON.stringify(opportunity, null, 2), }, ], }; } case 'get_user_positions': { const address = args?.address as string; if (!address || typeof address !== 'string') { throw new McpError( ErrorCode.InvalidParams, 'address parameter is required and must be a string' ); } if (!aaveClient.isValidAddress(address)) { throw new McpError( ErrorCode.InvalidParams, 'Invalid Ethereum address format' ); } const positions = await aaveClient.getUserReserves(address); return { content: [ { type: 'text', text: JSON.stringify( { address, collateralPositions: positions.collateral, debtPositions: positions.debt, }, null, 2 ), }, ], }; } case 'get_aave_reserves': { const reserves = await aaveClient.getAllReserves(); return { content: [ { type: 'text', text: JSON.stringify( { totalReserves: reserves.length, reserves: reserves.map((r) => ({ symbol: r.symbol, address: r.tokenAddress, decimals: r.decimals, ltv: parseFloat((Number(r.ltv) / 10000).toFixed(4)), liquidationThreshold: parseFloat((Number(r.liquidationThreshold) / 10000).toFixed(4)), liquidationBonus: parseFloat(((Number(r.liquidationBonus) - 10000) / 10000).toFixed(4)), canBeCollateral: r.usageAsCollateralEnabled, canBeBorrowed: r.borrowingEnabled, isActive: r.isActive, })), }, null, 2 ), }, ], }; } case 'get_asset_price': { const assetAddress = args?.assetAddress as string; if (!assetAddress || typeof assetAddress !== 'string') { throw new McpError( ErrorCode.InvalidParams, 'assetAddress parameter is required and must be a string' ); } if (!aaveClient.isValidAddress(assetAddress)) { throw new McpError( ErrorCode.InvalidParams, 'Invalid Ethereum address format' ); } const price = await aaveClient.getAssetPrice(assetAddress); return { content: [ { type: 'text', text: JSON.stringify( { assetAddress, priceUSD: price, }, null, 2 ), }, ], }; } case 'get_protocol_status': { const blockNumber = await aaveClient.getBlockNumber(); return { content: [ { type: 'text', text: JSON.stringify( { protocol: 'Aave V3', network: 'Ethereum Mainnet', blockNumber, poolAddress: '0x87870Bca3F3fD6335C3F4ce8392D69350B4fA4E2', status: 'operational', }, null, 2 ), }, ], }; } case 'batch_check_addresses': { const addresses = args?.addresses as string[]; if (!Array.isArray(addresses) || addresses.length === 0) { throw new McpError( ErrorCode.InvalidParams, 'addresses parameter is required and must be a non-empty array' ); } if (addresses.length > 20) { throw new McpError( ErrorCode.InvalidParams, 'Maximum 20 addresses allowed per batch request' ); } // Validate all addresses first const invalidAddresses = addresses.filter( (addr) => typeof addr !== 'string' || !aaveClient.isValidAddress(addr) ); if (invalidAddresses.length > 0) { throw new McpError( ErrorCode.InvalidParams, `Invalid Ethereum addresses: ${invalidAddresses.join(', ')}` ); } const results = await aaveClient.batchAnalyzeLiquidation(addresses); // Calculate summary statistics let liquidatable = 0; let atRisk = 0; let healthy = 0; let failed = 0; const formattedResults = results.map((r) => { // Count statistics if (r.error) { failed++; } else if (r.opportunity?.riskLevel === 'HIGH') { liquidatable++; } else if (r.opportunity) { atRisk++; } else { healthy++; } return { address: r.address, status: r.error ? 'ERROR' : r.opportunity ? r.opportunity.riskLevel === 'HIGH' ? 'LIQUIDATABLE' : 'AT_RISK' : 'HEALTHY', healthFactor: r.opportunity?.healthFactor || 'N/A', totalDebtUSD: r.opportunity?.totalDebtUSD || '0', riskLevel: r.opportunity?.riskLevel || 'NONE', error: r.error, }; }); const summary = { totalChecked: addresses.length, successful: addresses.length - failed, failed, liquidatable, atRisk, healthy, results: formattedResults, }; return { content: [ { type: 'text', text: JSON.stringify(summary, null, 2), }, ], }; } case 'validate_address': { const address = args?.address as string; if (!address || typeof address !== 'string') { throw new McpError( ErrorCode.InvalidParams, 'address parameter is required and must be a string' ); } const isValid = aaveClient.isValidAddress(address); return { content: [ { type: 'text', text: JSON.stringify( { address, isValid, message: isValid ? 'Valid Ethereum address format' : 'Invalid Ethereum address format', }, null, 2 ), }, ], }; } default: throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`); } } catch (error) { if (error instanceof McpError) { throw error; } const errorMessage = error instanceof Error ? error.message : String(error); throw new McpError(ErrorCode.InternalError, `Tool execution failed: ${errorMessage}`); } }); /** * Start the server */ async function main() { const transport = new StdioServerTransport(); await server.connect(transport); console.error('Aave Liquidation MCP Server running on stdio'); } main().catch((error) => { console.error('Fatal error:', error); process.exit(1); });

Implementation Reference

Latest Blog Posts

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/clumsynonono/aave-liquidation-mcp'

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