Ethereum RPC MCP Server
by 0xKoda
const axios = require('axios');
const { McpServer } = require('@modelcontextprotocol/sdk/server/mcp.js');
const { StdioServerTransport } = require('@modelcontextprotocol/sdk/server/stdio.js');
const { z } = require('zod');
// Redirect console.log to stderr to avoid breaking the MCP protocol
const originalConsoleLog = console.log;
console.log = function() {
console.error.apply(console, arguments);
};
// Ethereum RPC URL
const ETH_RPC_URL = 'https://eth.llamarpc.com';
// Initialize the MCP server
const server = new McpServer({
name: 'ethereum-rpc',
version: '1.0.0'
});
// Helper function to make RPC calls
async function makeRpcCall(method, params = []) {
try {
const response = await axios.post(ETH_RPC_URL, {
jsonrpc: '2.0',
id: 1,
method,
params
});
if (response.data.error) {
throw new Error(`RPC Error: ${response.data.error.message}`);
}
return response.data.result;
} catch (error) {
console.error(`Error making RPC call to ${method}:`, error.message);
throw error;
}
}
// Tool 1: eth_getCode - Gets the code at a specific address
server.tool(
'eth_getCode',
'Retrieves the code at a given Ethereum address',
{
address: z.string().regex(/^0x[a-fA-F0-9]{40}$/).describe('The Ethereum address to get code from'),
blockParameter: z.string().default('latest').describe('Block parameter (default: "latest")')
},
async (args) => {
try {
console.error(`Getting code for address: ${args.address} at block: ${args.blockParameter}`);
const code = await makeRpcCall('eth_getCode', [args.address, args.blockParameter]);
return {
content: [{
type: "text",
text: code === '0x' ?
`No code found at address ${args.address} (this may be a regular wallet address, not a contract)` :
`Contract code at ${args.address}:\n${code}`
}]
};
} catch (error) {
return {
content: [{ type: "text", text: `Error: Failed to get code. ${error.message}` }],
isError: true
};
}
}
);
// Tool 2: eth_gasPrice - Gets the current gas price
server.tool(
'eth_gasPrice',
'Retrieves the current gas price in wei',
{},
async () => {
try {
console.error('Getting current gas price');
const gasPrice = await makeRpcCall('eth_gasPrice');
// Convert hex gas price to decimal and then to Gwei for readability
const gasPriceWei = parseInt(gasPrice, 16);
const gasPriceGwei = gasPriceWei / 1e9;
return {
content: [{
type: "text",
text: `Current Gas Price:\n${gasPriceWei} Wei\n${gasPriceGwei.toFixed(2)} Gwei`
}]
};
} catch (error) {
return {
content: [{ type: "text", text: `Error: Failed to get gas price. ${error.message}` }],
isError: true
};
}
}
);
// Tool 3: eth_getBalance - Gets the balance of an account
server.tool(
'eth_getBalance',
'Retrieves the balance of a given Ethereum address',
{
address: z.string().regex(/^0x[a-fA-F0-9]{40}$/).describe('The Ethereum address to check balance'),
blockParameter: z.string().default('latest').describe('Block parameter (default: "latest")')
},
async (args) => {
try {
console.error(`Getting balance for address: ${args.address} at block: ${args.blockParameter}`);
const balance = await makeRpcCall('eth_getBalance', [args.address, args.blockParameter]);
// Convert hex balance to decimal and then to ETH for readability
const balanceWei = parseInt(balance, 16);
const balanceEth = balanceWei / 1e18;
return {
content: [{
type: "text",
text: `Balance for ${args.address}:\n${balanceWei} Wei\n${balanceEth.toFixed(6)} ETH`
}]
};
} catch (error) {
return {
content: [{ type: "text", text: `Error: Failed to get balance. ${error.message}` }],
isError: true
};
}
}
);
// Tool 4: eth_call - Executes a new message call without creating a transaction
server.tool(
'eth_call',
'Executes a call to a contract function without creating a transaction',
{
transaction: z.object({
from: z.string().regex(/^0x[a-fA-F0-9]{40}$/).optional().describe('The address the transaction is sent from'),
to: z.string().regex(/^0x[a-fA-F0-9]{40}$/).describe('The address the transaction is directed to'),
gas: z.string().regex(/^0x[a-fA-F0-9]+$/).optional().describe('Integer of the gas provided for the transaction execution in hex'),
gasPrice: z.string().regex(/^0x[a-fA-F0-9]+$/).optional().describe('Integer of the gas price used for each paid gas in hex'),
value: z.string().regex(/^0x[a-fA-F0-9]+$/).optional().describe('Integer of the value sent with this transaction in hex'),
data: z.string().regex(/^0x[a-fA-F0-9]*$/).describe('The compiled code of a contract OR the hash of the invoked method signature and encoded parameters')
}).describe('The transaction call object'),
blockParameter: z.string().default('latest').describe('Block parameter (default: "latest")')
},
async (args) => {
try {
console.error(`Executing eth_call with transaction to: ${args.transaction.to} at block: ${args.blockParameter}`);
const result = await makeRpcCall('eth_call', [args.transaction, args.blockParameter]);
return {
content: [{
type: "text",
text: `Call result:\n${result}`
}]
};
} catch (error) {
return {
content: [{ type: "text", text: `Error: Failed to execute call. ${error.message}` }],
isError: true
};
}
}
);
// Tool 5: eth_getLogs - Retrieves logs matching the given filter criteria
server.tool(
'eth_getLogs',
'Retrieves logs matching the given filter criteria',
{
filter: z.object({
fromBlock: z.string().optional().describe('Block number in hex or "latest", "earliest" or "pending"'),
toBlock: z.string().optional().describe('Block number in hex or "latest", "earliest" or "pending"'),
address: z.union([
z.string().regex(/^0x[a-fA-F0-9]{40}$/),
z.array(z.string().regex(/^0x[a-fA-F0-9]{40}$/))
]).optional().describe('Contract address or a list of addresses from which logs should originate'),
topics: z.array(z.union([
z.string().regex(/^0x[a-fA-F0-9]{64}$/),
z.array(z.string().regex(/^0x[a-fA-F0-9]{64}$/)),
z.null()
])).optional().describe('Array of 32 Bytes DATA topics')
}).describe('The filter options')
},
async (args) => {
try {
console.error(`Getting logs with filter: ${JSON.stringify(args.filter)}`);
const logs = await makeRpcCall('eth_getLogs', [args.filter]);
if (logs.length === 0) {
return {
content: [{ type: "text", text: "No logs found matching the filter criteria." }]
};
}
// Format logs for better readability
const formattedLogs = logs.map((log, index) => {
return `Log #${index + 1}:
Address: ${log.address}
Block Number: ${parseInt(log.blockNumber, 16)}
Transaction Hash: ${log.transactionHash}
Topics: ${log.topics.join('\n ')}
Data: ${log.data}`;
}).join('\n\n');
return {
content: [{
type: "text",
text: `Found ${logs.length} logs:\n\n${formattedLogs}`
}]
};
} catch (error) {
return {
content: [{ type: "text", text: `Error: Failed to get logs. ${error.message}` }],
isError: true
};
}
}
);
// Tool 6: eth_sendTransaction - Sends a transaction to the network
server.tool(
'eth_sendTransaction',
'Sends a transaction to the Ethereum network',
{
transaction: z.object({
from: z.string().regex(/^0x[a-fA-F0-9]{40}$/).describe('The address the transaction is sent from'),
to: z.string().regex(/^0x[a-fA-F0-9]{40}$/).optional().describe('The address the transaction is directed to'),
gas: z.string().regex(/^0x[a-fA-F0-9]+$/).optional().describe('Integer of the gas provided for the transaction execution in hex'),
gasPrice: z.string().regex(/^0x[a-fA-F0-9]+$/).optional().describe('Integer of the gas price used for each paid gas in hex'),
value: z.string().regex(/^0x[a-fA-F0-9]+$/).optional().describe('Integer of the value sent with this transaction in hex'),
data: z.string().regex(/^0x[a-fA-F0-9]*$/).optional().describe('The compiled code of a contract OR the hash of the invoked method signature and encoded parameters'),
nonce: z.string().regex(/^0x[a-fA-F0-9]+$/).optional().describe('Integer of a nonce used to prevent transaction replay')
}).describe('The transaction object')
},
async (args) => {
try {
console.error(`Sending transaction from: ${args.transaction.from}`);
// Note: This will likely fail with a public node as it requires an unlocked account
// Most public nodes don't allow sending transactions directly (would need a wallet/private key)
const txHash = await makeRpcCall('eth_sendTransaction', [args.transaction]);
return {
content: [{
type: "text",
text: `Transaction sent!\nTransaction Hash: ${txHash}\n\nNote: This request will only work on nodes where the 'from' account is unlocked. Most public nodes don't allow direct transaction sending.`
}]
};
} catch (error) {
return {
content: [{
type: "text",
text: `Error: Failed to send transaction. ${error.message}\n\nNote: Most public RPC endpoints don't allow sending raw transactions as it requires an unlocked account. Consider using a wallet or signing the transaction locally before broadcasting.`
}],
isError: true
};
}
}
);
// Connect to the stdio transport and start the server
server.connect(new StdioServerTransport())
.then(() => {
console.error('Ethereum RPC MCP Server is running...');
})
.catch((err) => {
console.error('Failed to start MCP server:', err);
process.exit(1);
});