14 min read
Overviewβ
The Model Context Protocol (MCP) enables Large Language Models (LLMs) to interact with external tools β like HTTP APIs, files, or even blockchains β using a standardized message-based protocol. Think of it as a "function-calling" interface that agents like Claude or Cursor can plug into, turning a script or service into an AI-native extension.
In this guide, you'll learn how to build and deploy an MCP server that enables LLM agents to access blockchain data across multiple EVM-compatible networks. This powerful integration allows AI models like Claude to interact directly with blockchain data, opening up new possibilities for Web3 automation and analysis.
What You Will Doβ
- Build an MCP server that interacts with multiple EVM chains
- Enable LLMs to obtain onchain data using Viem and QuickNode's multichain RPC
- Register tools, prompts, and resources to guide LLM behavior
- Run the server inside Claude Desktop and test with natural language requests
What You Will Needβ
- Node.js v20 or higher
- A free QuickNode account
- Claude Desktop (or compatible agent runner)
- Experience with Ethereum blockchain
What is MCP?β
The Model Context Protocol (MCP) is an open standard that allows AI agents to interact with external tools and data sources. It creates a structured communication channel between language models and tools.
Without MCP, AI models are limited to:
- Information they were trained on
- The conversation they're currently having
- No ability to check external data sources
- No way to perform actions in the real world
MCP bridges this gap by creating standardized ways for AI to access external tools and data.
Source (modified for the server we're building): Model Context Protocol
Key Conceptsβ
How MCP Servers Workβ
MCP servers are responsible for handling incoming requests from the client and returning the appropriate data. They use the MCP protocol to communicate with the LLM, and can be implemented in any programming language or framework.
Each server communicates with the LLM over standard channels (stdio, HTTP, or sockets), and returns well-structured output. This allows the LLM to understand the context and intent of the request, and to respond appropriately. The MCP protocol is designed to be extensible, allowing developers to add new tools, resources, and prompts as needed.
Tools: Functions the AI can call to perform specific actions. They can be anything from a simple API call to a complex task.
- Example:
eth_getBalance
to check a wallet's balance - Example:
weather_forecast
to get current weather data
Resources: Static knowledge the AI can reference. These influence the modelβs grounding and assumptions β for example, adding a gas-reference
resource helps Claude know whether a gas price is low, average, or high for a specific chain, without calling any tool.
- Example: Details about gas prices on a specific chain
- Example: Documentation about API parameters
Prompts: Pre-written instructions that guide the AI. They act like reusable templates for thought. Each prompt defines how the LLM should approach a task (e.g., βanalyze this walletβ) using tools and structured reasoning.
- Example: Templates for analyzing wallet activity
- Example: Step-by-step guides for performing complex tasks
When you connect an MCP server to an AI like Claude:
- The AI discovers what tools, resources, and prompts are available
- When needed, the AI can call these tools with specific parameters
- The server processes these requests and returns structured data
- The AI interprets the results and incorporates them into its response
Here's how the same user question plays out with and without an MCP server:
Simple Example: Checking a Wallet Balanceβ
Without MCP | With MCP | |
---|---|---|
User | "What's the balance of wallet 0x123...?" | "What's the balance of wallet 0x123...?" |
AI Process | No access to external data | [Calls eth_getBalance tool via MCP] |
AI Response | "I don't have access to current blockchain data, so I can't check that wallet's balance." | "That wallet currently holds 0.45 ETH on Ethereum." |
Result | β User request cannot be fulfilled | β Real-time blockchain data provided |
Request Lifecycle in the MCP Serverβ
Hereβs what happens when the agent makes a request through a prompt:
- Parses the request to determine the action and parameters
- Validates the parameters
- Selects the appropriate chain client
- Executes the request through Viem to QuickNode
- Formats the response and returns it to the agent
We learned how MCP servers work. Now, let's build one!
Project Structureβ
The project structure is as follows:
evm-mcp-server/
βββ index.ts # Entry point, sets up MCP server
βββ chains.ts # Chain config + QuickNode endpoint mapping
βββ clients.ts # Viem public client creator
βββ package.json # Dependencies and scripts
βββ prompts.ts # LLM prompt definitions
βββ resources.ts # External references (gas prices, explorers)
βββ tools.ts # MCP tools: getCode, getBalance, gasPrice
βββ tsconfig.json # TypeScript configuration
First, let's look at each file and their purpose. However, if you want to jump straight to the code, feel free to skip ahead to the Build Your EVM MCP Server section.
All code for this guide can be found in the QuickNode GitHub repository. We're here explaining how things work by giving you a high-level overview of the code, but we'll use the GitHub repository in the Build Your MCP Server section.
Entry Point: index.ts
β
This file is the entry point that initializes and starts the MCP server. It registers tools, prompts, and resources:
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
import { registerTools } from './tools'
import { registerPrompts } from './prompts'
import { registerResources } from './resources'
async function main() {
try {
// Create the MCP server
const server = new McpServer({
name: 'EVM MCP Server',
version: '0.1.0',
description: 'A server for LLM agents to access EVM blockchain data',
})
// Register all tools, prompts, and resources
registerTools(server)
registerPrompts(server)
registerResources(server)
// Start the MCP server
const transport = new StdioServerTransport()
await server.connect(transport)
} catch (error) {
console.error('β Failed to start server:', error)
process.exit(1)
}
}
// Run the main function
main().catch(error => {
console.error('β Unhandled error:', error)
process.exit(1)
})
Chain Config: chains.ts
β
This file defines blockchain configuration and builds RPC URLs based on QuickNode's multichain format.
// Get the endpoint name and token ID from environment variables
const QN_ENDPOINT_NAME = validateEnvVar('QN_ENDPOINT_NAME')
const QN_TOKEN_ID = validateEnvVar('QN_TOKEN_ID')
// Function to build QuickNode RPC URL based on network name
const buildRpcUrl = (networkName: string): string => {
// Special case for Ethereum mainnet
if (networkName === 'mainnet') {
return `https://${QN_ENDPOINT_NAME}.quiknode.pro/${QN_TOKEN_ID}/`
}
// Special case for Avalanche mainnet
if (networkName === 'avalanche-mainnet') {
return `https://${QN_ENDPOINT_NAME}.${networkName}.quiknode.pro/${QN_TOKEN_ID}/ext/bc/C/rpc`
}
// For other networks, include network name in the URL
return `https://${QN_ENDPOINT_NAME}.${networkName}.quiknode.pro/${QN_TOKEN_ID}/`
}
export const CHAINS = {
ethereum: {
network: 'mainnet',
rpc: buildRpcUrl('mainnet'),
name: 'Ethereum',
symbol: 'ETH',
decimals: 18,
},
// Other chains...
}
// Rest of the code...
Client Creator: clients.ts
β
This file creates clients for each chain using the createPublicClient
function from the Viem library to establish a connection to the chain using QuickNode RPC endpoints.
import { createPublicClient, http } from 'viem'
import { ChainId, getChain } from './chains'
// Cache for viem clients to avoid creating duplicate clients
const clientCache = new Map<ChainId, ReturnType<typeof createPublicClient>>()
export const getPublicClient = (chainId: ChainId) => {
// Return from cache if exists
if (clientCache.has(chainId)) {
return clientCache.get(chainId)!
}
// Get chain configuration
const chain = getChain(chainId)
// Create new public client
const client = createPublicClient({
transport: http(chain.rpc),
})
// Cache for future use
clientCache.set(chainId, client)
return client
}
Prompts: prompts.ts
β
This file defines prompts for LLM agents to use. The prompt object contains the description
and messages
properties, which are used by the MCP server to generate the prompt. Prompts include instructions for calling tools, interpreting results, and formatting responses.
// Register check-wallet prompt
server.prompt(
'check-wallet',
checkWalletSchema.shape,
({ address, chain }: { address: string; chain: string }) => ({
description: "Guide for analyzing a wallet's balance and context",
messages: [
{
role: 'user',
content: {
type: 'text',
text: `Please analyze this Ethereum wallet address: ${address} on ${chain} chain.
You need to analyze a wallet address on an EVM blockchain.
First, use the eth_getBalance tool to check the wallet's balance.
Next, use the eth_getCode tool to verify if it's a regular wallet or a contract.
Once you have this information, provide a summary of:
1. The wallet's address
2. The chain it's on
3. Its balance in the native token
4. Whether it's a regular wallet (EOA) or a contract
5. Any relevant observations about the balance (e.g., if it's empty, has significant funds, etc.)
Aim to be concise but informative in your analysis.`,
},
},
],
})
)
// Schema for check-wallet prompt
const checkWalletSchema = z.object({
address: z.string().refine(isAddress, {
message: 'Invalid Ethereum address format',
}),
chain: z
.string()
.refine((val): val is ChainId => Object.keys(CHAINS).includes(val), {
message:
'Unsupported chain. Use one of: ethereum, base, arbitrum, avalanche, bsc',
}),
})
// Rest of the code...
Resources: resources.ts
β
This file defines external references that can be used by LLM agents. In this case, we're giving them access to gas price levels for each chain, block explorer links for each chain, and some details about the chains themselves.
export const registerResources = (server: any) => {
// Register gas reference resource
server.resource(
'gas-reference',
'evm://docs/gas-reference',
async (uri: URL) => {
return {
contents: [
{
uri: uri.href,
text: JSON.stringify(gasReferencePoints, null, 2),
},
],
}
}
)
// Other resources...
}
// Gas reference points for each chain
const gasReferencePoints = {
ethereum: {
low: 20,
average: 40,
high: 100,
veryHigh: 200,
},
base: {
low: 0.05,
average: 0.1,
high: 0.3,
veryHigh: 0.5,
},
// Other chains...
}
// Rest of the code...
Tools: tools.ts
β
This file defines the tools that the server can call. In this case, we're using the eth_getBalance
, eth_getCode
, and eth_gasPrice
tools. Each tool is an async function that queries the blockchain via Viem and returns structured data for LLM consumption.
// Register tools with the MCP server
export const registerTools = (server: any) => {
// Register eth_getBalance tool
server.tool(
'eth_getBalance',
balanceSchema.shape,
async (args: z.infer<typeof balanceSchema>) => {
try {
const result = await getBalance(args)
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2),
},
],
}
} catch (error) {
// Handle errors
}
}
)
// Other tools...
}
// Schema for eth_getBalance tool
const balanceSchema = z.object({
address: z.string().refine(isAddress, {
message: 'Invalid Ethereum address format',
}),
chain: z
.string()
.refine((val): val is ChainId => Object.keys(CHAINS).includes(val), {
message:
'Unsupported chain. Use one of: ethereum, base, arbitrum, avalanche, bsc',
}),
})
// Other tool schemas...
/**
* Get the balance of an Ethereum address on the specified chain
*/
export const getBalance = async (params: z.infer<typeof balanceSchema>) => {
const { address, chain } = balanceSchema.parse(params)
try {
const client = getPublicClient(chain as ChainId)
const chainInfo = getChain(chain as ChainId)
// Get balance in wei
const balanceWei = await client.getBalance({ address })
// Format balance to ETH/native token
const balanceFormatted = formatEther(balanceWei)
return {
address,
chain: chainInfo.name,
balanceWei: balanceWei.toString(),
balanceFormatted: `${balanceFormatted} ${chainInfo.symbol}`,
symbol: chainInfo.symbol,
decimals: chainInfo.decimals,
}
} catch (error) {
return {
error: `Failed to get balance: ${(error as Error).message}`,
}
}
}
// Rest of the code...
Build Your EVM MCP Serverβ
We learned how each file serves a purpose in the MCP server. Now, let's build and run our server.
Get a Multichain Endpointβ
The MCP server we're building will support multiple EVM chains (Ethereum, Base, Arbitrum, Avalanche, and BSC). By leveraging the QuickNode's multichain format, we can easily connect to these chains using a single endpoint. If you don't have a QuickNode account, you can get a free one here.
- Log in to your QuickNode account.
- Go to the "Endpoints" tab.
- Click "Create Endpoint".
- Select Ethereum Mainnet or one of other EVM chains.
- After the endpoint is created, activate the multichain format.
- Note your endpoint URL, which will look like
https://{endpoint_name}.quiknode.pro/{token_id}/
orhttps://{endpoint_name}.{chain_name}.quiknode.pro/{token_id}
. - Extract the endpoint name and token ID from the URL.
Thanks to the multichain format, we can now connect to any EVM chain using a single endpoint. The endpoint name and token ID are used to identify the chain and its configuration.
Setup Instructionsβ
Now that we have our endpoint, let's set up our MCP server.
Step 1: Clone the Repositoryβ
git clone https://github.com/quiknode-labs/qn-guide-examples.git
cd qn-guide-examples/AI/evm-mcp-server
Step 2: Install Dependenciesβ
npm install
This will install the necessary dependencies for the project:
- @modelcontextprotocol/sdk: A TypeScript SDK for building MCP servers.
- viem: A TypeScript library for interacting with EVM blockchains.
- zod: A TypeScript library for defining schemas and validating data.
- typescript: A TypeScript compiler.
- @types/node: TypeScript type definitions for Node.js.
Step 3: Build the Projectβ
npm run build
This generates the build/
directory containing the compiled index.js
file, which serves as the serverβs entry point.
Step 4: Configure Claude Desktopβ
The server uses environment variables defined in Claude Desktopβs configuration file (claude_desktop_config.json
) to connect to QuickNode and run the server.
- Open Claude Desktop, navigate to Claude > Settings > Developer
- Edit
claude_desktop_config.json
to include the following (add undermcpServers
if other configurations exist):
{
"mcpServers": {
"evm": {
"command": "node",
"args": ["/absolute-path-to/build/index.js"],
"env": {
"QN_ENDPOINT_NAME": "your-quicknode-endpoint-name",
"QN_TOKEN_ID": "your-quicknode-token-id"
}
}
}
}
- Replace your-quicknode-endpoint-name with your QuickNode endpoint name.
- Replace your-quicknode-token-id with your QuickNode token ID.
- Replace /absolute-path-to with the absolute path to the project directory (e.g., /Users/username/qn-guide-examples/AI/evm-mcp-server).
Save the file and restart Claude Desktop. Your MCP server's tools, resources, and prompts should now be available in Claude.
Test Your MCP Serverβ
Let's start asking questions to see how the server works.
Wallet Analysisβ
Use the following prompt or use our pre-built prompts to guide Claude to analyze a wallet's balance and context:
Give the balance of 0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045 on Ethereum
Or, ask Claude to analyze a wallet's balance across all supported chains:
Give the balance of 0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045 address across all networks you support.
Claude will:
- Call the
eth_getBalance
tool - Return a response according to the prompt
Note that Claude will call the eth_getBalance
tool only for the chains it supports, thanks to the supported-chains
resource.
Contract Detectionβ
Use the following prompt or use our pre-built prompts to guide Claude to detect if a wallet is a contract:
What kind of contract is 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2?
Claude will:
- Detect itβs a contract (using bytecode presence)
- Explain if it is a known contract (using a resource or historical data)
Gas Evaluationβ
Use the following prompt or use our pre-built prompts to guide Claude to evaluate the gas price on the Ethereum network:
Analyze current gas price on Ethereum. Is now a good time to transact?
Claude will:
- Call the
eth_gasPrice
tool to fetch the current gas price from QuickNode - Reference the
gas-reference
resource you provided, which includes thresholds for low, average, high, and very high gas prices per chain - Compare the live gas data against these reference points
- Deliver a context-aware response indicating whether it's a good time to transact
Congratulations! You've built your own LLM-powered blockchain data analysis tool. Now, you can use Claude to analyze and interact with blockchain data, enabling you to build intelligent applications and automate processes.
What's Next?β
Once your EVM MCP Server is up and running, there are many ways to expand its capabilities and deepen its integration into AI workflows. Below are several areas to explore:
Extend EVM Functionalityβ
Add more native EVM methods to increase what the server can do:
eth_getLogs
: Monitor contract events, such as token transfers or DAO voteseth_call
: Read smart contract data (e.g., token balances, configurations)eth_blockNumber
: Fetch the latest block for chain health/status trackingeth_getTransactionByHash
: Analyze specific transactions
Build AI-Specific Enhancementsβ
LLM agents become more useful when guided with domain-specific reasoning and context. Consider:
- Specialized prompts: Design task-specific instructions for DeFi analysis, contract auditing, wallet profiling, etc.
- ENS support: Resolve
.eth
domain names for better user experience - ERC20 token and NFT data exploration: Use Token and NFT API v2 bundle to fetch token balances, NFT metadata, and more
- Real-time price data: Integrate token prices using a QuickNode Marketplace add-on, such as DEX Aggregator Trading API
- Trading functionality: If you want to go beyond read-only queries, check out our Build a Telegram Trading Bot on Base guide. Imagine combining MCP prompts with trading actions to create autonomous trading agents.
- Custom agents: Combine MCP with LangChain, AutoGen, or CrewAI to create fully autonomous agents with memory, planning, and blockchain access
- Explore multi-chain expansion: Interested in supporting Solana alongside EVM chains? Follow our How to Build a Solana MCP Server for LLM Integration guide to extend your agentβs reach across ecosystems.
Add Caching and Performance Optimizationβ
For frequently queried data, caching can reduce latency and avoid repeated RPC calls:
- Use Redis, SQLite, or simple file-based caches for
eth_getBalance
andeth_gasPrice
- Set up TTL (Time-To-Live) values for each chain/method to ensure freshness
- Use memoization patterns or custom wrappers to avoid duplicate Viem calls per run
Track Analytics and Usageβ
Understand how your tools are being used:
- Log incoming tool calls and prompt usage
- Monitor which chains are queried most
- Add basic usage metrics (e.g., volume per chain, latency per tool)
This data can help guide future improvements, like optimizing prompt templates or scaling out infrastructure.
Strengthen Security and Stabilityβ
Because MCP servers accept input from agents, itβs important to secure the surface area:
- Use Zod schemas to strictly validate and sanitize all input
- Read the MCP Security Considerations to prevent prompt injection or misuse
- Consider rate-limiting or adding auth layers if exposing the MCP server publicly
Conclusionβ
The EVM MCP Server provides a powerful bridge between LLM agents and blockchain data. By implementing the Model Context Protocol, we've created a standardized interface that allows AI models to access and analyze on-chain information across multiple EVM-compatible networks.
This technical foundation opens up numerous possibilities for AI-powered blockchain applications, from automated monitoring to intelligent analysis and recommendation systems. Whether youβre building agents for analysts, dApps, or internal automation, this is your foundation β build it once, then extend it everywhere.
If you have any questions or need help, feel free to reach out to us on our Discord or Twitter.
Additional Resourcesβ
- Model Context Protocol Documentation
- Viem Documentation
- QuickNode Multichain Guide
- EVM JSON-RPC Specification
- Claude Desktop Documentation
- MCP Inspector - A helpful tool for debugging your MCP server
- MCP Security Considerations - Important security considerations when building your MCP server
We β€οΈ Feedback!
Let us know if you have any feedback or requests for new topics. We'd love to hear from you.