# Directory Structure ``` ├── .gitignore ├── index.js ├── LICENSE ├── package-lock.json ├── package.json └── README.md ``` # Files -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- ``` 1 | # Dependency directories 2 | node_modules/ 3 | npm-debug.log 4 | yarn-debug.log 5 | yarn-error.log 6 | 7 | # Environment variables 8 | .env 9 | .env.local 10 | .env.development.local 11 | .env.test.local 12 | .env.production.local 13 | 14 | # Build outputs 15 | dist/ 16 | build/ 17 | 18 | # IDE and editor files 19 | .idea/ 20 | .vscode/ 21 | *.swp 22 | *.swo 23 | .DS_Store 24 | ``` -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- ```markdown 1 | # Ethereum RPC MCP Server 2 | 3 | A Model Context Protocol (MCP) server for interacting with Ethereum blockchain. 4 | 5 | ## Overview 6 | 7 | This MCP server provides tools to query Ethereum blockchain data through standard JSON-RPC methods. It enables AI assistants and applications to interact with the Ethereum blockchain through a standardized protocol. 8 | 9 | ## Features 10 | 11 | This MCP server provides three key Ethereum RPC methods as tools: 12 | 13 | - **eth_getCode**: Retrieve the code at a specific Ethereum address 14 | - **eth_gasPrice**: Get the current gas price on the Ethereum network 15 | - **eth_getBalance**: Check the balance of an Ethereum account 16 | 17 | Note: More are coming 18 | 19 | ## Usage 20 | 21 | ### Adding to Cursor 22 | 23 | To add this MCP to Cursor: 24 | 25 | 1. First, clone this repository: 26 | ```bash 27 | git clone https://github.com/yourusername/eth-mpc.git 28 | ``` 29 | 30 | 2. Go to Cursor settings → MCP → Add new MCP server 31 | 3. Enter a name (e.g., "eth-mcp") 32 | 4. Select "command" as the type 33 | 5. Input the full path to the script: 34 | ``` 35 | node /path/to/eth-mpc/index.js 36 | ``` 37 | 38 |  39 | 40 | 6. Click "Add" to enable the server 41 | 42 | Once added, the Ethereum RPC tools will be available to use within Cursor. 43 | 44 | 45 | The server uses stdio transport, making it compatible with MCP clients like Claude Desktop, Cursor, and others. 46 | 47 | ## Testing with MCP Inspector 48 | 49 | The MCP Inspector is a development tool for testing and debugging MCP servers. It provides an interactive interface to test your MCP server's functionality without needing a full AI client. 50 | 51 | ### Running the Inspector 52 | 53 | To test your Ethereum RPC MCP server with the Inspector: 54 | 55 | To run the Inspector: 56 | ```bash 57 | npx @modelcontextprotocol/inspector 58 | ``` 59 | 60 | 2. Input the command and path 61 | 62 | 3. The Inspector will connect to your running MCP server and display available tools. 63 | 64 | ### Testing Tools with Inspector 65 | 66 | The Inspector allows you to: 67 | 68 | - View available tools and their descriptions 69 | - Test each tool with different parameters 70 | - See the responses in a structured format 71 | - Debug any issues with your MCP server implementation 72 | 73 | For example, to test the `eth_getBalance` tool: 74 | 1. Select the tool in the Inspector interface 75 | 2. Enter a valid Ethereum address (e.g., `0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045` - Vitalik's address) 76 | 3. Use the default block parameter (`latest`) 77 | 4. Submit the request and view the response 78 | 79 | 80 | ## Integration with MCP Clients 81 | 82 | This MCP server can be integrated with any MCP-compatible client, including: 83 | 84 | - Claude Desktop 85 | - Claude Code 86 | - Cursor (instructions above) 87 | - Cline 88 | - Other MCP-compatible applications 89 | 90 | When integrated, the client application can use the tools provided by this server to query Ethereum blockchain data directly. 91 | 92 | ## Understanding MCP 93 | 94 | Model Context Protocol (MCP) is an open standard that allows AI models to interact with various tools and services. It provides a standardized way for developers to expose APIs, data sources, and functionality to AI assistants. 95 | 96 | ### Learn More About MCP 97 | 98 | MCP servers like this one form part of an ecosystem that allows AI assistants to perform complex tasks across multiple services without requiring custom integration for each service. 99 | 100 | 📚 **Official Documentation**: [Model Context Protocol Overview](https://modelcontextprotocol.io/sdk/java/mcp-overview) 101 | 102 | ## License 103 | 104 | MIT 105 | 106 | ## Contributing 107 | 108 | Contributions are welcome! Please feel free to submit a Pull Request. 109 | ``` -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "name": "eth-mcp", 3 | "version": "1.0.0", 4 | "description": "an mcp to interact with the evm", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "MIT", 11 | "dependencies": { 12 | "@modelcontextprotocol/sdk": "^1.6.1", 13 | "axios": "^1.8.1", 14 | "zod": "^3.24.2" 15 | } 16 | } 17 | ``` -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- ```javascript 1 | const axios = require('axios'); 2 | const { McpServer } = require('@modelcontextprotocol/sdk/server/mcp.js'); 3 | const { StdioServerTransport } = require('@modelcontextprotocol/sdk/server/stdio.js'); 4 | const { z } = require('zod'); 5 | 6 | // Redirect console.log to stderr to avoid breaking the MCP protocol 7 | const originalConsoleLog = console.log; 8 | console.log = function() { 9 | console.error.apply(console, arguments); 10 | }; 11 | 12 | // Ethereum RPC URL 13 | const ETH_RPC_URL = 'https://eth.llamarpc.com'; 14 | 15 | // Initialize the MCP server 16 | const server = new McpServer({ 17 | name: 'ethereum-rpc', 18 | version: '1.0.0' 19 | }); 20 | 21 | // Define prompts for common Ethereum interactions 22 | server.prompt( 23 | 'check-contract', 24 | { 25 | address: z.string().regex(/^0x[a-fA-F0-9]{40}$/).describe('The Ethereum address to check') 26 | }, 27 | ({ address }) => ({ 28 | messages: [ 29 | { 30 | role: 'user', 31 | content: { 32 | type: 'text', 33 | text: `Please analyze the contract at address ${address}: 34 | 1. First, get the contract code 35 | 2. Check if it's a contract or regular address 36 | 3. If it's a contract, help me understand what type of contract it might be based on the code` 37 | } 38 | } 39 | ] 40 | }) 41 | ); 42 | 43 | server.prompt( 44 | 'check-wallet', 45 | { 46 | address: z.string().regex(/^0x[a-fA-F0-9]{40}$/).describe('The Ethereum address to check') 47 | }, 48 | ({ address }) => ({ 49 | messages: [ 50 | { 51 | role: 'user', 52 | content: { 53 | type: 'text', 54 | text: `Please analyze the wallet at address ${address}: 55 | 1. Get the current balance 56 | 2. Format the balance in both Wei and ETH 57 | 3. Provide context about whether this is a significant amount` 58 | } 59 | } 60 | ] 61 | }) 62 | ); 63 | 64 | server.prompt( 65 | 'gas-analysis', 66 | {}, 67 | () => ({ 68 | messages: [ 69 | { 70 | role: 'user', 71 | content: { 72 | type: 'text', 73 | text: `Please analyze the current gas situation: 74 | 1. Get the current gas price 75 | 2. Convert it to Gwei for readability 76 | 3. Suggest whether this is a good time for transactions based on historical averages` 77 | } 78 | } 79 | ] 80 | }) 81 | ); 82 | 83 | // Helper function to make RPC calls 84 | async function makeRpcCall(method, params = []) { 85 | try { 86 | const response = await axios.post(ETH_RPC_URL, { 87 | jsonrpc: '2.0', 88 | id: 1, 89 | method, 90 | params 91 | }); 92 | 93 | if (response.data.error) { 94 | throw new Error(`RPC Error: ${response.data.error.message}`); 95 | } 96 | 97 | return response.data.result; 98 | } catch (error) { 99 | console.error(`Error making RPC call to ${method}:`, error.message); 100 | throw error; 101 | } 102 | } 103 | 104 | // Tool 1: eth_getCode - Gets the code at a specific address 105 | server.tool( 106 | 'eth_getCode', 107 | 'Retrieves the code at a given Ethereum address', 108 | { 109 | address: z.string().regex(/^0x[a-fA-F0-9]{40}$/).describe('The Ethereum address to get code from'), 110 | blockParameter: z.string().default('latest').describe('Block parameter (default: "latest")') 111 | }, 112 | async (args) => { 113 | try { 114 | console.error(`Getting code for address: ${args.address} at block: ${args.blockParameter}`); 115 | 116 | const code = await makeRpcCall('eth_getCode', [args.address, args.blockParameter]); 117 | 118 | return { 119 | content: [{ 120 | type: "text", 121 | text: code === '0x' ? 122 | `No code found at address ${args.address} (this may be a regular wallet address, not a contract)` : 123 | `Contract code at ${args.address}:\n${code}` 124 | }] 125 | }; 126 | } catch (error) { 127 | return { 128 | content: [{ type: "text", text: `Error: Failed to get code. ${error.message}` }], 129 | isError: true 130 | }; 131 | } 132 | } 133 | ); 134 | 135 | // Tool 2: eth_gasPrice - Gets the current gas price 136 | server.tool( 137 | 'eth_gasPrice', 138 | 'Retrieves the current gas price in wei', 139 | {}, 140 | async () => { 141 | try { 142 | console.error('Getting current gas price'); 143 | 144 | const gasPrice = await makeRpcCall('eth_gasPrice'); 145 | // Convert hex gas price to decimal and then to Gwei for readability 146 | const gasPriceWei = parseInt(gasPrice, 16); 147 | const gasPriceGwei = gasPriceWei / 1e9; 148 | 149 | return { 150 | content: [{ 151 | type: "text", 152 | text: `Current Gas Price:\n${gasPriceWei} Wei\n${gasPriceGwei.toFixed(2)} Gwei` 153 | }] 154 | }; 155 | } catch (error) { 156 | return { 157 | content: [{ type: "text", text: `Error: Failed to get gas price. ${error.message}` }], 158 | isError: true 159 | }; 160 | } 161 | } 162 | ); 163 | 164 | // Tool 3: eth_getBalance - Gets the balance of an account 165 | server.tool( 166 | 'eth_getBalance', 167 | 'Retrieves the balance of a given Ethereum address', 168 | { 169 | address: z.string().regex(/^0x[a-fA-F0-9]{40}$/).describe('The Ethereum address to check balance'), 170 | blockParameter: z.string().default('latest').describe('Block parameter (default: "latest")') 171 | }, 172 | async (args) => { 173 | try { 174 | console.error(`Getting balance for address: ${args.address} at block: ${args.blockParameter}`); 175 | 176 | const balance = await makeRpcCall('eth_getBalance', [args.address, args.blockParameter]); 177 | // Convert hex balance to decimal and then to ETH for readability 178 | const balanceWei = parseInt(balance, 16); 179 | const balanceEth = balanceWei / 1e18; 180 | 181 | return { 182 | content: [{ 183 | type: "text", 184 | text: `Balance for ${args.address}:\n${balanceWei} Wei\n${balanceEth.toFixed(6)} ETH` 185 | }] 186 | }; 187 | } catch (error) { 188 | return { 189 | content: [{ type: "text", text: `Error: Failed to get balance. ${error.message}` }], 190 | isError: true 191 | }; 192 | } 193 | } 194 | ); 195 | 196 | // Connect to the stdio transport and start the server 197 | server.connect(new StdioServerTransport()) 198 | .then(() => { 199 | console.error('Ethereum RPC MCP Server is running...'); 200 | }) 201 | .catch((err) => { 202 | console.error('Failed to start MCP server:', err); 203 | process.exit(1); 204 | }); 205 | ```