Repository: mcpdotdirect/evm-mcp-server
Branch: main
Commit: e87a8c3ab1d3
Files: 30
Total size: 172.2 KB
Directory structure:
gitextract_kze5jsqh/
├── .cursor/
│ └── mcp.json
├── .github/
│ └── workflows/
│ └── release-publish.yml
├── .gitignore
├── .npmignore
├── CHANGELOG.md
├── LICENSE
├── README.md
├── bin/
│ └── cli.js
├── funding.json
├── package.json
├── src/
│ ├── core/
│ │ ├── chains.ts
│ │ ├── prompts.ts
│ │ ├── resources.ts
│ │ ├── services/
│ │ │ ├── abi.ts
│ │ │ ├── balance.ts
│ │ │ ├── blocks.ts
│ │ │ ├── clients.ts
│ │ │ ├── contracts.ts
│ │ │ ├── ens.ts
│ │ │ ├── index.ts
│ │ │ ├── tokens.ts
│ │ │ ├── transactions.ts
│ │ │ ├── transfer.ts
│ │ │ ├── utils.ts
│ │ │ └── wallet.ts
│ │ └── tools.ts
│ ├── index.ts
│ └── server/
│ ├── http-server.ts
│ └── server.ts
└── tsconfig.json
================================================
FILE CONTENTS
================================================
================================================
FILE: .cursor/mcp.json
================================================
{
"mcpServers": {
"evm-mcp-server": {
"command": "npx",
"args": [
"-y",
"@mcpdotdirect/evm-mcp-server"
]
},
"evm-mcp-http": {
"command": "npx",
"args": [
"-y",
"@mcpdotdirect/evm-mcp-server",
"--http"
]
}
}
}
================================================
FILE: .github/workflows/release-publish.yml
================================================
name: Release and Publish
on:
workflow_dispatch:
inputs:
version_type:
description: 'Version type (prerelease, prepatch, patch, preminor, minor, premajor, major)'
required: true
default: 'patch'
type: choice
options:
- prerelease
- prepatch
- patch
- preminor
- minor
- premajor
- major
custom_version:
description: 'Custom version (leave empty to use version_type)'
required: false
type: string
dist_tag:
description: 'npm distribution tag (latest, next, beta, etc)'
required: false
default: 'latest'
type: string
jobs:
release-and-publish:
runs-on: ubuntu-latest
permissions:
contents: write
packages: write
id-token: write
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ secrets.PAT_GITHUB }}
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '18.x'
registry-url: 'https://registry.npmjs.org'
- name: Setup Bun
uses: oven-sh/setup-bun@v1
with:
bun-version: latest
- name: Install dependencies
run: bun install
- name: Configure Git
run: |
git config --local user.email "action@github.com"
git config --local user.name "GitHub Action"
git config pull.rebase false
- name: Bump version
id: bump_version
run: |
if [ -n "${{ github.event.inputs.custom_version }}" ]; then
echo "Using custom version ${{ github.event.inputs.custom_version }}"
npm version ${{ github.event.inputs.custom_version }} --no-git-tag-version
echo "VERSION=${{ github.event.inputs.custom_version }}" >> $GITHUB_ENV
else
echo "Bumping ${{ github.event.inputs.version_type }} version"
NEW_VERSION=$(npm version ${{ github.event.inputs.version_type }} --no-git-tag-version)
echo "VERSION=${NEW_VERSION:1}" >> $GITHUB_ENV
fi
echo "New version: ${{ env.VERSION }}"
- name: Generate Changelog
run: |
npm run changelog
npm run changelog:latest
- name: Build project
run: bun run build && bun run build:http
- name: Commit and push changes
run: |
git pull origin main --no-edit
git add package.json CHANGELOG.md
git commit -m "Bump version to v${{ env.VERSION }}"
git push --force-with-lease
- name: Create and push tag
run: |
git tag -d "v${{ env.VERSION }}" 2>/dev/null || true
git push origin --delete "v${{ env.VERSION }}" 2>/dev/null || true
git tag -a "v${{ env.VERSION }}" -m "Release v${{ env.VERSION }}"
git push origin "v${{ env.VERSION }}"
- name: Create GitHub Release
uses: softprops/action-gh-release@v1
with:
tag_name: v${{ env.VERSION }}
name: Release v${{ env.VERSION }}
body_path: RELEASE_NOTES.md
generate_release_notes: false
env:
GITHUB_TOKEN: ${{ secrets.PAT_GITHUB }}
- name: Publish to npm
run: npm publish --access public --provenance --tag ${{ github.event.inputs.dist_tag || 'latest' }}
env:
# Restored the token here to ensure it works
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
================================================
FILE: .gitignore
================================================
# dependencies (bun install)
node_modules
# output
out
dist
*.tgz
# code coverage
coverage
*.lcov
# logs
logs
_.log
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
# caches
.eslintcache
.cache
*.tsbuildinfo
# IntelliJ based IDEs
.idea
# Context
mcp-context/
build/
# Finder (MacOS) folder config
.DS_Store
================================================
FILE: .npmignore
================================================
# Source code (since we're publishing built files)
src/
# Development files
.git/
.github/
.vscode/
.idea/
.cursor/
mcp-context/
*.tsbuildinfo
# Temp files
tmp/
temp/
# Test files
test/
tests/
__tests__/
*.spec.ts
*.test.ts
# Docs (except README.md which is included in "files")
docs/
# Build artifacts
node_modules/
coverage/
bun.lock
yarn.lock
package-lock.json
================================================
FILE: CHANGELOG.md
================================================
## [2.0.4](https://github.com/mcpdotdirect/evm-mcp-server/compare/v2.0.3...v2.0.4) (2025-11-26)
## [2.0.3](https://github.com/mcpdotdirect/evm-mcp-server/compare/v2.0.2...v2.0.3) (2025-11-26)
## [2.0.2](https://github.com/mcpdotdirect/evm-mcp-server/compare/v2.0.1...v2.0.2) (2025-11-26)
## [2.0.1](https://github.com/mcpdotdirect/evm-mcp-server/compare/v2.0.0...v2.0.1) (2025-11-26)
### Bug Fixes
* critical bug fixes and add message signing capabilities ([8591ac2](https://github.com/mcpdotdirect/evm-mcp-server/commit/8591ac2b87ddaafb5f7d8862a177c452dd740053))
### Features
* add multicall support for batch contract reads ([dd3c354](https://github.com/mcpdotdirect/evm-mcp-server/commit/dd3c354fff315866982fcf85b8c67e6f120f1ef2))
# [2.0.0](https://github.com/mcpdotdirect/evm-mcp-server/compare/v1.2.0...v2.0.0) (2025-11-20)
### Bug Fixes
* add Express back for reliability and improve wallet.ts type safety ([07687b1](https://github.com/mcpdotdirect/evm-mcp-server/commit/07687b1f2f74ad05b4919e0d1342957665943188))
* add production hardening for security and reliability ([b02dad9](https://github.com/mcpdotdirect/evm-mcp-server/commit/b02dad95a15628debc8ec129aee297d951f36fc0))
### Features
* add `write_contract` tool and `interact_with_contract` prompt for safe contract interaction. ([8cee62f](https://github.com/mcpdotdirect/evm-mcp-server/commit/8cee62f8ba8b2461153623473847c480624ebf93))
* Add flexible wallet configuration supporting private key or mnemonic phrase, centralize wallet logic, and update documentation. ([9e2a0ac](https://github.com/mcpdotdirect/evm-mcp-server/commit/9e2a0ac103ad20e42ac14c405ab8c7acad8e78d7))
* enhance prompt content and structure for token transfers, transaction diagnosis, and wallet analysis ([554ac8c](https://github.com/mcpdotdirect/evm-mcp-server/commit/554ac8ce2cd90fe4bead6aa9b7c51a9e9bd0bebc))
* Refactor ABI fetching to use a unified Etherscan v2 API and enhance transaction prompts with detailed, safety-focused instructions. ([3b7bac3](https://github.com/mcpdotdirect/evm-mcp-server/commit/3b7bac33ca3852d93a6829a3b5f2524789efe7ed))
* replace generic EVM prompts with new task-oriented workflows for transfers, diagnostics, wallet analysis, and approvals, and add an ABI service. ([1e1d879](https://github.com/mcpdotdirect/evm-mcp-server/commit/1e1d8799394e4903548dae7ead47896851235fe8))
* Update MCP SDK, configure server capabilities, and bump server version to 2.0.0. ([25ab4d9](https://github.com/mcpdotdirect/evm-mcp-server/commit/25ab4d9d516298c0847968759fbe814283735945))
* updated docs ([796245b](https://github.com/mcpdotdirect/evm-mcp-server/commit/796245bb7fea9a38fdd2b83cdb0702fc6b506c32))
# [1.2.0](https://github.com/mcpdotdirect/evm-mcp-server/compare/v1.1.3...v1.2.0) (2025-05-23)
### Features
* add Filecoin Calibration network to chains and update mappings ([9790118](https://github.com/mcpdotdirect/evm-mcp-server/commit/97901181139a8574f688179864331777c7fda422))
## [1.1.3](https://github.com/mcpdotdirect/evm-mcp-server/compare/v1.1.2...v1.1.3) (2025-03-22)
## [1.1.2](https://github.com/mcpdotdirect/evm-mcp-server/compare/v1.1.1...v1.1.2) (2025-03-22)
## [1.1.1](https://github.com/mcpdotdirect/evm-mcp-server/compare/v1.1.0...v1.1.1) (2025-03-22)
### Bug Fixes
* fixed naming of github secret ([4300dad](https://github.com/mcpdotdirect/evm-mcp-server/commit/4300dad343dc696c9e345d9b18e37bbb481db961))
# [1.1.0](https://github.com/mcpdotdirect/evm-mcp-server/compare/db4d20f0aeb0b34f67b4be3b38c6bb662682bfb6...v1.1.0) (2025-03-22)
### Bug Fixes
* fixed tools names to be callable by Cursor ([3938549](https://github.com/mcpdotdirect/evm-mcp-server/commit/3938549381d2b1abb406d25ccda365a53ef3555d))
* following standard naming, fixed SSE server ([b20a12d](https://github.com/mcpdotdirect/evm-mcp-server/commit/b20a12d81c25a262389bd8781d73095ec69d265b))
### Features
* add Lumia mainnet and testnet support in chains.ts and update README ([ee55fa7](https://github.com/mcpdotdirect/evm-mcp-server/commit/ee55fa750d4759d5d4e7254ce811f62a4fd5c6e9))
* adding ENS support ([4f19f12](https://github.com/mcpdotdirect/evm-mcp-server/commit/4f19f12c0df163fbade10f2334f2690d735831ea))
* adding get_address_from_private_key tool ([befc357](https://github.com/mcpdotdirect/evm-mcp-server/commit/befc35769dd21cfa031c084115ea59eeeecbf5b4))
* implemented v0 of EVM MCP server, needs testing ([db4d20f](https://github.com/mcpdotdirect/evm-mcp-server/commit/db4d20f0aeb0b34f67b4be3b38c6bb662682bfb6))
* npm public release ([df6d52d](https://github.com/mcpdotdirect/evm-mcp-server/commit/df6d52db01e0b290f0da7ea1a087243484ce4e5c))
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2025 mcpdotdirect
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: README.md
================================================
# EVM MCP Server





A comprehensive Model Context Protocol (MCP) server that provides blockchain services across 60+ EVM-compatible networks. This server enables AI agents to interact with Ethereum, Optimism, Arbitrum, Base, Polygon, and many other EVM chains with a unified interface through 22 tools and 10 AI-guided prompts.
## 📋 Contents
- [Overview](#overview)
- [Features](#features)
- [Supported Networks](#supported-networks)
- [Prerequisites](#prerequisites)
- [Installation](#installation)
- [Configuration](#configuration)
- [Environment Variables](#environment-variables)
- [Server Configuration](#server-configuration)
- [Usage](#usage)
- [API Reference](#api-reference)
- [Tools](#tools)
- [Prompts](#prompts)
- [Resources](#resources)
- [Security Considerations](#security-considerations)
- [Project Structure](#project-structure)
- [Development](#development)
- [License](#license)
## 🔭 Overview
The MCP EVM Server leverages the Model Context Protocol to provide blockchain services to AI agents. It supports a wide range of services including:
- Reading blockchain state (balances, transactions, blocks, etc.)
- Interacting with smart contracts with **automatic ABI fetching** from block explorers
- Transferring tokens (native, ERC20, ERC721, ERC1155)
- Querying token metadata and balances
- Chain-specific services across 60+ EVM networks (34 mainnets + 26 testnets)
- **ENS name resolution** for all address parameters (use human-readable names like 'vitalik.eth' instead of addresses)
- **AI-friendly prompts** that guide agents through complex workflows
All services are exposed through a consistent interface of MCP tools, resources, and prompts, making it easy for AI agents to discover and use blockchain functionality. **Every tool that accepts Ethereum addresses also supports ENS names**, automatically resolving them to addresses behind the scenes. The server includes intelligent ABI fetching, eliminating the need to know contract ABIs in advance.
## ✨ Features
### Blockchain Data Access
- **Multi-chain support** for 60+ EVM-compatible networks (34 mainnets + 26 testnets)
- **Chain information** including blockNumber, chainId, and RPCs
- **Block data** access by number, hash, or latest
- **Transaction details** and receipts with decoded logs
- **Address balances** for native tokens and all token standards
- **ENS resolution** for human-readable Ethereum addresses (use 'vitalik.eth' instead of '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045')
### Token services
- **ERC20 Tokens**
- Get token metadata (name, symbol, decimals, supply)
- Check token balances
- Transfer tokens between addresses
- Approve spending allowances
- **NFTs (ERC721)**
- Get collection and token metadata
- Verify token ownership
- Transfer NFTs between addresses
- Retrieve token URIs and count holdings
- **Multi-tokens (ERC1155)**
- Get token balances and metadata
- Transfer tokens with quantity
- Access token URIs
### Smart Contract Interactions
- **Read contract state** through view/pure functions
- **Write to contracts** - Execute any state-changing function with automatic ABI fetching
- **Contract verification** to distinguish from EOAs
- **Event logs** retrieval and filtering
- **Automatic ABI fetching** from Etherscan v2 API across all 60+ networks (no need to know ABIs in advance)
- **ABI parsing and validation** with function discovery
### Comprehensive Transaction Support
- **Flexible Wallet Support** - Configure with Private Key or Mnemonic (BIP-39) with HD path support
- **Native token transfers** across all supported networks
- **Gas estimation** for transaction planning
- **Transaction status** and receipt information
- **Error handling** with descriptive messages
### Message Signing Capabilities
- **Personal Message Signing** - Sign arbitrary messages for authentication and verification
- **EIP-712 Typed Data Signing** - Sign structured data for gasless transactions and meta-transactions
- **SIWE Support** - Enable Sign-In With Ethereum authentication flows
- **Permit Signatures** - Create off-chain approvals for gasless token operations
- **Meta-Transaction Support** - Sign transaction data for relay services and gasless transfers
### AI-Guided Workflows (Prompts)
- **Transaction preparation** - Guidance for planning and executing transfers
- **Wallet analysis** - Tools for analyzing wallet activity and holdings
- **Smart contract exploration** - Interactive ABI fetching and contract analysis
- **Contract interaction** - Safe execution of write operations on smart contracts
- **Network information** - Learning about EVM networks and comparisons
- **Approval auditing** - Reviewing and managing token approvals
- **Error diagnosis** - Troubleshooting transaction failures
## 🌐 Supported Networks
### Mainnets
- Ethereum (ETH)
- Optimism (OP)
- Arbitrum (ARB)
- Arbitrum Nova
- Base
- Polygon (MATIC)
- Polygon zkEVM
- Avalanche (AVAX)
- Binance Smart Chain (BSC)
- zkSync Era
- Linea
- Celo
- Gnosis (xDai)
- Fantom (FTM)
- Filecoin (FIL)
- Moonbeam
- Moonriver
- Cronos
- Scroll
- Mantle
- Manta
- Blast
- Fraxtal
- Mode
- Metis
- Kroma
- Zora
- Aurora
- Canto
- Flow
- Lumia
### Testnets
- Sepolia
- Optimism Sepolia
- Arbitrum Sepolia
- Base Sepolia
- Polygon Amoy
- Avalanche Fuji
- BSC Testnet
- zkSync Sepolia
- Linea Sepolia
- Scroll Sepolia
- Mantle Sepolia
- Manta Sepolia
- Blast Sepolia
- Fraxtal Testnet
- Mode Testnet
- Metis Sepolia
- Kroma Sepolia
- Zora Sepolia
- Celo Alfajores
- Goerli
- Holesky
- Flow Testnet
- Filecoin Calibration
- Lumia Testnet
## 🛠️ Prerequisites
- [Bun](https://bun.sh/) 1.0.0 or higher (recommended)
- Node.js 20.0.0 or higher (if not using Bun)
- Optional: [Etherscan API key](https://etherscan.io/apis) for ABI fetching
## 📦 Installation
```bash
# Clone the repository
git clone https://github.com/mcpdotdirect/mcp-evm-server.git
cd mcp-evm-server
# Install dependencies with Bun
bun install
# Or with npm
npm install
```
## ⚙️ Configuration
### Environment Variables
The server uses the following environment variables. For write operations and ABI fetching, you must configure these variables:
#### Wallet Configuration (For Write Operations)
You can configure your wallet using **either** a private key or a mnemonic phrase:
**Option 1: Private Key**
```bash
export EVM_PRIVATE_KEY="0x..." # Your private key in hex format (with or without 0x prefix)
```
**Option 2: Mnemonic Phrase (Recommended for HD Wallets)**
```bash
export EVM_MNEMONIC="word1 word2 word3 ... word12" # Your 12 or 24 word BIP-39 mnemonic
export EVM_ACCOUNT_INDEX="0" # Optional: Account index for HD wallet derivation (default: 0)
```
The mnemonic option supports hierarchical deterministic (HD) wallet derivation:
- Uses BIP-39 standard mnemonic phrases (12 or 24 words)
- Supports BIP-44 derivation path: `m/44'/60'/0'/0/{accountIndex}`
- `EVM_ACCOUNT_INDEX` allows you to derive different accounts from the same mnemonic
- Default account index is 0 (first account)
**Wallet is used for:**
- Transferring native tokens (`transfer_native` tool)
- Transferring ERC20 tokens (`transfer_erc20` tool)
- Approving token spending (`approve_token_spending` tool)
- Writing to smart contracts (`write_contract` tool)
- Signing messages for authentication (`sign_message` tool)
- Signing structured data for gasless transactions (`sign_typed_data` tool)
⚠️ **Security**:
- Never commit your private key or mnemonic to version control
- Use environment variables or a secure key management system
- Store mnemonics securely - they provide access to all derived accounts
- Consider using different account indices for different purposes
#### API Keys (For ABI Fetching)
```bash
export ETHERSCAN_API_KEY="your-api-key-here"
```
This API key is optional but required for:
- Automatic ABI fetching from block explorers (`get_contract_abi` tool)
- Auto-fetching ABIs when reading contracts (`read_contract` tool with `abiJson` parameter)
- The `fetch_and_analyze_abi` prompt
Get your free API key from:
- [Etherscan](https://etherscan.io/apis) - For Ethereum and compatible chains
- The same key works across all 60+ EVM networks via the Etherscan v2 API
### Server Configuration
The server uses the following default configuration:
- **Default Chain ID**: 1 (Ethereum Mainnet)
- **Server Port**: 3001
- **Server Host**: 0.0.0.0 (accessible from any network interface)
These values are hardcoded in the application. If you need to modify them, you can edit the following files:
- For chain configuration: `src/core/chains.ts`
- For server configuration: `src/server/http-server.ts`
## 🚀 Usage
### Using npx (No Installation Required)
You can run the MCP EVM Server directly without installation using npx:
```bash
# Run the server in stdio mode (for CLI tools)
npx @mcpdotdirect/evm-mcp-server
# Run the server in HTTP mode (for web applications)
npx @mcpdotdirect/evm-mcp-server --http
```
### Running the Server Locally
Start the server using stdio (for embedding in CLI tools):
```bash
# Start the stdio server
bun start
# Development mode with auto-reload
bun dev
```
Or start the HTTP server with SSE for web applications:
```bash
# Start the HTTP server
bun start:http
# Development mode with auto-reload
bun dev:http
```
### Connecting to the Server
Connect to this MCP server using any MCP-compatible client. For testing and debugging, you can use the [MCP Inspector](https://github.com/modelcontextprotocol/inspector).
### Connecting from Cursor
To connect to the MCP server from Cursor:
1. Open Cursor and go to Settings (gear icon in the bottom left)
2. Click on "Features" in the left sidebar
3. Scroll down to "MCP Servers" section
4. Click "Add new MCP server"
5. Enter the following details:
- Server name: `evm-mcp-server`
- Type: `command`
- Command: `npx @mcpdotdirect/evm-mcp-server`
6. Click "Save"
Once connected, you can use the MCP server's capabilities directly within Cursor. The server will appear in the MCP Servers list and can be enabled/disabled as needed.
### Using mcp.json with Cursor
For a more portable configuration that you can share with your team or use across projects, you can create an `.cursor/mcp.json` file in your project's root directory:
```json
{
"mcpServers": {
"evm-mcp-server": {
"command": "npx",
"args": ["-y", "@mcpdotdirect/evm-mcp-server"]
},
"evm-mcp-http": {
"command": "npx",
"args": ["-y", "@mcpdotdirect/evm-mcp-server", "--http"]
}
}
}
```
Place this file in your project's `.cursor` directory (create it if it doesn't exist), and Cursor will automatically detect and use these MCP server configurations when working in that project. This approach makes it easy to:
1. Share MCP configurations with your team
2. Version control your MCP setup
3. Use different server configurations for different projects
### Example: HTTP Mode with SSE
If you're developing a web application and want to connect to the HTTP server with Server-Sent Events (SSE), you can use this configuration:
```json
{
"mcpServers": {
"evm-mcp-sse": {
"url": "http://localhost:3001/sse"
}
}
}
```
This connects directly to the HTTP server's SSE endpoint, which is useful for:
- Web applications that need to connect to the MCP server from the browser
- Environments where running local commands isn't ideal
- Sharing a single MCP server instance among multiple users or applications
To use this configuration:
1. Create a `.cursor` directory in your project root if it doesn't exist
2. Save the above JSON as `mcp.json` in the `.cursor` directory
3. Restart Cursor or open your project
4. Cursor will detect the configuration and offer to enable the server(s)
### Example: Using the MCP Server in Cursor
After configuring the MCP server with `mcp.json`, you can easily use it in Cursor. Here's an example workflow:
1. Create a new JavaScript/TypeScript file in your project:
```javascript
// blockchain-example.js
async function main() {
try {
// Get ETH balance for an address using ENS
console.log("Getting ETH balance for vitalik.eth...");
// When using with Cursor, you can simply ask Cursor to:
// "Check the ETH balance of vitalik.eth on mainnet"
// Or "Transfer 0.1 ETH from my wallet to vitalik.eth"
// Cursor will use the MCP server to execute these operations
// without requiring any additional code from you
// This is the power of the MCP integration - your AI assistant
// can directly interact with blockchain data and operations
} catch (error) {
console.error("Error:", error.message);
}
}
main();
```
2. With the file open in Cursor, you can ask Cursor to:
- "Check the current ETH balance of vitalik.eth"
- "Look up the price of USDC on Ethereum"
- "Show me the latest block on Optimism"
- "Check if 0x1234... is a contract address"
3. Cursor will use the MCP server to execute these operations and return the results directly in your conversation.
The MCP server handles all the blockchain communication while allowing Cursor to understand and execute blockchain-related tasks through natural language.
### Connecting using Claude CLI
If you're using Claude CLI, you can connect to the MCP server with just two commands:
```bash
# Add the MCP server
claude mcp add evm-mcp-server npx @mcpdotdirect/evm-mcp-server
# Start Claude with the MCP server enabled
claude
```
### Example: Getting a Token Balance with ENS
```javascript
// Example of using the MCP client to check a token balance using ENS
const mcp = new McpClient("http://localhost:3000");
const result = await mcp.invokeTool("get-token-balance", {
tokenAddress: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", // USDC on Ethereum
ownerAddress: "vitalik.eth", // ENS name instead of address
network: "ethereum",
});
console.log(result);
// {
// tokenAddress: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
// owner: "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045",
// network: "ethereum",
// raw: "1000000000",
// formatted: "1000",
// symbol: "USDC",
// decimals: 6
// }
```
### Example: Resolving an ENS Name
```javascript
// Example of using the MCP client to resolve an ENS name to an address
const mcp = new McpClient("http://localhost:3000");
const result = await mcp.invokeTool("resolve-ens", {
ensName: "vitalik.eth",
network: "ethereum",
});
console.log(result);
// {
// ensName: "vitalik.eth",
// normalizedName: "vitalik.eth",
// resolvedAddress: "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045",
// network: "ethereum"
// }
```
### Example: Batch Multiple Calls with Multicall
```javascript
// Example of using multicall to batch multiple contract reads in a single RPC call
const mcp = new McpClient("http://localhost:3000");
const result = await mcp.invokeTool("multicall", {
network: "ethereum",
calls: [
{
contractAddress: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", // USDC
functionName: "balanceOf",
args: ["0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045"],
},
{
contractAddress: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", // USDC
functionName: "symbol",
},
{
contractAddress: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", // USDC
functionName: "decimals",
},
],
});
console.log(result);
// {
// network: "ethereum",
// totalCalls: 3,
// successfulCalls: 3,
// failedCalls: 0,
// results: [
// { contractAddress: "0xA0b...", functionName: "balanceOf", result: "1000000000", status: "success" },
// { contractAddress: "0xA0b...", functionName: "symbol", result: "USDC", status: "success" },
// { contractAddress: "0xA0b...", functionName: "decimals", result: "6", status: "success" }
// ]
// }
```
## 📚 API Reference
### Tools
The server provides 25 focused MCP tools for agents. **All tools that accept address parameters support both Ethereum addresses and ENS names.**
#### Wallet Information
| Tool Name | Description | Key Parameters |
| -------------------- | --------------------------------------------------------------- | -------------- |
| `get_wallet_address` | Get the address of the configured wallet (from EVM_PRIVATE_KEY) | none |
#### Network Information
| Tool Name | Description | Key Parameters |
| ------------------------ | ----------------------------------- | -------------- |
| `get_chain_info` | Get network information | `network` |
| `get_supported_networks` | List all supported EVM networks | none |
| `get_gas_price` | Get current gas prices on a network | `network` |
#### ENS Services
| Tool Name | Description | Key Parameters |
| -------------------- | ---------------------------------- | -------------------- |
| `resolve_ens_name` | Resolve ENS name to address | `ensName`, `network` |
| `lookup_ens_address` | Reverse lookup address to ENS name | `address`, `network` |
#### Block & Transaction Information
| Tool Name | Description | Key Parameters |
| ------------------------- | --------------------------------- | --------------------------------------- |
| `get_block` | Get block data | `blockNumber` or `blockHash`, `network` |
| `get_latest_block` | Get latest block data | `network` |
| `get_transaction` | Get transaction details | `txHash`, `network` |
| `get_transaction_receipt` | Get transaction receipt with logs | `txHash`, `network` |
| `wait_for_transaction` | Wait for transaction confirmation | `txHash`, `confirmations`, `network` |
#### Balance & Token Information
| Tool Name | Description | Key Parameters |
| ------------------- | ------------------------------ | ----------------------------------------------------------------------------------------------------- |
| `get_balance` | Get native token balance | `address` (address/ENS), `network` |
| `get_token_balance` | Check ERC20 token balance | `tokenAddress` (address/ENS), `ownerAddress` (address/ENS), `network` |
| `get_allowance` | Check token spending allowance | `tokenAddress` (address/ENS), `ownerAddress` (address/ENS), `spenderAddress` (address/ENS), `network` |
#### Smart Contract Interactions
| Tool Name | Description | Key Parameters |
| ------------------ | --------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------ |
| `get_contract_abi` | Fetch contract ABI from block explorer (60+ networks) | `contractAddress` (address/ENS), `network` |
| `read_contract` | Read smart contract state (auto-fetches ABI if needed) | `contractAddress`, `functionName`, `args[]`, `abiJson` (optional), `network` |
| `write_contract` | Execute state-changing functions (auto-fetches ABI if needed) | `contractAddress`, `functionName`, `args[]`, `value` (optional), `abiJson` (optional), `network` |
| `multicall` | Batch multiple read calls into a single RPC request (uses Multicall3) | `calls[]` (array of contract calls), `allowFailure` (optional), `network` |
#### Token Transfers
| Tool Name | Description | Key Parameters |
| ------------------------ | ------------------------------ | --------------------------------------------------------------------------------- |
| `transfer_native` | Send native tokens (ETH, etc.) | `to` (address/ENS), `amount`, `network` |
| `transfer_erc20` | Transfer ERC20 tokens | `tokenAddress` (address/ENS), `to` (address/ENS), `amount`, `network` |
| `approve_token_spending` | Approve token allowances | `tokenAddress` (address/ENS), `spenderAddress` (address/ENS), `amount`, `network` |
#### NFT Services
| Tool Name | Description | Key Parameters |
| --------------------- | ------------------------- | -------------------------------------------------------------------------------- |
| `get_nft_info` | Get NFT (ERC721) metadata | `tokenAddress` (address/ENS), `tokenId`, `network` |
| `get_erc1155_balance` | Check ERC1155 balance | `tokenAddress` (address/ENS), `tokenId`, `ownerAddress` (address/ENS), `network` |
#### Message Signing
| Tool Name | Description | Key Parameters |
| ----------------- | ---------------------------------------------------------------------------------------- | ------------------------------------------------------- |
| `sign_message` | Sign arbitrary messages for authentication and verification (SIWE, off-chain signatures) | `message` |
| `sign_typed_data` | Sign EIP-712 structured data for gasless transactions, permits, and meta-transactions | `domainJson`, `typesJson`, `primaryType`, `messageJson` |
### Resources
The server exposes blockchain data through the following MCP resource URIs. All resource URIs that accept addresses also support ENS names, which are automatically resolved to addresses.
#### Blockchain Resources
| Resource URI Pattern | Description |
| ------------------------------------------- | ---------------------------------------- |
| `evm://{network}/chain` | Chain information for a specific network |
| `evm://chain` | Ethereum mainnet chain information |
| `evm://{network}/block/{blockNumber}` | Block data by number |
| `evm://{network}/block/latest` | Latest block data |
| `evm://{network}/address/{address}/balance` | Native token balance |
| `evm://{network}/tx/{txHash}` | Transaction details |
| `evm://{network}/tx/{txHash}/receipt` | Transaction receipt with logs |
#### Token Resources
| Resource URI Pattern | Description |
| ---------------------------------------------------------------------- | ------------------------------ |
| `evm://{network}/token/{tokenAddress}` | ERC20 token information |
| `evm://{network}/token/{tokenAddress}/balanceOf/{address}` | ERC20 token balance |
| `evm://{network}/nft/{tokenAddress}/{tokenId}` | NFT (ERC721) token information |
| `evm://{network}/nft/{tokenAddress}/{tokenId}/isOwnedBy/{address}` | NFT ownership verification |
| `evm://{network}/erc1155/{tokenAddress}/{tokenId}/uri` | ERC1155 token URI |
| `evm://{network}/erc1155/{tokenAddress}/{tokenId}/balanceOf/{address}` | ERC1155 token balance |
## 🔒 Security Considerations
- **Private keys** are used only for transaction signing and are never stored by the server
- Consider implementing additional authentication mechanisms for production use
- Use HTTPS for the HTTP server in production environments
- Implement rate limiting to prevent abuse
- For high-value services, consider adding confirmation steps
## 📁 Project Structure
```
mcp-evm-server/
├── src/
│ ├── index.ts # Main stdio server entry point
│ ├── server/ # Server-related files
│ │ ├── http-server.ts # HTTP server with SSE
│ │ └── server.ts # General server setup
│ ├── core/
│ │ ├── chains.ts # Chain definitions and utilities
│ │ ├── resources.ts # MCP resources implementation
│ │ ├── tools.ts # MCP tools implementation
│ │ ├── prompts.ts # MCP prompts implementation
│ │ └── services/ # Core blockchain services
│ │ ├── index.ts # Operation exports
│ │ ├── balance.ts # Balance services
│ │ ├── transfer.ts # Token transfer services
│ │ ├── utils.ts # Utility functions
│ │ ├── tokens.ts # Token metadata services
│ │ ├── contracts.ts # Contract interactions
│ │ ├── transactions.ts # Transaction services
│ │ └── blocks.ts # Block services
│ │ └── clients.ts # RPC client utilities
├── package.json
├── tsconfig.json
└── README.md
```
## 🛠️ Development
To modify or extend the server:
1. Add new services in the appropriate file under `src/core/services/`
2. Register new tools in `src/core/tools.ts`
3. Register new resources in `src/core/resources.ts`
4. Add new network support in `src/core/chains.ts`
5. To change server configuration, edit the hardcoded values in `src/server/http-server.ts`
## 📄 License
This project is licensed under the terms of the [MIT License](./LICENSE).
================================================
FILE: bin/cli.js
================================================
#!/usr/bin/env node
import { fileURLToPath } from 'url';
import { dirname, resolve } from 'path';
import { spawn } from 'child_process';
import { createRequire } from 'module';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const require = createRequire(import.meta.url);
// Parse command line arguments
const args = process.argv.slice(2);
const httpMode = args.includes('--http') || args.includes('-h');
console.log(`Starting EVM MCP Server in ${httpMode ? 'HTTP' : 'stdio'} mode...`);
// Determine which file to execute
const scriptPath = resolve(__dirname, '../build', httpMode ? 'http-server.js' : 'index.js');
try {
// Check if the built files exist
require.resolve(scriptPath);
// Execute the server
const server = spawn('node', [scriptPath], {
stdio: 'inherit',
shell: false
});
server.on('error', (err) => {
console.error('Failed to start server:', err);
process.exit(1);
});
// Handle clean shutdown
const cleanup = () => {
if (!server.killed) {
server.kill();
}
};
process.on('SIGINT', cleanup);
process.on('SIGTERM', cleanup);
process.on('exit', cleanup);
} catch (error) {
console.error('Error: Server files not found. The package may not be built correctly.');
console.error('Please try reinstalling the package or contact the maintainers.');
console.error(error);
process.exit(1);
}
================================================
FILE: funding.json
================================================
{
"opRetro": {
"projectId": "0xc4f4309de505d2581218e6a4f9634e37d546c8f2bc51242540ca1486b965c0f2"
}
}
================================================
FILE: package.json
================================================
{
"name": "@mcpdotdirect/evm-mcp-server",
"module": "src/index.ts",
"type": "module",
"version": "2.0.4",
"description": "MCP server for interacting with EVM-compatible blockchains - supports 22 tools and 10 prompts across 60+ networks",
"bin": {
"evm-mcp-server": "./bin/cli.js"
},
"main": "build/index.js",
"files": [
"build/",
"bin/",
"README.md",
"LICENSE"
],
"scripts": {
"start": "bun run src/index.ts",
"build": "bun build src/index.ts --outdir build --target node",
"build:http": "bun build src/server/http-server.ts --outdir build --target node --outfile http-server.js",
"dev": "bun --watch src/index.ts",
"start:http": "bun run src/server/http-server.ts",
"dev:http": "bun --watch src/server/http-server.ts",
"prepublishOnly": "bun run build && bun run build:http",
"version:patch": "npm version patch",
"version:minor": "npm version minor",
"version:major": "npm version major",
"release": "npm publish",
"changelog": "conventional-changelog -p angular -i CHANGELOG.md -s -r 0",
"changelog:latest": "conventional-changelog -p angular -r 1 > RELEASE_NOTES.md",
"inspect": "npx @modelcontextprotocol/inspector node build/index.js"
},
"devDependencies": {
"@types/bun": "latest",
"@types/express": "^5.0.0",
"@types/node": "^22.0.0",
"conventional-changelog-cli": "^5.0.0"
},
"peerDependencies": {
"typescript": "^5.8.2"
},
"dependencies": {
"@modelcontextprotocol/sdk": "^1.22.0",
"express": "^4.21.2",
"viem": "^2.39.3",
"zod": "^3.24.3"
},
"keywords": [
"mcp",
"model-context-protocol",
"evm",
"blockchain",
"ethereum",
"web3",
"smart-contracts",
"ai",
"agent"
],
"author": "vcart <info@mcp.direct>",
"license": "MIT",
"engines": {
"node": ">=20.0.0"
},
"repository": {
"type": "git",
"url": "https://github.com/mcpdotdirect/evm-mcp-server"
},
"bugs": {
"url": "https://github.com/mcpdotdirect/evm-mcp-server/issues"
},
"homepage": "https://github.com/mcpdotdirect/evm-mcp-server#readme",
"publishConfig": {
"access": "public"
}
}
================================================
FILE: src/core/chains.ts
================================================
import { type Chain } from 'viem';
import {
// Mainnets
mainnet,
optimism,
arbitrum,
arbitrumNova,
base,
polygon,
polygonZkEvm,
avalanche,
bsc,
zksync,
linea,
celo,
gnosis,
fantom,
filecoin,
moonbeam,
moonriver,
cronos,
lumiaMainnet,
scroll,
mantle,
manta,
blast,
fraxtal,
mode,
metis,
kroma,
zora,
aurora,
canto,
flowMainnet,
// Testnets
sepolia,
optimismSepolia,
arbitrumSepolia,
baseSepolia,
polygonAmoy,
avalancheFuji,
bscTestnet,
zksyncSepoliaTestnet,
lineaSepolia,
lumiaTestnet,
scrollSepolia,
mantleSepoliaTestnet,
mantaSepoliaTestnet,
blastSepolia,
fraxtalTestnet,
modeTestnet,
metisSepolia,
kromaSepolia,
zoraSepolia,
celoAlfajores,
goerli,
holesky,
flowTestnet,
filecoinCalibration
} from 'viem/chains';
// Default configuration values
export const DEFAULT_RPC_URL = 'https://eth.llamarpc.com';
export const DEFAULT_CHAIN_ID = 1;
// Map chain IDs to chains
export const chainMap: Record<number, Chain> = {
// Mainnets
1: mainnet,
10: optimism,
42161: arbitrum,
42170: arbitrumNova,
8453: base,
137: polygon,
1101: polygonZkEvm,
43114: avalanche,
56: bsc,
324: zksync,
59144: linea,
42220: celo,
100: gnosis,
250: fantom,
314: filecoin,
1284: moonbeam,
1285: moonriver,
25: cronos,
534352: scroll,
5000: mantle,
169: manta,
994873017: lumiaMainnet,
81457: blast,
252: fraxtal,
34443: mode,
1088: metis,
255: kroma,
7777777: zora,
1313161554: aurora,
7700: canto,
747: flowMainnet,
// Testnets
11155111: sepolia,
11155420: optimismSepolia,
421614: arbitrumSepolia,
84532: baseSepolia,
80002: polygonAmoy,
43113: avalancheFuji,
97: bscTestnet,
300: zksyncSepoliaTestnet,
59141: lineaSepolia,
1952959480: lumiaTestnet,
534351: scrollSepolia,
5003: mantleSepoliaTestnet,
3441006: mantaSepoliaTestnet,
168587773: blastSepolia,
2522: fraxtalTestnet,
919: modeTestnet,
59902: metisSepolia,
2358: kromaSepolia,
999999999: zoraSepolia,
44787: celoAlfajores,
5: goerli,
17000: holesky,
545: flowTestnet,
314159: filecoinCalibration,
};
// Map network names to chain IDs for easier reference
export const networkNameMap: Record<string, number> = {
// Mainnets
'ethereum': 1,
'mainnet': 1,
'eth': 1,
'optimism': 10,
'op': 10,
'arbitrum': 42161,
'arb': 42161,
'arbitrum-nova': 42170,
'arbitrumnova': 42170,
'base': 8453,
'polygon': 137,
'matic': 137,
'polygon-zkevm': 1101,
'polygonzkevm': 1101,
'avalanche': 43114,
'avax': 43114,
'binance': 56,
'bsc': 56,
'zksync': 324,
'linea': 59144,
'celo': 42220,
'gnosis': 100,
'xdai': 100,
'fantom': 250,
'ftm': 250,
'filecoin': 314,
'fil': 314,
'moonbeam': 1284,
'moonriver': 1285,
'cronos': 25,
'scroll': 534352,
'mantle': 5000,
'manta': 169,
'lumia': 994873017,
'blast': 81457,
'fraxtal': 252,
'mode': 34443,
'metis': 1088,
'kroma': 255,
'zora': 7777777,
'aurora': 1313161554,
'canto': 7700,
'flow': 747,
// Testnets
'sepolia': 11155111,
'optimism-sepolia': 11155420,
'optimismsepolia': 11155420,
'arbitrum-sepolia': 421614,
'arbitrumsepolia': 421614,
'base-sepolia': 84532,
'basesepolia': 84532,
'polygon-amoy': 80002,
'polygonamoy': 80002,
'avalanche-fuji': 43113,
'avalanchefuji': 43113,
'fuji': 43113,
'bsc-testnet': 97,
'bsctestnet': 97,
'zksync-sepolia': 300,
'zksyncsepolia': 300,
'linea-sepolia': 59141,
'lineasepolia': 59141,
'lumia-testnet': 1952959480,
'scroll-sepolia': 534351,
'scrollsepolia': 534351,
'mantle-sepolia': 5003,
'mantlesepolia': 5003,
'manta-sepolia': 3441006,
'mantasepolia': 3441006,
'blast-sepolia': 168587773,
'blastsepolia': 168587773,
'fraxtal-testnet': 2522,
'fraxtaltestnet': 2522,
'mode-testnet': 919,
'modetestnet': 919,
'metis-sepolia': 59902,
'metissepolia': 59902,
'kroma-sepolia': 2358,
'kromasepolia': 2358,
'zora-sepolia': 999999999,
'zorasepolia': 999999999,
'celo-alfajores': 44787,
'celoalfajores': 44787,
'alfajores': 44787,
'goerli': 5,
'holesky': 17000,
'flow-testnet': 545,
'filecoin-calibration': 314159,
};
// Map chain IDs to RPC URLs
export const rpcUrlMap: Record<number, string> = {
// Mainnets
1: 'https://eth.llamarpc.com',
10: 'https://mainnet.optimism.io',
42161: 'https://arb1.arbitrum.io/rpc',
42170: 'https://nova.arbitrum.io/rpc',
8453: 'https://mainnet.base.org',
137: 'https://polygon-rpc.com',
1101: 'https://zkevm-rpc.com',
43114: 'https://api.avax.network/ext/bc/C/rpc',
56: 'https://bsc-dataseed.binance.org',
324: 'https://mainnet.era.zksync.io',
59144: 'https://rpc.linea.build',
42220: 'https://forno.celo.org',
100: 'https://rpc.gnosischain.com',
250: 'https://rpc.ftm.tools',
314: 'https://api.node.glif.io/rpc/v1',
1284: 'https://rpc.api.moonbeam.network',
1285: 'https://rpc.api.moonriver.moonbeam.network',
25: 'https://evm.cronos.org',
534352: 'https://rpc.scroll.io',
5000: 'https://rpc.mantle.xyz',
169: 'https://pacific-rpc.manta.network/http',
81457: 'https://rpc.blast.io',
252: 'https://rpc.frax.com',
994873017: 'https://mainnet-rpc.lumia.org',
34443: 'https://mainnet.mode.network',
1088: 'https://andromeda.metis.io/?owner=1088',
255: 'https://api.kroma.network',
7777777: 'https://rpc.zora.energy',
1313161554: 'https://mainnet.aurora.dev',
7700: 'https://canto.gravitychain.io',
747: 'https://mainnet.evm.nodes.onflow.org',
// Testnets
11155111: 'https://sepolia.drpc.org',
11155420: 'https://sepolia.optimism.io',
421614: 'https://sepolia-rpc.arbitrum.io/rpc',
84532: 'https://sepolia.base.org',
80002: 'https://rpc-amoy.polygon.technology',
43113: 'https://api.avax-test.network/ext/bc/C/rpc',
97: 'https://data-seed-prebsc-1-s1.binance.org:8545',
300: 'https://sepolia.era.zksync.dev',
59141: 'https://rpc.sepolia.linea.build',
534351: 'https://sepolia-rpc.scroll.io',
5003: 'https://rpc.sepolia.mantle.xyz',
3441006: 'https://pacific-rpc.sepolia.manta.network/http',
1952959480: 'https://testnet-rpc.lumia.org',
168587773: 'https://sepolia.blast.io',
2522: 'https://rpc.testnet.frax.com',
919: 'https://sepolia.mode.network',
59902: 'https://sepolia.metis.io/?owner=59902',
2358: 'https://api.sepolia.kroma.network',
999999999: 'https://sepolia.rpc.zora.energy',
44787: 'https://alfajores-forno.celo-testnet.org',
5: 'https://rpc.ankr.com/eth_goerli',
17000: 'https://ethereum-holesky.publicnode.com',
545: 'https://testnet.evm.nodes.onflow.org',
314159: 'https://api.calibration.node.glif.io/rpc/v1',
};
/**
* Resolves a chain identifier (number or string) to a chain ID
* @param chainIdentifier Chain ID (number) or network name (string)
* @returns The resolved chain ID
*/
export function resolveChainId(chainIdentifier: number | string): number {
if (typeof chainIdentifier === 'number') {
return chainIdentifier;
}
// Convert to lowercase for case-insensitive matching
const networkName = chainIdentifier.toLowerCase();
// Check if the network name is in our map
const chainId = networkNameMap[networkName];
if (chainId !== undefined) {
return chainId;
}
// Try parsing as a number
const parsedId = parseInt(networkName);
if (!isNaN(parsedId)) {
return parsedId;
}
// Default to mainnet if not found
return DEFAULT_CHAIN_ID;
}
/**
* Returns the chain configuration for the specified chain ID or network name
* @param chainIdentifier Chain ID (number) or network name (string)
* @returns The chain configuration
* @throws Error if the network is not supported (when string is provided)
*/
export function getChain(chainIdentifier: number | string = DEFAULT_CHAIN_ID): Chain {
if (typeof chainIdentifier === 'string') {
const networkName = chainIdentifier.toLowerCase();
// Try to get from direct network name mapping first
if (networkNameMap[networkName]) {
return chainMap[networkNameMap[networkName]] || mainnet;
}
// If not found, throw an error
throw new Error(`Unsupported network: ${chainIdentifier}`);
}
// If it's a number, return the chain from chainMap
return chainMap[chainIdentifier] || mainnet;
}
/**
* Gets the appropriate RPC URL for the specified chain ID or network name
* @param chainIdentifier Chain ID (number) or network name (string)
* @returns The RPC URL for the specified chain
*/
export function getRpcUrl(chainIdentifier: number | string = DEFAULT_CHAIN_ID): string {
const chainId = typeof chainIdentifier === 'string'
? resolveChainId(chainIdentifier)
: chainIdentifier;
return rpcUrlMap[chainId] || DEFAULT_RPC_URL;
}
/**
* Get a list of supported networks
* @returns Array of supported network names (excluding short aliases)
*/
export function getSupportedNetworks(): string[] {
return Object.keys(networkNameMap)
.filter(name => name.length > 2) // Filter out short aliases
.sort();
}
================================================
FILE: src/core/prompts.ts
================================================
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
/**
* Register task-oriented prompts with the MCP server
*
* All prompts follow a consistent structure:
* - Clear objective statement
* - Step-by-step instructions
* - Expected outputs
* - Safety/security considerations
*
* Prompts guide the model through complex workflows that would otherwise
* require multiple tool calls in the correct sequence.
*
* @param server The MCP server instance
*/
export function registerEVMPrompts(server: McpServer) {
// ============================================================================
// TRANSACTION PROMPTS
// ============================================================================
server.registerPrompt(
"prepare_transfer",
{
description: "Safely prepare and execute a token transfer with validation checks",
argsSchema: {
tokenType: z.enum(["native", "erc20"]).describe("Token type: 'native' for ETH/MATIC or 'erc20' for contract tokens"),
recipient: z.string().describe("Recipient address or ENS name"),
amount: z.string().describe("Amount to transfer (in ether for native, token units for ERC20)"),
network: z.string().optional().describe("Network name (default: ethereum)"),
tokenAddress: z.string().optional().describe("Token contract address (required for ERC20)")
}
},
({ tokenType, recipient, amount, network = "ethereum", tokenAddress }) => ({
messages: [{
role: "user",
content: {
type: "text",
text: `# Token Transfer Task
**Objective**: Safely transfer ${amount} ${tokenType === "native" ? "native tokens" : "ERC20 tokens"} to ${recipient} on ${network}
## Validation & Checks
Before executing any transfer:
1. **Wallet Verification**: Call \`get_wallet_address\` to confirm the sending wallet
2. **Balance Check**:
${tokenType === "native"
? "- Call `get_balance` to verify native token balance"
: "- Call `get_token_balance` with tokenAddress=${tokenAddress} to verify balance"}
3. **Gas Analysis**: Call \`get_gas_price\` to assess current network costs
${tokenType === "erc20" ? `4. **Approval Check**: Call \`get_allowance\` to verify approval (if needed for protocols)` : ""}
## Execution Steps
${tokenType === "native" ? `
1. Summarize: sender address, recipient, amount, and estimated gas cost
2. Request confirmation from user
3. Call \`transfer_native\` with to="${recipient}", amount="${amount}", network="${network}"
4. Return transaction hash to user
5. Call \`wait_for_transaction\` to confirm completion
` : `
1. Check if approval is needed:
- If allowance < amount: Call \`approve_token_spending\` first
- Then proceed with transfer
2. Summarize: sender, recipient, token, amount, decimals, gas estimate
3. Request confirmation
4. Call \`transfer_erc20\` with tokenAddress, recipient, amount
5. Wait for confirmation with \`wait_for_transaction\`
`}
## Output Format
- **Transaction Hash**: Clear hex value
- **Status**: Pending or Confirmed
- **Cost Estimate**: Gas price and total cost
- **User Confirmation**: Always ask before sending
## Safety Considerations
- Never send more than available balance
- Double-check recipient address
- Warn about high gas prices
- Explain any approval requirements
`
}
}]
})
);
server.registerPrompt(
"diagnose_transaction",
{
description: "Analyze transaction status, failures, and provide debugging insights",
argsSchema: {
txHash: z.string().describe("Transaction hash to diagnose (0x...)"),
network: z.string().optional().describe("Network name (default: ethereum)")
}
},
({ txHash, network = "ethereum" }) => ({
messages: [{
role: "user",
content: {
type: "text",
text: `# Transaction Diagnosis
**Objective**: Analyze transaction ${txHash} on ${network} and identify any issues
## Investigation Process
### 1. Gather Transaction Data
- Call \`get_transaction\` to fetch transaction details
- Call \`get_transaction_receipt\` to get status and gas used
- Note: both calls are read-only and free
### 2. Status Assessment
Determine transaction state:
- **Pending**: Not yet mined (check mempool conditions)
- **Confirmed**: Successfully executed (status='success')
- **Failed**: Execution failed (status='failed')
- **Replaced**: Transaction was dropped/replaced (check nonce)
### 3. Failure Analysis
If transaction failed, investigate:
**Out of Gas**:
- Compare gasUsed vs gasLimit in receipt
- If gasUsed >= gasLimit, suggest increasing gas limit
**Contract Revert**:
- Check function called and parameters
- Verify sufficient balance/approvals
- Look for require/revert statements in contract
**Invalid Nonce**:
- Compare transaction nonce with account's current nonce
- Suggest pending transactions may need replacement
**Other Issues**:
- Check sender/recipient addresses are valid
- Verify function parameters are correct type
- Look for access control restrictions
### 4. Gas Analysis
- Calculate gas cost: gasUsed * gasPrice
- Compare to current gas prices (call \`get_gas_price\`)
- Assess if overpaid or underpaid
## Output Format
Provide structured diagnosis:
- **Status**: Pending/Confirmed/Failed with reason
- **Transaction Hash**: The hash analyzed
- **From/To**: Addresses involved
- **Function**: What was called
- **Gas Analysis**: Used vs limit, cost
- **Issue (if failed)**: Root cause and explanation
- **Recommended Actions**: Next steps to resolve
## Important Notes
- Be specific about error messages and codes
- Provide actionable recommendations
- Link issues to specific contract behavior
- Suggest solutions (retry, increase gas, fix parameters, etc.)
`
}
}]
})
);
// ============================================================================
// WALLET ANALYSIS PROMPTS
// ============================================================================
server.registerPrompt(
"analyze_wallet",
{
description: "Get comprehensive overview of wallet assets, balances, and activity",
argsSchema: {
address: z.string().describe("Wallet address or ENS name to analyze"),
network: z.string().optional().describe("Network name (default: ethereum)"),
tokens: z.string().optional().describe("Comma-separated token addresses to check")
}
},
({ address, network = "ethereum", tokens }) => {
const tokenList = tokens ? tokens.split(',').map(t => t.trim()) : [];
return {
messages: [{
role: "user",
content: {
type: "text",
text: `# Wallet Analysis
**Objective**: Provide complete asset overview for ${address} on ${network}
## Information Gathering
### 1. Address Resolution
- If input contains '.eth', call \`resolve_ens_name\` to get address
- Otherwise use as direct address
- Provide both resolved address and ENS name if applicable
### 2. Native Token Balance
- Call \`get_balance\` to fetch native token (ETH/MATIC/etc) balance
- Report both wei and ether/human-readable formats
- Note: Free read-only call
### 3. Token Balances
${tokenList.length > 0
? `- Call \`get_token_balance\` for each token:\n${tokenList.map(t => ` * ${t}`).join('\n')}`
: `- If specific tokens provided: call \`get_token_balance\` for each
- Include token symbol and decimals if available`}
## Output Format
Provide analysis with clear sections:
**Wallet Overview**
- Address: [address]
- ENS Name: [name or none]
- Network: [network]
**Native Token Balance**
- Ether: [formatted amount]
- Wei: [raw amount]
- In USD (if price available): [estimated value]
**Token Holdings** (if requested)
- Token: [address]
- Symbol: [symbol]
- Balance: [formatted]
- Decimals: [decimals]
**Summary**
- Total assets value (if prices available)
- Primary holdings
- Notable observations
## Key Considerations
- Show both formatted and raw amounts
- Include token decimals for precision
- Note if wallet has low/no balance
- Highlight any unusual patterns
- Be clear about what data was available vs not
`
}
}]
};
}
);
server.registerPrompt(
"audit_approvals",
{
description: "Review token approvals and identify security risks from unlimited spend",
argsSchema: {
address: z.string().optional().describe("Wallet to audit (default: configured wallet)"),
tokenAddress: z.string().describe("Token contract address to check approvals for"),
network: z.string().optional().describe("Network name (default: ethereum)")
}
},
({ address, tokenAddress, network = "ethereum" }) => ({
messages: [{
role: "user",
content: {
type: "text",
text: `# Token Approval Audit
**Objective**: Check and analyze token approvals to identify security risks
## Approval Analysis
### 1. Get Configured Wallet (if needed)
- If no address provided: call \`get_wallet_address\` to get the configured wallet
- Use that as the owner for approval checks
### 2. Check Current Approvals
- Call \`get_allowance\` with:
* tokenAddress: ${tokenAddress}
* ownerAddress: [wallet address from step 1]
* spenderAddress: [contract being analyzed]
- Note the allowance amount returned
### 3. Interpret Results
**Allowance = 0**
- No approval set
- User must approve before spender can use tokens
- Safe state
**Allowance < Max Value**
- Limited approval (safest approach)
- Spender can only use up to this amount
- Tokens are protected
**Allowance = Max uint256 (unlimited)**
- Dangerous! Spender has unlimited access
- Common but risky pattern
- Should be revoked if not actively used
## Security Assessment
For each approval found:
1. **Risk Level**: Low/Medium/High based on:
- Is it unlimited (high risk)?
- How trusted is the spender?
- Is it actively used?
2. **Recommendations**:
- Revoke unknown/untrusted spenders
- Lower limits on high-risk approvals
- Keep active approvals but monitor
- Remove expired/legacy approvals
## Output Format
**Token Approval Audit Report**
For each spender:
- **Spender Address**: [contract address]
- **Current Allowance**: [amount or "Unlimited"]
- **Risk Level**: Low/Medium/High
- **Status**: Active/Unused
- **Recommendation**: Keep/Reduce/Revoke
**Summary**
- Total dangerous approvals: [count]
- Recommendations: [action items]
- Overall risk: Safe/Moderate/High
## Important Notes
- Unlimited approvals are a major attack vector
- Only approve what's necessary
- Regularly audit and revoke unused approvals
- Be especially careful with new/unknown contracts
`
}
}]
})
);
// ============================================================================
// SMART CONTRACT ANALYSIS PROMPTS
// ============================================================================
server.registerPrompt(
"fetch_and_analyze_abi",
{
description: "Fetch contract ABI from block explorer and provide comprehensive analysis",
argsSchema: {
contractAddress: z.string().describe("Contract address to analyze"),
network: z.string().optional().describe("Network name (default: ethereum)"),
findFunction: z.string().optional().describe("Specific function to analyze (e.g., 'swap', 'mint')")
}
},
({ contractAddress, network = "ethereum", findFunction }) => ({
messages: [{
role: "user",
content: {
type: "text",
text: `# ABI Fetch and Analysis
**Objective**: Retrieve and analyze contract ABI from block explorer
## Prerequisites
- Contract must be verified on block explorer (Etherscan/Polygonscan/etc)
- ETHERSCAN_API_KEY environment variable required
- Supports 30+ EVM networks via unified Etherscan v2 API
- Read-only, no gas cost
## Fetching Process
### 1. Fetch the ABI
- Call \`get_contract_abi\` with contractAddress="${contractAddress}", network="${network}"
- Returns full ABI array with all functions, events, state variables
- Includes metadata about each function (inputs, outputs, mutability)
### 2. Parse and Categorize
Organize functions by type:
**View/Pure Functions** (Read-only, free):
- Check current state
- Query data without state change
- Safe to call
**State-Changing Functions**:
- Payable: require ETH value
- Nonpayable: modify contract state
- Cost gas, need signer
**Admin Functions**:
- Often restricted (onlyOwner, etc)
- Control contract behavior
- High risk if compromised
### 3. Analyze Structure
- Count functions by type
- Identify events and their usage
- Look for special functions (constructor, fallback, receive)
- Check for custom errors
${findFunction ? `### 4. Find Specific Function
- Search for "${findFunction}" in ABI
- Document: inputs, outputs, mutability
- Explain what it does
- Note any access controls` : `### 4. Key Functions
- Identify most important/used functions
- Explain inputs and outputs
- Note special requirements`}
## Function Analysis Format
For important functions provide:
- **Name**: Function name
- **Type**: View/Pure/Payable/Nonpayable
- **Inputs**: Parameter names and types with descriptions
- **Outputs**: Return values and types
- **Access**: Public/External/Restricted
- **Purpose**: What it does
- **Usage**: How to call it
## Security Analysis
Look for:
- **Proxy Patterns**: Is this a proxy contract?
- **Access Controls**: Who can call what?
- **Special Functions**: Initialization, upgrade paths
- **Obvious Issues**: Reentrancy risks, overflow/underflow patterns
- **Standard Compliance**: Is it ERC20/721/1155 compatible?
## Output Format
**Contract Analysis Report**
- **Contract Type**: Identified purpose (Token/DEX/Lending/etc)
- **Network**: Where deployed
- **Verified**: Yes (since we fetched ABI)
- **Function Count**: Total functions by type
**Function Categories**:
- View/Pure: [list of read functions]
- Write: [list of state-changing functions]
- Admin: [restricted functions]
**Key Functions**:
[Detailed analysis of important functions]
**Security Notes**:
[Vulnerabilities, patterns, recommendations]
**How to Interact**:
[Step-by-step guide for common operations]
`
}
}]
})
);
server.registerPrompt(
"explore_contract",
{
description: "Analyze contract functions and state without requiring full ABI",
argsSchema: {
contractAddress: z.string().describe("Contract address to explore"),
network: z.string().optional().describe("Network name (default: ethereum)"),
fetchAbi: z.string().optional().describe("Set to 'true' to auto-fetch ABI (requires ETHERSCAN_API_KEY)")
}
},
({ contractAddress, network = "ethereum", fetchAbi }) => ({
messages: [{
role: "user",
content: {
type: "text",
text: `# Contract Exploration
**Objective**: Understand what contract ${contractAddress} does and how to use it
## Exploration Strategy
${fetchAbi === 'true'
? `### With Full ABI (Fetched)
1. Call \`get_contract_abi\` to fetch verified ABI
2. Parse all available functions
3. Call \`read_contract\` for important state functions
4. Build comprehensive understanding
`
: `### Without Full ABI (Probing)
1. Test common function signatures
2. Call \`read_contract\` with standard functions:
- name(), symbol(), decimals(), totalSupply()
- owner(), paused(), version()
- balanceOf(), allowance(), totalSupply()
3. Infer contract type from successful calls
`}
## Detection Process
### 1. Identify Contract Type
Based on available functions, determine:
- **Token**: Has name, symbol, decimals, totalSupply, balanceOf
- **NFT/ERC721**: Has tokenURI, ownerOf, name, symbol
- **NFT/ERC1155**: Has uri, balanceOf, balanceOfBatch
- **Staking**: Has stake, unstake, reward, claim functions
- **DEX**: Has swap, liquidity, pair functions
- **Other**: Analyze unique functions
### 2. Gather Key Information
For each contract type:
**Token (ERC20)**:
- name, symbol, decimals, totalSupply
- If owner, supply cap, minting rules
- If tax/fee mechanism
**NFT (ERC721)**:
- name, symbol, totalSupply
- baseURI, tokenURI patterns
- royalty info if available
**Staking/Farming**:
- Pool info, APY, reward token
- Lockup periods, early withdrawal penalties
- Reward distribution mechanism
### 3. Security Assessment
- Check for pause functions (risk of rug)
- Look for upgrade mechanisms (upgradeable proxy)
- Identify admin-only functions
- Note unusual patterns
## Output Format
**Contract Overview**
- Address: [address]
- Type: [identified type]
- Network: [network]
- Verified: [yes/if ABI was fetched]
**Key Properties**
[Type-specific details discovered]
**Available Functions**
- Read-only: [list]
- State-changing: [list]
- Admin: [list if any]
**How to Use**
[Step-by-step guide for primary use case]
**Security Notes**
[Observations and recommendations]
**Limitations**
[What couldn't be determined without full ABI]
## When to Use ABI Fetch
- Need complete function list
- Want detailed parameter information
- Exploring unfamiliar/complex contracts
- Security due diligence
- Learn contract architecture
`
}
}]
})
);
// ============================================================================
// NETWORK & EDUCATION PROMPTS
// ============================================================================
server.registerPrompt(
"interact_with_contract",
{
description: "Safely execute write operations on a smart contract with validation and confirmation",
argsSchema: {
contractAddress: z.string().describe("Contract address to interact with"),
functionName: z.string().describe("Function to call (e.g., 'mint', 'swap', 'stake')"),
args: z.string().optional().describe("Comma-separated function arguments"),
value: z.string().optional().describe("ETH value to send (for payable functions)"),
network: z.string().optional().describe("Network name (default: ethereum)")
}
},
({ contractAddress, functionName, args, value, network = "ethereum" }) => {
const argsList = args ? args.split(',').map(a => a.trim()) : [];
return {
messages: [{
role: "user",
content: {
type: "text",
text: `# Smart Contract Interaction
**Objective**: Safely execute ${functionName} on contract ${contractAddress} on ${network}
## Prerequisites Check
### 1. Wallet Verification
- Call \`get_wallet_address\` to confirm the wallet that will execute this transaction
- Verify this is the correct wallet for this operation
### 2. Contract Analysis
- Call \`get_contract_abi\` to fetch and analyze the contract ABI
- Verify the function exists and understand its parameters
- Check function type:
* **View/Pure**: Read-only (use \`read_contract\` instead)
* **Nonpayable**: State-changing, no ETH required
* **Payable**: State-changing, can accept ETH
### 3. Function Parameter Validation
For function: **${functionName}**
${argsList.length > 0 ? `Arguments provided: ${argsList.join(', ')}` : 'No arguments provided'}
- Verify parameter types match the ABI
- Validate addresses are checksummed
- Check numeric values are in correct units
- Resolve any ENS names to addresses if needed
### 4. Pre-execution Checks
**Balance Check**:
- Call \`get_balance\` to verify sufficient native token balance
- Account for gas costs + value (if payable)
**Gas Estimation**:
- Call \`get_gas_price\` to estimate transaction cost
- Calculate total cost: (gas_price * estimated_gas) + value
**State Verification** (if applicable):
- Use \`read_contract\` to check current contract state
- Verify conditions are met (e.g., allowances, balances, ownership)
## Execution Process
### 1. Present Summary to User
Before executing, show:
- **Contract**: ${contractAddress}
- **Network**: ${network}
- **Function**: ${functionName}
- **Arguments**: ${argsList.length > 0 ? argsList.join(', ') : 'None'}
${value ? `- **Value**: ${value} ETH` : ''}
- **From**: [wallet address from step 1]
- **Estimated Gas Cost**: [from gas estimation]
- **Total Cost**: [gas + value]
### 2. Request User Confirmation
⚠️ **IMPORTANT**: Always ask user to confirm before executing write operations
- Clearly state what will happen
- Show all costs involved
- Explain any risks or irreversible actions
### 3. Execute Transaction
Only after user confirms:
\`\`\`
Call write_contract with:
- contractAddress: "${contractAddress}"
- functionName: "${functionName}"
${argsList.length > 0 ? `- args: ${JSON.stringify(argsList)}` : ''}
${value ? `- value: "${value}"` : ''}
- network: "${network}"
\`\`\`
### 4. Monitor Transaction
After execution:
1. Return transaction hash to user
2. Call \`wait_for_transaction\` to monitor confirmation
3. Call \`get_transaction_receipt\` to verify success
4. If failed, call \`diagnose_transaction\` to understand why
## Output Format
**Pre-Execution Summary**:
- Contract details
- Function and parameters
- Cost breakdown
- Risk assessment
**Confirmation Request**:
"Ready to execute ${functionName} on ${contractAddress}. This will cost approximately [X] ETH. Proceed? (yes/no)"
**Execution Result**:
- Transaction Hash: [hash]
- Status: Pending/Confirmed/Failed
- Block Number: [if confirmed]
- Gas Used: [actual gas used]
- Total Cost: [final cost]
## Safety Considerations
### Critical Checks
- ✅ Verify contract is verified on block explorer
- ✅ Check function parameters are correct type and format
- ✅ Ensure sufficient balance for gas + value
- ✅ Validate addresses (no typos, correct network)
- ✅ Understand what the function does before calling
### Common Risks
- **Irreversible**: Most blockchain transactions cannot be undone
- **Gas Loss**: Failed transactions still consume gas
- **Approval Risks**: Be careful with unlimited approvals
- **Reentrancy**: Some functions may be vulnerable
- **Access Control**: Verify you have permission to call this function
### Red Flags
🚨 Stop and warn user if:
- Contract is not verified
- Function requires admin/owner privileges you don't have
- Unusually high gas estimate
- Suspicious parameter values
- Contract has known vulnerabilities
## Error Handling
If transaction fails:
1. Get the revert reason from receipt
2. Check common issues:
- Insufficient balance/allowance
- Access control (onlyOwner, etc.)
- Invalid parameters
- Contract paused
- Slippage (for DEX operations)
3. Provide actionable fix suggestions
4. Offer to retry with corrected parameters
## Example Workflow
For a token mint operation:
1. ✅ Verify wallet
2. ✅ Fetch contract ABI
3. ✅ Check mint function exists and is callable
4. ✅ Verify sufficient ETH for gas
5. ✅ Show summary: "Minting 1 NFT will cost ~0.002 ETH"
6. ⏸️ Wait for user confirmation
7. ✅ Execute write_contract
8. ✅ Monitor transaction
9. ✅ Confirm success and return token ID
**Remember**: Always prioritize user safety and transparency!
`
}
}]
};
}
);
server.registerPrompt(
"explain_evm_concept",
{
description: "Explain EVM and blockchain concepts with examples",
argsSchema: {
concept: z.string().describe("Concept to explain (gas, nonce, smart contracts, MEV, etc)")
}
},
({ concept }) => ({
messages: [{
role: "user",
content: {
type: "text",
text: `# Concept Explanation: ${concept}
**Objective**: Provide clear, practical explanation of "${concept}"
## Explanation Structure
### 1. Definition
- What is it?
- Simple one-sentence summary
- Technical name/terminology
### 2. How It Works
- Step-by-step explanation
- Why it exists/why it's important
- How it relates to blockchain
### 3. Real-World Analogy
- Compare to familiar concept
- Make it relatable for beginners
- Highlight key differences
### 4. Practical Examples
- Real transaction examples
- Numbers and metrics where applicable
- Common scenarios
- Edge cases or gotchas
### 5. Relevance to Users
- Why should developers care?
- How does it affect transactions?
- How to optimize/reduce costs?
- Common mistakes to avoid
## Output Format
Provide explanation in sections:
**What is ${concept}?**
[Definition and overview]
**How Does It Work?**
[Mechanics and process]
**Example**
[Real or hypothetical scenario]
**Key Takeaways**
[Bullet points of important facts]
**Common Questions**
- Question 1? Answer
- Question 2? Answer
## Important
- Use clear, non-technical language first
- Progress to technical details
- Include concrete numbers where helpful
- Be honest about complexity
- Suggest further learning if needed
`
}
}]
})
);
server.registerPrompt(
"compare_networks",
{
description: "Compare multiple EVM networks on key metrics and characteristics",
argsSchema: {
networks: z.string().describe("Comma-separated network names (ethereum,polygon,arbitrum)")
}
},
({ networks }) => {
const networkList = networks.split(',').map(n => n.trim());
return {
messages: [{
role: "user",
content: {
type: "text",
text: `# Network Comparison
**Objective**: Compare ${networkList.join(', ')} on key metrics
## Comparison Metrics
### 1. Network Health (Current)
For each network, call:
- \`get_chain_info\` for chain ID and current block
- \`get_gas_price\` for current gas costs
- \`get_latest_block\` for block time and recent activity
### 2. Key Characteristics
Compare across these dimensions:
**Architecture**:
- Execution layer (Rollup/Sidechain/L1)
- Consensus mechanism
- Finality
- Decentralization level
**Performance**:
- Block time (seconds per block)
- Transactions per second (TPS)
- Confirmation time
- Throughput
**Costs**:
- Current gas prices (in gwei)
- Average transaction cost
- Cost to deploy contract
- Price trends
**Security**:
- Validator count / decentralization
- Mainnet maturity
- Track record
- Security audits
**Ecosystem**:
- Major protocols deployed
- Liquidity depth
- Developer activity
- Community size
## Comparison Table
Create table with:
- Network name
- Block time
- TPS capacity
- Current gas (gwei)
- Est. tx cost (USD)
- Security level
- Best for
## Analysis
For each network:
- **Strengths**: What it does well
- **Weaknesses**: Limitations
- **Best Use Cases**: When to use
- **Trade-offs**: Speed vs cost vs security
## Recommendations
Provide guidance:
- For small frequent transactions: [network]
- For large one-time transfers: [network]
- For DeFi/trading: [network]
- For NFTs: [network]
- For cost optimization: [network]
## Output Format
**Network Comparison Analysis**
[Comparison table]
**Network Profiles**
For each network:
- Overview
- Current metrics
- Strengths
- Weaknesses
- Best use cases
**Recommendations**
Based on user needs:
- Speed priority: [suggestion]
- Cost priority: [suggestion]
- Security priority: [suggestion]
- Overall best: [suggestion]
**Decision Matrix**
Help user choose based on:
- Transaction frequency
- Transaction size
- Budget constraints
- Required finality
- Ecosystem needs
`
}
}]
};
}
);
server.registerPrompt(
"check_network_status",
{
description: "Check current network health and conditions",
argsSchema: {
network: z.string().optional().describe("Network name (default: ethereum)")
}
},
({ network = "ethereum" }) => ({
messages: [{
role: "user",
content: {
type: "text",
text: `# Network Status Check
**Objective**: Assess health and current conditions of ${network}
## Status Assessment
### 1. Gather Current Data
Call these read-only tools:
- \`get_chain_info\` for chain ID and current block number
- \`get_latest_block\` for block details and timing
- \`get_gas_price\` for current gas prices
### 2. Network Health Analysis
**Block Production**:
- Current block number
- Block timing (normal ~12-15 sec for Ethereum)
- Consistent vs irregular blocks
- Any gaps or delays
**Gas Market**:
- Base fee level (in gwei)
- Priority fee level
- Gas price trend (up/down/stable)
- Congestion level
**Overall Status**:
- Operational: Yes/No
- Issues detected: Yes/No
- Performance: Normal/Degraded/Critical
### 3. Congestion Assessment
Evaluate:
- Current gas prices vs average
- Pending transaction count
- Memory pool size
- Are transactions backing up?
## Output Format
**Network Status Report: ${network}**
**Overall Status**
- Operational Status: [Online/Degraded/Offline]
- Current Block: [number]
- Network Time: [timestamp]
- Last Updated: [when]
**Performance Metrics**
- Block Time: [seconds] (normal: 12-15s)
- Gas Base Fee: [gwei]
- Priority Fee: [gwei]
- Total Cost for Standard Tx: [estimate USD]
**Congestion Level**
- Level: [Low/Moderate/High/Critical]
- Current vs Historical: [comparison]
- Trend: [increasing/stable/decreasing]
**Network Activity**
- Blocks per minute: [rate]
- Recent block details: [hash, time, tx count]
- Network security: [indicators]
**Recommendations**
For **sending transactions now**:
- Best for: [low-value / high-value / time-critical]
- Gas setting: [standard / fast / extreme]
- Estimated cost: [range]
- Estimated wait time: [minutes]
**If Congested**:
- Consider using: [alternative networks]
- Wait time: [estimated minutes]
- Cost to expedite: [gas increase needed]
**If Issues Detected**:
- Known issues: [list if any]
- Expected duration: [if known]
- Recommended action: [wait / use alternate / etc]
## Key Metrics
Reference points for interpretation:
- Ethereum normal block: 12-15 seconds
- Polygon normal: 2 seconds
- Arbitrum normal: <1 second
- Normal gas: 20-50 gwei
- High congestion: 100+ gwei
`
}
}]
})
);
}
================================================
FILE: src/core/resources.ts
================================================
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { getSupportedNetworks } from "./chains.js";
/**
* Register EVM-related resources with the MCP server
*
* Resources are application-driven, read-only data that clients can explicitly load.
* For an AI agent use case, most data should be exposed through tools instead,
* which allow the model to discover and autonomously fetch information.
*
* The supported_networks resource provides a static reference list that clients
* may want to browse when configuring which networks to use.
*
* @param server The MCP server instance
*/
export function registerEVMResources(server: McpServer) {
server.registerResource(
"supported_networks",
"evm://networks",
{ description: "Get list of all supported EVM networks and their configuration", mimeType: "application/json" },
async (uri) => {
try {
const networks = getSupportedNetworks();
return {
contents: [{
uri: uri.href,
text: JSON.stringify({ supportedNetworks: networks }, null, 2)
}]
};
} catch (error) {
return {
contents: [{
uri: uri.href,
text: `Error: ${error instanceof Error ? error.message : String(error)}`
}]
};
}
}
);
}
================================================
FILE: src/core/services/abi.ts
================================================
import { type Address } from 'viem';
import { resolveChainId, getSupportedNetworks } from '../chains.js';
/**
* Fetch contract ABI from Etherscan v2 API (unified endpoint for all EVM chains)
* Requires ETHERSCAN_API_KEY environment variable to be set
*
* @param contractAddress The contract address to fetch ABI for
* @param network The network name or chain ID
* @returns The contract ABI as a JSON string
*/
export async function fetchContractABI(
contractAddress: Address,
network: string = 'ethereum'
): Promise<string> {
const apiKey = process.env.ETHERSCAN_API_KEY;
if (!apiKey) {
throw new Error('ETHERSCAN_API_KEY environment variable is not set. Set it to fetch contract ABIs from block explorers.');
}
// Resolve chain ID using the chains.ts utilities
let chainId: number;
try {
chainId = resolveChainId(network);
} catch (error) {
const supported = getSupportedNetworks();
throw new Error(`Network "${network}" is not supported. Supported: ${supported.join(', ')}`);
}
try {
// Use unified Etherscan v2 API endpoint
const url = new URL('https://api.etherscan.io/v2/api');
url.searchParams.set('module', 'contract');
url.searchParams.set('action', 'getabi');
url.searchParams.set('address', contractAddress);
url.searchParams.set('chainid', chainId.toString());
url.searchParams.set('apikey', apiKey);
const response = await fetch(url.toString());
const data = await response.json() as any;
if (data.status === '0') {
throw new Error(data.result || 'Failed to fetch ABI from block explorer');
}
if (!data.result) {
throw new Error('No ABI found for this contract. Contract might not be verified.');
}
return data.result;
} catch (error) {
if (error instanceof Error) {
throw new Error(`Failed to fetch ABI: ${error.message}`);
}
throw error;
}
}
/**
* Parse and validate an ABI JSON string
* @param abiJson The ABI as a JSON string
* @returns Parsed ABI array
*/
export function parseABI(abiJson: string): any[] {
try {
const abi = JSON.parse(abiJson);
if (!Array.isArray(abi)) {
throw new Error('ABI must be a JSON array');
}
return abi;
} catch (error) {
throw new Error(`Invalid ABI JSON: ${error instanceof Error ? error.message : String(error)}`);
}
}
/**
* Get list of readable functions from an ABI
* @param abi The contract ABI
* @returns Array of read-only function names
*/
export function getReadableFunctions(abi: any[]): string[] {
return abi
.filter(item =>
item.type === 'function' &&
(item.stateMutability === 'view' || item.stateMutability === 'pure')
)
.map(item => item.name)
.filter(Boolean);
}
/**
* Get a specific function from an ABI
* @param abi The contract ABI
* @param functionName The function name to find
* @returns The function ABI object
*/
export function getFunctionFromABI(abi: any[], functionName: string): any {
const fn = abi.find(item =>
item.type === 'function' && item.name === functionName
);
if (!fn) {
throw new Error(`Function "${functionName}" not found in ABI`);
}
return fn;
}
================================================
FILE: src/core/services/balance.ts
================================================
import {
formatEther,
formatUnits,
type Address,
type Abi,
getContract
} from 'viem';
import { getPublicClient } from './clients.js';
import { readContract } from './contracts.js';
import { resolveAddress } from './ens.js';
// Standard ERC20 ABI (minimal for reading)
const erc20Abi = [
{
inputs: [],
name: 'symbol',
outputs: [{ type: 'string' }],
stateMutability: 'view',
type: 'function'
},
{
inputs: [],
name: 'decimals',
outputs: [{ type: 'uint8' }],
stateMutability: 'view',
type: 'function'
},
{
inputs: [{ type: 'address', name: 'account' }],
name: 'balanceOf',
outputs: [{ type: 'uint256' }],
stateMutability: 'view',
type: 'function'
}
] as const;
// Standard ERC721 ABI (minimal for reading)
const erc721Abi = [
{
inputs: [{ type: 'address', name: 'owner' }],
name: 'balanceOf',
outputs: [{ type: 'uint256' }],
stateMutability: 'view',
type: 'function'
},
{
inputs: [{ type: 'uint256', name: 'tokenId' }],
name: 'ownerOf',
outputs: [{ type: 'address' }],
stateMutability: 'view',
type: 'function'
}
] as const;
// Standard ERC1155 ABI (minimal for reading)
const erc1155Abi = [
{
inputs: [
{ type: 'address', name: 'account' },
{ type: 'uint256', name: 'id' }
],
name: 'balanceOf',
outputs: [{ type: 'uint256' }],
stateMutability: 'view',
type: 'function'
}
] as const;
/**
* Get the ETH balance for an address
* @param addressOrEns Ethereum address or ENS name
* @param network Network name or chain ID
* @returns Balance in wei and ether
*/
export async function getETHBalance(
addressOrEns: string,
network = 'ethereum'
): Promise<{ wei: bigint; ether: string }> {
// Resolve ENS name to address if needed
const address = await resolveAddress(addressOrEns, network);
const client = getPublicClient(network);
const balance = await client.getBalance({ address });
return {
wei: balance,
ether: formatEther(balance)
};
}
/**
* Get the balance of an ERC20 token for an address
* @param tokenAddressOrEns Token contract address or ENS name
* @param ownerAddressOrEns Owner address or ENS name
* @param network Network name or chain ID
* @returns Token balance with formatting information
*/
export async function getERC20Balance(
tokenAddressOrEns: string,
ownerAddressOrEns: string,
network = 'ethereum'
): Promise<{
raw: bigint;
formatted: string;
token: {
symbol: string;
decimals: number;
}
}> {
// Resolve ENS names to addresses if needed
const tokenAddress = await resolveAddress(tokenAddressOrEns, network);
const ownerAddress = await resolveAddress(ownerAddressOrEns, network);
const publicClient = getPublicClient(network);
const contract = getContract({
address: tokenAddress,
abi: erc20Abi,
client: publicClient,
});
const [balance, symbol, decimals] = await Promise.all([
contract.read.balanceOf([ownerAddress]),
contract.read.symbol(),
contract.read.decimals()
]);
return {
raw: balance,
formatted: formatUnits(balance, decimals),
token: {
symbol,
decimals
}
};
}
/**
* Check if an address owns a specific NFT
* @param tokenAddressOrEns NFT contract address or ENS name
* @param ownerAddressOrEns Owner address or ENS name
* @param tokenId Token ID to check
* @param network Network name or chain ID
* @returns True if the address owns the NFT
*/
export async function isNFTOwner(
tokenAddressOrEns: string,
ownerAddressOrEns: string,
tokenId: bigint,
network = 'ethereum'
): Promise<boolean> {
// Resolve ENS names to addresses if needed
const tokenAddress = await resolveAddress(tokenAddressOrEns, network);
const ownerAddress = await resolveAddress(ownerAddressOrEns, network);
try {
const actualOwner = await readContract({
address: tokenAddress,
abi: erc721Abi,
functionName: 'ownerOf',
args: [tokenId]
}, network) as Address;
return actualOwner.toLowerCase() === ownerAddress.toLowerCase();
} catch (error: any) {
console.error(`Error checking NFT ownership: ${error.message}`);
return false;
}
}
/**
* Get the number of NFTs owned by an address for a specific collection
* @param tokenAddressOrEns NFT contract address or ENS name
* @param ownerAddressOrEns Owner address or ENS name
* @param network Network name or chain ID
* @returns Number of NFTs owned
*/
export async function getERC721Balance(
tokenAddressOrEns: string,
ownerAddressOrEns: string,
network = 'ethereum'
): Promise<bigint> {
// Resolve ENS names to addresses if needed
const tokenAddress = await resolveAddress(tokenAddressOrEns, network);
const ownerAddress = await resolveAddress(ownerAddressOrEns, network);
return readContract({
address: tokenAddress,
abi: erc721Abi,
functionName: 'balanceOf',
args: [ownerAddress]
}, network) as Promise<bigint>;
}
/**
* Get the balance of an ERC1155 token for an address
* @param tokenAddressOrEns ERC1155 contract address or ENS name
* @param ownerAddressOrEns Owner address or ENS name
* @param tokenId Token ID to check
* @param network Network name or chain ID
* @returns Token balance
*/
export async function getERC1155Balance(
tokenAddressOrEns: string,
ownerAddressOrEns: string,
tokenId: bigint,
network = 'ethereum'
): Promise<bigint> {
// Resolve ENS names to addresses if needed
const tokenAddress = await resolveAddress(tokenAddressOrEns, network);
const ownerAddress = await resolveAddress(ownerAddressOrEns, network);
return readContract({
address: tokenAddress,
abi: erc1155Abi,
functionName: 'balanceOf',
args: [ownerAddress, tokenId]
}, network) as Promise<bigint>;
}
================================================
FILE: src/core/services/blocks.ts
================================================
import {
type Hash,
type Block
} from 'viem';
import { getPublicClient } from './clients.js';
/**
* Get the current block number for a specific network
*/
export async function getBlockNumber(network = 'ethereum'): Promise<bigint> {
const client = getPublicClient(network);
return await client.getBlockNumber();
}
/**
* Get a block by number for a specific network
*/
export async function getBlockByNumber(
blockNumber: number,
network = 'ethereum'
): Promise<Block> {
const client = getPublicClient(network);
return await client.getBlock({ blockNumber: BigInt(blockNumber) });
}
/**
* Get a block by hash for a specific network
*/
export async function getBlockByHash(
blockHash: Hash,
network = 'ethereum'
): Promise<Block> {
const client = getPublicClient(network);
return await client.getBlock({ blockHash });
}
/**
* Get the latest block for a specific network
*/
export async function getLatestBlock(network = 'ethereum'): Promise<Block> {
const client = getPublicClient(network);
return await client.getBlock();
}
================================================
FILE: src/core/services/clients.ts
================================================
import {
createPublicClient,
createWalletClient,
http,
type PublicClient,
type WalletClient,
type Hex,
type Address
} from 'viem';
import { privateKeyToAccount } from 'viem/accounts';
import { getChain, getRpcUrl } from '../chains.js';
// Cache for clients to avoid recreating them for each request
const clientCache = new Map<string, PublicClient>();
/**
* Get a public client for a specific network
*/
export function getPublicClient(network = 'ethereum'): PublicClient {
const cacheKey = String(network);
// Return cached client if available
if (clientCache.has(cacheKey)) {
return clientCache.get(cacheKey)!;
}
// Create a new client
const chain = getChain(network);
const rpcUrl = getRpcUrl(network);
const client = createPublicClient({
chain,
transport: http(rpcUrl)
});
// Cache the client
clientCache.set(cacheKey, client);
return client;
}
/**
* Create a wallet client for a specific network and private key
*/
export function getWalletClient(privateKey: Hex, network = 'ethereum'): WalletClient {
const chain = getChain(network);
const rpcUrl = getRpcUrl(network);
const account = privateKeyToAccount(privateKey);
return createWalletClient({
account,
chain,
transport: http(rpcUrl)
});
}
/**
* Get an Ethereum address from a private key
* @param privateKey The private key in hex format (with or without 0x prefix)
* @returns The Ethereum address derived from the private key
*/
export function getAddressFromPrivateKey(privateKey: Hex): Address {
const account = privateKeyToAccount(privateKey);
return account.address;
}
================================================
FILE: src/core/services/contracts.ts
================================================
import {
type Address,
type Hash,
type Hex,
type ReadContractParameters,
type GetLogsParameters,
type Log
} from 'viem';
import { getPublicClient, getWalletClient } from './clients.js';
import { resolveAddress } from './ens.js';
/**
* Read from a contract for a specific network
*/
export async function readContract(params: ReadContractParameters, network = 'ethereum') {
const client = getPublicClient(network);
return await client.readContract(params);
}
/**
* Write to a contract for a specific network
*/
export async function writeContract(
privateKey: Hex,
params: Record<string, any>,
network = 'ethereum'
): Promise<Hash> {
const client = getWalletClient(privateKey, network);
return await client.writeContract(params as any);
}
/**
* Get logs for a specific network
*/
export async function getLogs(params: GetLogsParameters, network = 'ethereum'): Promise<Log[]> {
const client = getPublicClient(network);
return await client.getLogs(params);
}
/**
* Check if an address is a contract
* @param addressOrEns Address or ENS name to check
* @param network Network name or chain ID
* @returns True if the address is a contract, false if it's an EOA
*/
export async function isContract(addressOrEns: string, network = 'ethereum'): Promise<boolean> {
// Resolve ENS name to address if needed
const address = await resolveAddress(addressOrEns, network);
const client = getPublicClient(network);
const code = await client.getBytecode({ address });
return code !== undefined && code !== '0x';
}
/**
* Batch multiple contract read calls into a single RPC request using Multicall3
* @param contracts Array of contract calls to batch
* @param allowFailure If true, returns partial results even if some calls fail
* @param network Network name or chain ID
* @returns Array of results with status
*/
export async function multicall(
contracts: Array<{
address: Address;
abi: any[];
functionName: string;
args?: any[];
}>,
allowFailure = true,
network = 'ethereum'
): Promise<any> {
const client = getPublicClient(network);
return await client.multicall({
contracts: contracts as any,
allowFailure
});
}
================================================
FILE: src/core/services/ens.ts
================================================
import { normalize } from 'viem/ens';
import { getPublicClient } from './clients.js';
import { type Address } from 'viem';
/**
* Resolves an ENS name to an Ethereum address or returns the original address if it's already valid
* @param addressOrEns An Ethereum address or ENS name
* @param network The network to use for ENS resolution (defaults to Ethereum mainnet)
* @returns The resolved Ethereum address
*/
export async function resolveAddress(
addressOrEns: string,
network = 'ethereum'
): Promise<Address> {
// If it's already a valid Ethereum address (0x followed by 40 hex chars), return it
if (/^0x[a-fA-F0-9]{40}$/.test(addressOrEns)) {
return addressOrEns as Address;
}
// If it looks like an ENS name (contains a dot), try to resolve it
if (addressOrEns.includes('.')) {
try {
// Normalize the ENS name first
const normalizedEns = normalize(addressOrEns);
// Get the public client for the network
const publicClient = getPublicClient(network);
// Resolve the ENS name to an address
const address = await publicClient.getEnsAddress({
name: normalizedEns,
});
if (!address) {
throw new Error(`ENS name ${addressOrEns} could not be resolved to an address`);
}
return address;
} catch (error: any) {
throw new Error(`Failed to resolve ENS name ${addressOrEns}: ${error.message}`);
}
}
// If it's neither a valid address nor an ENS name, throw an error
throw new Error(`Invalid address or ENS name: ${addressOrEns}`);
}
================================================
FILE: src/core/services/index.ts
================================================
// Export all services
export * from './clients.js';
export * from './balance.js';
export * from './transfer.js';
export * from './blocks.js';
export * from './transactions.js';
export * from './contracts.js';
export * from './tokens.js';
export * from './ens.js';
export * from './abi.js';
export * from './wallet.js';
export { utils as helpers } from './utils.js';
// Re-export common types for convenience
export type {
Address,
Hash,
Hex,
Block,
TransactionReceipt,
Log
} from 'viem';
================================================
FILE: src/core/services/tokens.ts
================================================
import {
type Address,
type Hex,
type Hash,
formatUnits,
getContract
} from 'viem';
import { getPublicClient } from './clients.js';
// Standard ERC20 ABI (minimal for reading)
const erc20Abi = [
{
inputs: [],
name: 'name',
outputs: [{ type: 'string' }],
stateMutability: 'view',
type: 'function'
},
{
inputs: [],
name: 'symbol',
outputs: [{ type: 'string' }],
stateMutability: 'view',
type: 'function'
},
{
inputs: [],
name: 'decimals',
outputs: [{ type: 'uint8' }],
stateMutability: 'view',
type: 'function'
},
{
inputs: [],
name: 'totalSupply',
outputs: [{ type: 'uint256' }],
stateMutability: 'view',
type: 'function'
}
] as const;
// Standard ERC721 ABI (minimal for reading)
const erc721Abi = [
{
inputs: [],
name: 'name',
outputs: [{ type: 'string' }],
stateMutability: 'view',
type: 'function'
},
{
inputs: [],
name: 'symbol',
outputs: [{ type: 'string' }],
stateMutability: 'view',
type: 'function'
},
{
inputs: [{ type: 'uint256', name: 'tokenId' }],
name: 'tokenURI',
outputs: [{ type: 'string' }],
stateMutability: 'view',
type: 'function'
}
] as const;
// Standard ERC1155 ABI (minimal for reading)
const erc1155Abi = [
{
inputs: [{ type: 'uint256', name: 'id' }],
name: 'uri',
outputs: [{ type: 'string' }],
stateMutability: 'view',
type: 'function'
}
] as const;
/**
* Get ERC20 token information
*/
export async function getERC20TokenInfo(
tokenAddress: Address,
network: string = 'ethereum'
): Promise<{
name: string;
symbol: string;
decimals: number;
totalSupply: bigint;
formattedTotalSupply: string;
}> {
const publicClient = getPublicClient(network);
const contract = getContract({
address: tokenAddress,
abi: erc20Abi,
client: publicClient,
});
const [name, symbol, decimals, totalSupply] = await Promise.all([
contract.read.name(),
contract.read.symbol(),
contract.read.decimals(),
contract.read.totalSupply()
]);
return {
name,
symbol,
decimals,
totalSupply,
formattedTotalSupply: formatUnits(totalSupply, decimals)
};
}
/**
* Get ERC721 token metadata
*/
export async function getERC721TokenMetadata(
tokenAddress: Address,
tokenId: bigint,
network: string = 'ethereum'
): Promise<{
name: string;
symbol: string;
tokenURI: string;
}> {
const publicClient = getPublicClient(network);
const contract = getContract({
address: tokenAddress,
abi: erc721Abi,
client: publicClient,
});
const [name, symbol, tokenURI] = await Promise.all([
contract.read.name(),
contract.read.symbol(),
contract.read.tokenURI([tokenId])
]);
return {
name,
symbol,
tokenURI
};
}
/**
* Get ERC1155 token URI
*/
export async function getERC1155TokenURI(
tokenAddress: Address,
tokenId: bigint,
network: string = 'ethereum'
): Promise<string> {
const publicClient = getPublicClient(network);
const contract = getContract({
address: tokenAddress,
abi: erc1155Abi,
client: publicClient,
});
return contract.read.uri([tokenId]);
}
================================================
FILE: src/core/services/transactions.ts
================================================
import {
type Address,
type Hash,
type TransactionReceipt,
type EstimateGasParameters
} from 'viem';
import { getPublicClient } from './clients.js';
/**
* Get a transaction by hash for a specific network
*/
export async function getTransaction(hash: Hash, network = 'ethereum') {
const client = getPublicClient(network);
return await client.getTransaction({ hash });
}
/**
* Get a transaction receipt by hash for a specific network
*/
export async function getTransactionReceipt(hash: Hash, network = 'ethereum'): Promise<TransactionReceipt> {
const client = getPublicClient(network);
return await client.getTransactionReceipt({ hash });
}
/**
* Get the transaction count for an address for a specific network
*/
export async function getTransactionCount(address: Address, network = 'ethereum'): Promise<number> {
const client = getPublicClient(network);
const count = await client.getTransactionCount({ address });
return Number(count);
}
/**
* Estimate gas for a transaction for a specific network
*/
export async function estimateGas(params: EstimateGasParameters, network = 'ethereum'): Promise<bigint> {
const client = getPublicClient(network);
return await client.estimateGas(params);
}
/**
* Get the chain ID for a specific network
*/
export async function getChainId(network = 'ethereum'): Promise<number> {
const client = getPublicClient(network);
const chainId = await client.getChainId();
return Number(chainId);
}
================================================
FILE: src/core/services/transfer.ts
================================================
import {
parseEther,
parseUnits,
formatUnits,
type Address,
type Hash,
type Hex,
type Abi,
getContract,
type Account
} from 'viem';
import { getPublicClient, getWalletClient } from './clients.js';
import { getChain } from '../chains.js';
import { resolveAddress } from './ens.js';
// Standard ERC20 ABI for transfers
const erc20TransferAbi = [
{
inputs: [
{ type: 'address', name: 'to' },
{ type: 'uint256', name: 'amount' }
],
name: 'transfer',
outputs: [{ type: 'bool' }],
stateMutability: 'nonpayable',
type: 'function'
},
{
inputs: [
{ type: 'address', name: 'spender' },
{ type: 'uint256', name: 'amount' }
],
name: 'approve',
outputs: [{ type: 'bool' }],
stateMutability: 'nonpayable',
type: 'function'
},
{
inputs: [],
name: 'decimals',
outputs: [{ type: 'uint8' }],
stateMutability: 'view',
type: 'function'
},
{
inputs: [],
name: 'symbol',
outputs: [{ type: 'string' }],
stateMutability: 'view',
type: 'function'
}
] as const;
// Standard ERC721 ABI for transfers
const erc721TransferAbi = [
{
inputs: [
{ type: 'address', name: 'from' },
{ type: 'address', name: 'to' },
{ type: 'uint256', name: 'tokenId' }
],
name: 'transferFrom',
outputs: [],
stateMutability: 'nonpayable',
type: 'function'
},
{
inputs: [],
name: 'name',
outputs: [{ type: 'string' }],
stateMutability: 'view',
type: 'function'
},
{
inputs: [],
name: 'symbol',
outputs: [{ type: 'string' }],
stateMutability: 'view',
type: 'function'
},
{
inputs: [{ type: 'uint256', name: 'tokenId' }],
name: 'ownerOf',
outputs: [{ type: 'address' }],
stateMutability: 'view',
type: 'function'
}
] as const;
// ERC1155 ABI for transfers
const erc1155TransferAbi = [
{
inputs: [
{ type: 'address', name: 'from' },
{ type: 'address', name: 'to' },
{ type: 'uint256', name: 'id' },
{ type: 'uint256', name: 'amount' },
{ type: 'bytes', name: 'data' }
],
name: 'safeTransferFrom',
outputs: [],
stateMutability: 'nonpayable',
type: 'function'
},
{
inputs: [
{ type: 'address', name: 'account' },
{ type: 'uint256', name: 'id' }
],
name: 'balanceOf',
outputs: [{ type: 'uint256' }],
stateMutability: 'view',
type: 'function'
}
] as const;
/**
* Transfer ETH to an address
* @param privateKey Sender's private key
* @param toAddressOrEns Recipient address or ENS name
* @param amount Amount to send in ETH
* @param network Network name or chain ID
* @returns Transaction hash
*/
export async function transferETH(
privateKey: string | Hex,
toAddressOrEns: string,
amount: string, // in ether
network = 'ethereum'
): Promise<Hash> {
// Resolve ENS name to address if needed
const toAddress = await resolveAddress(toAddressOrEns, network);
// Ensure the private key has 0x prefix
const formattedKey = typeof privateKey === 'string' && !privateKey.startsWith('0x')
? `0x${privateKey}` as Hex
: privateKey as Hex;
const client = getWalletClient(formattedKey, network);
const amountWei = parseEther(amount);
return client.sendTransaction({
to: toAddress,
value: amountWei,
account: client.account!,
chain: client.chain
});
}
/**
* Transfer ERC20 tokens to an address
* @param tokenAddressOrEns Token contract address or ENS name
* @param toAddressOrEns Recipient address or ENS name
* @param amount Amount to send (in token units)
* @param privateKey Sender's private key
* @param network Network name or chain ID
* @returns Transaction details
*/
export async function transferERC20(
tokenAddressOrEns: string,
toAddressOrEns: string,
amount: string,
privateKey: string | `0x${string}`,
network: string = 'ethereum'
): Promise<{
txHash: Hash;
amount: {
raw: bigint;
formatted: string;
};
token: {
symbol: string;
decimals: number;
};
}> {
// Resolve ENS names to addresses if needed
const tokenAddress = await resolveAddress(tokenAddressOrEns, network) as Address;
const toAddress = await resolveAddress(toAddressOrEns, network) as Address;
// Ensure the private key has 0x prefix
const formattedKey = typeof privateKey === 'string' && !privateKey.startsWith('0x')
? `0x${privateKey}` as `0x${string}`
: privateKey as `0x${string}`;
// Get token details
const publicClient = getPublicClient(network);
const contract = getContract({
address: tokenAddress,
abi: erc20TransferAbi,
client: publicClient,
});
// Get token decimals and symbol
const decimals = await contract.read.decimals();
const symbol = await contract.read.symbol();
// Parse the amount with the correct number of decimals
const rawAmount = parseUnits(amount, decimals);
// Create wallet client for sending the transaction
const walletClient = getWalletClient(formattedKey, network);
// Send the transaction
const hash = await walletClient.writeContract({
address: tokenAddress,
abi: erc20TransferAbi,
functionName: 'transfer',
args: [toAddress, rawAmount],
account: walletClient.account!,
chain: walletClient.chain
});
return {
txHash: hash,
amount: {
raw: rawAmount,
formatted: amount
},
token: {
symbol,
decimals
}
};
}
/**
* Approve ERC20 token spending
* @param tokenAddressOrEns Token contract address or ENS name
* @param spenderAddressOrEns Spender address or ENS name
* @param amount Amount to approve (in token units)
* @param privateKey Owner's private key
* @param network Network name or chain ID
* @returns Transaction details
*/
export async function approveERC20(
tokenAddressOrEns: string,
spenderAddressOrEns: string,
amount: string,
privateKey: string | `0x${string}`,
network: string = 'ethereum'
): Promise<{
txHash: Hash;
amount: {
raw: bigint;
formatted: string;
};
token: {
symbol: string;
decimals: number;
};
}> {
// Resolve ENS names to addresses if needed
const tokenAddress = await resolveAddress(tokenAddressOrEns, network) as Address;
const spenderAddress = await resolveAddress(spenderAddressOrEns, network) as Address;
// Ensure the private key has 0x prefix
const formattedKey = typeof privateKey === 'string' && !privateKey.startsWith('0x')
? `0x${privateKey}` as `0x${string}`
: privateKey as `0x${string}`;
// Get token details
const publicClient = getPublicClient(network);
const contract = getContract({
address: tokenAddress,
abi: erc20TransferAbi,
client: publicClient,
});
// Get token decimals and symbol
const decimals = await contract.read.decimals();
const symbol = await contract.read.symbol();
// Parse the amount with the correct number of decimals
const rawAmount = parseUnits(amount, decimals);
// Create wallet client for sending the transaction
const walletClient = getWalletClient(formattedKey, network);
// Send the transaction
const hash = await walletClient.writeContract({
address: tokenAddress,
abi: erc20TransferAbi,
functionName: 'approve',
args: [spenderAddress, rawAmount],
account: walletClient.account!,
chain: walletClient.chain
});
return {
txHash: hash,
amount: {
raw: rawAmount,
formatted: amount
},
token: {
symbol,
decimals
}
};
}
/**
* Transfer an NFT (ERC721) to an address
* @param tokenAddressOrEns NFT contract address or ENS name
* @param toAddressOrEns Recipient address or ENS name
* @param tokenId Token ID to transfer
* @param privateKey Owner's private key
* @param network Network name or chain ID
* @returns Transaction details
*/
export async function transferERC721(
tokenAddressOrEns: string,
toAddressOrEns: string,
tokenId: bigint,
privateKey: string | `0x${string}`,
network: string = 'ethereum'
): Promise<{
txHash: Hash;
tokenId: string;
token: {
name: string;
symbol: string;
};
}> {
// Resolve ENS names to addresses if needed
const tokenAddress = await resolveAddress(tokenAddressOrEns, network) as Address;
const toAddress = await resolveAddress(toAddressOrEns, network) as Address;
// Ensure the private key has 0x prefix
const formattedKey = typeof privateKey === 'string' && !privateKey.startsWith('0x')
? `0x${privateKey}` as `0x${string}`
: privateKey as `0x${string}`;
// Create wallet client for sending the transaction
const walletClient = getWalletClient(formattedKey, network);
const fromAddress = walletClient.account!.address;
// Send the transaction
const hash = await walletClient.writeContract({
address: tokenAddress,
abi: erc721TransferAbi,
functionName: 'transferFrom',
args: [fromAddress, toAddress, tokenId],
account: walletClient.account!,
chain: walletClient.chain
});
// Get token metadata
const publicClient = getPublicClient(network);
const contract = getContract({
address: tokenAddress,
abi: erc721TransferAbi,
client: publicClient,
});
// Get token name and symbol
let name = 'Unknown';
let symbol = 'NFT';
try {
[name, symbol] = await Promise.all([
contract.read.name(),
contract.read.symbol()
]);
} catch (error) {
console.error('Error fetching NFT metadata:', error);
}
return {
txHash: hash,
tokenId: tokenId.toString(),
token: {
name,
symbol
}
};
}
/**
* Transfer ERC1155 tokens to an address
* @param tokenAddressOrEns Token contract address or ENS name
* @param toAddressOrEns Recipient address or ENS name
* @param tokenId Token ID to transfer
* @param amount Amount to transfer
* @param privateKey Owner's private key
* @param network Network name or chain ID
* @returns Transaction details
*/
export async function transferERC1155(
tokenAddressOrEns: string,
toAddressOrEns: string,
tokenId: bigint,
amount: string,
privateKey: string | `0x${string}`,
network: string = 'ethereum'
): Promise<{
txHash: Hash;
tokenId: string;
amount: string;
}> {
// Resolve ENS names to addresses if needed
const tokenAddress = await resolveAddress(tokenAddressOrEns, network) as Address;
const toAddress = await resolveAddress(toAddressOrEns, network) as Address;
// Ensure the private key has 0x prefix
const formattedKey = typeof privateKey === 'string' && !privateKey.startsWith('0x')
? `0x${privateKey}` as `0x${string}`
: privateKey as `0x${string}`;
// Create wallet client for sending the transaction
const walletClient = getWalletClient(formattedKey, network);
const fromAddress = walletClient.account!.address;
// Parse amount to bigint
const amountBigInt = BigInt(amount);
// Send the transaction
const hash = await walletClient.writeContract({
address: tokenAddress,
abi: erc1155TransferAbi,
functionName: 'safeTransferFrom',
args: [fromAddress, toAddress, tokenId, amountBigInt, '0x'],
account: walletClient.account!,
chain: walletClient.chain
});
return {
txHash: hash,
tokenId: tokenId.toString(),
amount
};
}
================================================
FILE: src/core/services/utils.ts
================================================
import {
parseEther,
formatEther,
type Account,
type Hash,
type Chain,
type WalletClient,
type Transport,
type HttpTransport
} from 'viem';
/**
* Utility functions for formatting and parsing values
*/
export const utils = {
// Convert ether to wei
parseEther,
// Convert wei to ether
formatEther,
// Format a bigint to a string
formatBigInt: (value: bigint): string => value.toString(),
// Format an object to JSON with bigint handling
formatJson: (obj: unknown): string => JSON.stringify(obj, (_, value) =>
typeof value === 'bigint' ? value.toString() : value, 2),
// Format a number with commas
formatNumber: (value: number | string): string => {
return Number(value).toLocaleString();
},
// Convert a hex string to a number
hexToNumber: (hex: string): number => {
return parseInt(hex, 16);
},
// Convert a number to a hex string
numberToHex: (num: number): string => {
return '0x' + num.toString(16);
}
};
================================================
FILE: src/core/services/wallet.ts
================================================
import { type Address, type Hex } from 'viem';
import { privateKeyToAccount, mnemonicToAccount, type HDAccount, type PrivateKeyAccount } from 'viem/accounts';
/**
* Get the configured account from environment (private key or mnemonic)
*
* Configuration options:
* - EVM_PRIVATE_KEY: Hex private key (with or without 0x prefix)
* - EVM_MNEMONIC: BIP-39 mnemonic phrase (12 or 24 words)
* - EVM_ACCOUNT_INDEX: Optional account index for HD wallet derivation (default: 0)
*/
export const getConfiguredAccount = (): HDAccount | PrivateKeyAccount => {
const privateKey = process.env.EVM_PRIVATE_KEY;
const mnemonic = process.env.EVM_MNEMONIC;
const accountIndexStr = process.env.EVM_ACCOUNT_INDEX || '0';
const accountIndex = parseInt(accountIndexStr, 10);
// Validate account index
if (isNaN(accountIndex) || accountIndex < 0 || !Number.isInteger(accountIndex)) {
throw new Error(
`Invalid EVM_ACCOUNT_INDEX: "${accountIndexStr}". Must be a non-negative integer.`
);
}
if (privateKey) {
// Use private key if provided
const key = (privateKey.startsWith('0x') ? privateKey : `0x${privateKey}`) as Hex;
return privateKeyToAccount(key);
} else if (mnemonic) {
// Use mnemonic if provided
return mnemonicToAccount(mnemonic, { accountIndex });
} else {
throw new Error(
"Neither EVM_PRIVATE_KEY nor EVM_MNEMONIC environment variable is set. " +
"Configure one of them to enable write operations.\n" +
"- EVM_PRIVATE_KEY: Your private key in hex format\n" +
"- EVM_MNEMONIC: Your 12 or 24 word mnemonic phrase\n" +
"- EVM_ACCOUNT_INDEX: (Optional) Account index for HD wallet (default: 0)"
);
}
};
/**
* Helper to get the configured private key (for services that need it)
*
* For HDAccount (from mnemonic): extracts private key from HD key
* For PrivateKeyAccount: returns the original private key
*/
export const getConfiguredPrivateKey = (): Hex => {
const account = getConfiguredAccount();
// Check if this is an HDAccount (has getHdKey method)
if ('getHdKey' in account && typeof account.getHdKey === 'function') {
const hdKey = account.getHdKey();
if (!hdKey.privateKey) {
throw new Error("Unable to derive private key from HD account - no private key in HD key");
}
// Convert Uint8Array to hex string (compatible with Bun and Node)
const privateKeyHex = Array.from(hdKey.privateKey)
.map(byte => byte.toString(16).padStart(2, '0'))
.join('');
return `0x${privateKeyHex}` as Hex;
}
// For PrivateKeyAccount, re-read from environment since we created from it
if ('source' in account && account.source === 'privateKey') {
const privateKey = process.env.EVM_PRIVATE_KEY;
if (privateKey) {
return (privateKey.startsWith('0x') ? privateKey : `0x${privateKey}`) as Hex;
}
}
throw new Error("Unable to extract private key from account");
};
/**
* Helper to get wallet address
*/
export const getWalletAddressFromKey = (): Address => {
const account = getConfiguredAccount();
return account.address;
};
/**
* Helper to get configured wallet object
*/
export const getConfiguredWallet = (): { address: Address } => {
return { address: getWalletAddressFromKey() };
};
/**
* Sign an arbitrary message using the configured wallet
* @param message The message to sign (can be a string or hex data)
* @returns The signature as a hex string
*/
export const signMessage = async (message: string): Promise<string> => {
const account = getConfiguredAccount();
// Use the account's signMessage method directly
const signature = await account.signMessage({
message: message
});
return signature;
};
/**
* Sign typed data (EIP-712) using the configured wallet
* @param domain The EIP-712 domain
* @param types The types definition (excluding EIP712Domain)
* @param primaryType The primary type name
* @param message The message data to sign
* @returns The signature as a hex string
*/
export const signTypedData = async (
domain: {
name?: string;
version?: string;
chainId?: number;
verifyingContract?: Address;
salt?: `0x${string}`;
},
types: Record<string, Array<{ name: string; type: string }>>,
primaryType: string,
message: Record<string, any>
): Promise<string> => {
const account = getConfiguredAccount();
// Use the account's signTypedData method
const signature = await account.signTypedData({
domain,
types,
primaryType,
message
});
return signature;
};
================================================
FILE: src/core/tools.ts
================================================
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
import { getSupportedNetworks, getRpcUrl } from "./chains.js";
import * as services from "./services/index.js";
import { type Address, type Hex, type Hash } from 'viem';
import { normalize } from 'viem/ens';
/**
* Register all EVM-related tools with the MCP server
*
* SECURITY: Either EVM_PRIVATE_KEY or EVM_MNEMONIC environment variable must be set for write operations.
* Private keys and mnemonics are never passed as tool arguments for security reasons.
* Tools will use the configured wallet for all transactions.
*
* Configuration options:
* - EVM_PRIVATE_KEY: Hex private key (with or without 0x prefix)
* - EVM_MNEMONIC: BIP-39 mnemonic phrase (12 or 24 words)
* - EVM_ACCOUNT_INDEX: Optional account index for HD wallet derivation (default: 0)
*
* All tools that accept addresses also support ENS names (e.g., 'vitalik.eth').
* ENS names are automatically resolved to addresses using the Ethereum Name Service.
*
* @param server The MCP server instance
*/
export function registerEVMTools(server: McpServer) {
// Helpers are now imported from services/wallet.ts
const { getConfiguredPrivateKey, getWalletAddressFromKey, getConfiguredWallet } = services;
// ============================================================================
// WALLET INFORMATION TOOLS (Read-only)
// ============================================================================
server.registerTool(
"get_wallet_address",
{
description: "Get the address of the configured wallet. Use this to verify which wallet is active.",
inputSchema: {},
annotations: {
title: "Get Wallet Address",
readOnlyHint: true,
destructiveHint: false,
idempotentHint: true,
openWorldHint: false
}
},
async () => {
try {
const address = getWalletAddressFromKey();
return {
content: [{
type: "text",
text: JSON.stringify({
address,
message: "This is the wallet that will be used for all transactions"
}, null, 2)
}]
};
} catch (error) {
return {
content: [{ type: "text", text: `Error: ${error instanceof Error ? error.message : String(error)}` }],
isError: true
};
}
}
);
// ============================================================================
// NETWORK INFORMATION TOOLS (Read-only)
// ============================================================================
server.registerTool(
"get_chain_info",
{
description: "Get information about an EVM network: chain ID, current block number, and RPC endpoint",
inputSchema: {
network: z.string().optional().describe("Network name (e.g., 'ethereum', 'optimism', 'arbitrum', 'base') or chain ID. Defaults to Ethereum mainnet.")
},
annotations: {
title: "Get Chain Info",
readOnlyHint: true,
destructiveHint: false,
idempotentHint: true,
openWorldHint: true
}
},
async ({ network = "ethereum" }) => {
try {
const chainId = await services.getChainId(network);
const blockNumber = await services.getBlockNumber(network);
const rpcUrl = getRpcUrl(network);
return {
content: [{
type: "text",
text: JSON.stringify({ network, chainId, blockNumber: blockNumber.toString(), rpcUrl }, null, 2)
}]
};
} catch (error) {
return {
content: [{ type: "text", text: `Error fetching chain info: ${error instanceof Error ? error.message : String(error)}` }],
isError: true
};
}
}
);
server.registerTool(
"get_supported_networks",
{
description: "Get a list of all supported EVM networks",
inputSchema: {},
annotations: {
title: "Get Supported Networks",
readOnlyHint: true,
destructiveHint: false,
idempotentHint: true,
openWorldHint: false
}
},
async () => {
try {
const networks = getSupportedNetworks();
return {
content: [{ type: "text", text: JSON.stringify({ supportedNetworks: networks }, null, 2) }]
};
} catch (error) {
return {
content: [{ type: "text", text: `Error: ${error instanceof Error ? error.message : String(error)}` }],
isError: true
};
}
}
);
server.registerTool(
"get_gas_price",
{
description: "Get current gas prices (base fee, standard, and fast) for a network",
inputSchema: {
network: z.string().optional().describe("Network name or chain ID. Defaults to Ethereum mainnet.")
},
annotations: {
title: "Get Gas Prices",
readOnlyHint: true,
destructiveHint: false,
idempotentHint: false,
openWorldHint: true
}
},
async ({ network = "ethereum" }) => {
try {
const client = await services.getPublicClient(network);
const [baseFee, priorityFee] = await Promise.all([
client.getGasPrice(),
client.estimateMaxPriorityFeePerGas()
]);
return {
content: [{
type: "text",
text: JSON.stringify({
network,
baseFeePerGas: baseFee.toString(),
priorityFeePerGas: priorityFee?.toString() || "N/A",
currency: "wei"
}, null, 2)
}]
};
} catch (error) {
return {
content: [{ type: "text", text: `Error fetching gas prices: ${error instanceof Error ? error.message : String(error)}` }],
isError: true
};
}
}
);
// ============================================================================
// ENS TOOLS (Read-only)
// ============================================================================
server.registerTool(
"resolve_ens_name",
{
description: "Resolve an ENS name to an Ethereum address",
inputSchema: {
ensName: z.string().describe("ENS name to resolve (e.g., 'vitalik.eth')"),
network: z.string().optional().describe("Network name or chain ID. ENS resolution works best on Ethereum mainnet. Defaults to Ethereum mainnet.")
},
annotations: {
title: "Resolve ENS Name",
readOnlyHint: true,
destructiveHint: false,
idempotentHint: true,
openWorldHint: true
}
},
async ({ ensName, network = "ethereum" }) => {
try {
if (!ensName.includes('.')) {
return {
content: [{ type: "text", text: `Error: "${ensName}" is not a valid ENS name. ENS names must contain a dot (e.g., 'name.eth').` }],
isError: true
};
}
const normalizedEns = normalize(ensName);
const address = await services.resolveAddress(ensName, network);
return {
content: [{
type: "text",
text: JSON.stringify({
ensName,
normalizedName: normalizedEns,
resolvedAddress: address,
network
}, null, 2)
}]
};
} catch (error) {
return {
content: [{ type: "text", text: `Error resolving ENS name: ${error instanceof Error ? error.message : String(error)}` }],
isError: true
};
}
}
);
server.registerTool(
"lookup_ens_address",
{
description: "Lookup the ENS name for an Ethereum address (reverse resolution)",
inputSchema: {
address: z.string().describe("Ethereum address to lookup"),
network: z.string().optional().describe("Network name or chain ID. Defaults to Ethereum mainnet.")
},
annotations: {
title: "Lookup ENS Address",
readOnlyHint: true,
destructiveHint: false,
idempotentHint: true,
openWorldHint: true
}
},
async ({ address, network = "ethereum" }) => {
try {
const client = await services.getPublicClient(network);
const ensName = await client.getEnsName({
address: address as Address
});
return {
content: [{
type: "text",
text: JSON.stringify({
address,
ensName: ensName || "No ENS name found",
network
}, null, 2)
}]
};
} catch (error) {
return {
content: [{ type: "text", text: `Error looking up ENS name: ${error instanceof Error ? error.message : String(error)}` }],
isError: true
};
}
}
);
// ============================================================================
// BLOCK TOOLS (Read-only)
// ============================================================================
server.registerTool(
"get_block",
{
description: "Get block details by block number or hash",
inputSchema: {
blockIdentifier: z.string().describe("Block number (as string) or block hash"),
network: z.string().optional().describe("Network name or chain ID. Defaults to Ethereum mainnet.")
},
annotations: {
title: "Get Block",
readOnlyHint: true,
destructiveHint: false,
idempotentHint: true,
openWorldHint: true
}
},
async ({ blockIdentifier, network = "ethereum" }) => {
try {
let block;
if (blockIdentifier.startsWith("0x") && blockIdentifier.length === 66) {
// It's a hash
block = await services.getBlockByHash(blockIdentifier as Hash, network);
} else {
// It's a number
block = await services.getBlockByNumber(parseInt(blockIdentifier), network);
}
return { content: [{ type: "text", text: services.helpers.formatJson(block) }] };
} catch (error) {
return {
content: [{ type: "text", text: `Error fetching block: ${error instanceof Error ? error.message : String(error)}` }],
isError: true
};
}
}
);
server.registerTool(
"get_latest_block",
{
description: "Get the latest block from the network",
inputSchema: {
network: z.string().optional().describe("Network name or chain ID. Defaults to Ethereum mainnet.")
},
annotations: {
title: "Get Latest Block",
readOnlyHint: true,
destructiveHint: false,
idempotentHint: false,
openWorldHint: true
}
},
async ({ network = "ethereum" }) => {
try {
const block = await services.getLatestBlock(network);
return { content: [{ type: "text", text: services.helpers.formatJson(block) }] };
} catch (error) {
return {
content: [{ type: "text", text: `Error fetching latest block: ${error instanceof Error ? error.message : String(error)}` }],
isError: true
};
}
}
);
// ============================================================================
// BALANCE TOOLS (Read-only)
// ============================================================================
server.registerTool(
"get_balance",
{
description: "Get the native token balance (ETH, MATIC, etc.) for an address",
inputSchema: {
address: z.string().describe("The wallet address or ENS name"),
network: z.string().optional().describe("Network name or chain ID. Defaults to Ethereum mainnet.")
},
annotations: {
title: "Get Native Token Balance",
readOnlyHint: true,
destructiveHint: false,
idempotentHint: true,
openWorldHint: true
}
},
async ({ address, network = "ethereum" }) => {
try {
const balance = await services.getETHBalance(address as Address, network);
return {
content: [{
type: "text",
text: JSON.stringify({
network,
address,
balance: { wei: balance.wei.toString(), ether: balance.ether }
}, null, 2)
}]
};
} catch (error) {
return {
content: [{ type: "text", text: `Error fetching balance: ${error instanceof Error ? error.message : String(error)}` }],
isError: true
};
}
}
);
server.registerTool(
"get_token_balance",
{
description: "Get the ERC20 token balance for an address",
inputSchema: {
address: z.string().describe("The wallet address or ENS name"),
tokenAddress: z.string().describe("The ERC20 token contract address"),
network: z.string().optional().describe("Network name or chain ID. Defaults to Ethereum mainnet.")
},
annotations: {
title: "Get ERC20 Token Balance",
readOnlyHint: true,
destructiveHint: false,
idempotentHint: true,
openWorldHint: true
}
},
async ({ address, tokenAddress, network = "ethereum" }) => {
try {
const balance = await services.getERC20Balance(tokenAddress as Address, address as Address, network);
return {
content: [{
type: "text",
text: JSON.stringify({
network,
tokenAddress,
address,
balance: {
raw: balance.raw.toString(),
formatted: balance.formatted,
symbol: balance.token.symbol,
decimals: balance.token.decimals
}
}, null, 2)
}]
};
} catch (error) {
return {
content: [{ type: "text", text: `Error fetching token balance: ${error instanceof Error ? error.message : String(error)}` }],
isError: true
};
}
}
);
server.registerTool(
"get_allowance",
{
description: "Check the allowance granted to a spender for a token. This tells you how much of a token an address can spend on your behalf.",
inputSchema: {
tokenAddress: z.string().describe("The ERC20 token contract address"),
spenderAddress: z.string().describe("The address allowed to spend the token (usually a contract address)"),
ownerAddress: z.string().optional().describe("The owner address (defaults to the configured wallet)"),
network: z.string().optional().describe("Network name or chain ID. Defaults to Ethereum mainnet.")
},
annotations: {
title: "Get Token Allowance",
readOnlyHint: true,
destructiveHint: false,
idempotentHint: true,
openWorldHint: true
}
},
async ({ tokenAddress, spenderAddress, ownerAddress, network = "ethereum" }) => {
try {
const owner = ownerAddress ? (ownerAddress as Address) : getConfiguredWallet().address;
const client = await services.getPublicClient(network);
const allowance = await client.readContract({
address: tokenAddress as Address,
abi: [
{
name: 'allowance',
type: 'function',
inputs: [
{ name: 'owner', type: 'address' },
{ name: 'spender', type: 'address' }
],
outputs: [{ name: '', type: 'uint256' }],
stateMutability: 'view'
}
],
functionName: 'allowance',
args: [owner, spenderAddress as Address]
});
return {
content: [{
type: "text",
text: JSON.stringify({
network,
tokenAddress,
owner,
spenderAddress,
allowance: allowance.toString(),
message: allowance === 0n ? "No allowance set" : "Allowance is set"
}, null, 2)
}]
};
} catch (error) {
return {
content: [{ type: "text", text: `Error fetching allowance: ${error instanceof Error ? error.message : String(error)}` }],
isError: true
};
}
}
);
// ============================================================================
// TRANSACTION TOOLS (Read-only)
// ============================================================================
server.registerTool(
"get_transaction",
{
description: "Get transaction details by transaction hash",
inputSchema: {
txHash: z.string().describe("Transaction hash (0x...)"),
network: z.string().optional().describe("Network name or chain ID. Defaults to Ethereum mainnet.")
},
annotations: {
title: "Get Transaction",
readOnlyHint: true,
destructiveHint: false,
idempotentHint: true,
openWorldHint: true
}
},
async ({ txHash, network = "ethereum" }) => {
try {
const tx = await services.getTransaction(txHash as Hash, network);
return { content: [{ type: "text", text: services.helpers.formatJson(tx) }] };
} catch (error) {
return {
content: [{ type: "text", text: `Error fetching transaction: ${error instanceof Error ? error.message : String(error)}` }],
isError: true
};
}
}
);
server.registerTool(
"get_transaction_receipt",
{
description: "Get transaction receipt (confirmation status, gas used, logs). Use this to check if a transaction has been confirmed.",
inputSchema: {
txHash: z.string().describe("Transaction hash (0x...)"),
network: z.string().optional().describe("Network name or chain ID. Defaults to Ethereum mainnet.")
},
annotations: {
title: "Get Transaction Receipt",
readOnlyHint: true,
destructiveHint: false,
idempotentHint: true,
openWorldHint: true
}
},
async ({ txHash, network = "ethereum" }) => {
try {
const client = await services.getPublicClient(network);
const receipt = await client.getTransactionReceipt({
hash: txHash as Hash
});
return { content: [{ type: "text", text: services.helpers.formatJson(receipt) }] };
} catch (error) {
return {
content: [{ type: "text", text: `Error fetching transaction receipt: ${error instanceof Error ? error.message : String(error)}` }],
isError: true
};
}
}
);
server.registerTool(
"wait_for_transaction",
{
description: "Wait for a transaction to be confirmed (mined). Polls the network until confirmation.",
inputSchema: {
txHash: z.string().describe("Transaction hash (0x...)"),
confirmations: z.number().optional().describe("Number of block confirmations required. Defaults to 1."),
network: z.string().optional().describe("Network name or chain ID. Defaults to Ethereum mainnet.")
},
annotations: {
title: "Wait For Transaction",
readOnlyHint: true,
destructiveHint: false,
idempotentHint: false,
openWorldHint: true
}
},
async ({ txHash, confirmations = 1, network = "ethereum" }) => {
try {
const client = await services.getPublicClient(network);
const receipt = await client.waitForTransactionReceipt({
hash: txHash as Hash,
confirmations
});
return {
content: [{
type: "text",
text: JSON.stringify({
network,
txHash,
status: receipt.status === 'success' ? 'confirmed' : 'failed',
blockNumber: receipt.blockNumber.toString(),
gasUsed: receipt.gasUsed.toString(),
confirmations
}, null, 2)
}]
};
} catch (error) {
return {
content: [{ type: "text", text: `Error waiting for transaction: ${error instanceof Error ? error.message : String(error)}` }],
isError: true
};
}
}
);
// ============================================================================
// SMART CONTRACT TOOLS
// ============================================================================
server.registerTool(
"get_contract_abi",
{
description: "Fetch a contract's full ABI from Etherscan/block explorers. Use this to understand verified contracts before interacting. Requires ETHERSCAN_API_KEY. Supports 30+ EVM networks. Works best with verified contracts on block explorers.",
inputSchema: {
contractAddress: z.string().describe("The contract address (0x...)"),
network: z.string().optional().describe("Network name or chain ID. Defaults to ethereum. Supported: ethereum, polygon, arbitrum, optimism, base, avalanche, gnosis, fantom, bsc, celo, scroll, linea, zksync, manta, blast, and testnets (sepolia, mumbai, arbitrum-sepolia, optimism-sepolia, base-sepolia, avalanche-fuji)")
},
annotations: {
title: "Get Contract ABI",
readOnlyHint: true,
destructiveHint: false,
idempotentHint: true,
openWorldHint: true
}
},
async ({ contractAddress, network = "ethereum" }) => {
try {
const abi = await services.fetchContractABI(contractAddress as Address, network);
const parsed = services.parseABI(abi);
const readableFunctions = services.getReadableFunctions(parsed);
return {
content: [{
type: "text",
text: JSON.stringify({
contractAddress,
network,
abiFormat: "json",
readableFunctions,
totalFunctions: parsed.filter(i => i.type === 'function').length,
abi: parsed
}, null, 2)
}]
};
} catch (error) {
return {
content: [{ type: "text", text: `Error fetching ABI: ${error instanceof Error ? error.message : String(error)}` }],
isError: true
};
}
}
);
server.registerTool(
"read_contract",
{
description: "Call read-only functions on a smart contract. Automatically fetches ABI from block explorer if not provided (requires ETHERSCAN_API_KEY). Falls back to common functions if contract is not verified. Use this to query contract state and data.",
inputSchema: {
contractAddress: z.string().describe("The contract address"),
functionName: z.string().describe("Function name (e.g., 'name', 'symbol', 'balanceOf', 'totalSupply', 'owner')"),
args: z.array(z.string()).optional().describe("Function arguments as strings (e.g., ['0xAddress'] for balanceOf)"),
abiJson: z.string().optional().describe("Full contract ABI as JSON string (optional - will auto-fetch verified contract ABI if not provided)"),
network: z.string().optional().describe("Network name or chain ID. Defaults to Ethereum mainnet.")
},
annotations: {
title: "Read Smart Contract",
readOnlyHint: true,
destructiveHint: false,
idempotentHint: true,
openWorldHint: true
}
},
async ({ contractAddress, functionName, args = [], abiJson, network = "ethereum" }) => {
try {
const client = await services.getPublicClient(network);
let abi: any[] | undefined;
let functionAbi: any;
// If ABI is provided, use it
if (abiJson) {
try {
abi = services.parseABI(abiJson);
functionAbi = services.getFunctionFromABI(abi, functionName);
} catch (error) {
return {
content: [{
type: "text",
text: `Error parsing provided ABI: ${error instanceof Error ? error.message : String(error)}`
}],
isError: true
};
}
} else {
// Try to auto-fetch ABI from block explorer
try {
const fetchedAbi = await services.fetchContractABI(contractAddress as Address, network);
abi = services.parseABI(fetchedAbi);
functionAbi = services.getFunctionFromABI(abi, functionName);
} catch (fetchError) {
// Fall back to common function signatures
const commonFunctions: { [key: string]: any } = {
'name': { inputs: [], outputs: [{ type: 'string' }] },
'symbol': { inputs: [], outputs: [{ type: 'string' }] },
'decimals': { inputs: [], outputs: [{ type: 'uint8' }] },
'totalSupply': { inputs: [], outputs: [{ type: 'uint256' }] },
'balanceOf': { inputs: [{ type: 'address' }], outputs: [{ type: 'uint256' }] },
'allowance': { inputs: [{ type: 'address' }, { type: 'address' }], outputs: [{ type: 'uint256' }] },
};
if (!commonFunctions[functionName]) {
return {
content: [{
type: "text",
text: `Error: Could not auto-fetch ABI (${fetchError instanceof Error ? fetchError.message : String(fetchError)}). Function '${functionName}' not in common signatures. Use get_contract_abi to fetch and provide the full ABI, or provide abiJson parameter.`
}],
isError: true
};
}
functionAbi = {
name: functionName,
type: 'function',
inputs: commonFunctions[functionName].inputs,
outputs: commonFunctions[functionName].outputs,
stateMutability: 'view'
};
}
}
const result = await client.readContract({
address: contractAddress as Address,
abi: [functionAbi],
functionName: functionName,
args: args as any
});
return {
content: [{
type: "text",
text: JSON.stringify({
contractAddress,
function: functionName,
args: args.length > 0 ? args : undefined,
result: result?.toString(),
abiSource: abiJson ? 'provided' : 'auto-fetched or built-in'
}, null, 2)
}]
};
} catch (error) {
return {
content: [{ type: "text", text: `Error reading contract: ${error instanceof Error ? error.message : String(error)}` }],
isError: true
};
}
}
);
server.registerTool(
"write_contract",
{
description: "Execute state-changing functions on a smart contract. Automatically fetches ABI from block explorer if not provided (requires ETHERSCAN_API_KEY). Use this to call any write function on verified contracts. Requires wallet to be configured (via private key or mnemonic).",
inputSchema: {
contractAddress: z.string().describe("The contract address"),
functionName: z.string().describe("Function name to call (e.g., 'mint', 'swap', 'stake', 'approve')"),
args: z.array(z.string()).optional().describe("Function arguments as strings (e.g., ['0xAddress', '1000000'])"),
value: z.string().optional().describe("ETH value to send with transaction in ether (e.g., '0.1' for payable functions)"),
abiJson: z.string().optional().describe("Full contract ABI as JSON string (optional - will auto-fetch verified contract ABI if not provided)"),
network: z.string().optional().describe("Network name or chain ID. Defaults to Ethereum mainnet.")
},
annotations: {
title: "Write to Smart Contract",
readOnlyHint: false,
destructiveHint: true,
idempotentHint: false,
openWorldHint: true
}
},
async ({ contractAddress, functionName, args = [], value, abiJson, network = "ethereum" }) => {
try {
const privateKey = getConfiguredPrivateKey();
const senderAddress = getWalletAddressFromKey();
const client = await services.getPublicClient(network);
let abi: any[] | undefined;
let functionAbi: any;
// If ABI is provided, use it
if (abiJson) {
try {
abi = services.parseABI(abiJson);
functionAbi = services.getFunctionFromABI(abi, functionName);
} catch (error) {
return {
content: [{
type: "text",
text: `Error parsing provided ABI: ${error instanceof Error ? error.message : String(error)}`
}],
isError: true
};
}
} else {
// Try to auto-fetch ABI from block explorer
try {
const fetchedAbi = await services.fetchContractABI(contractAddress as Address, network);
abi = services.parseABI(fetchedAbi);
functionAbi = services.getFunctionFromABI(abi, functionName);
} catch (fetchError) {
return {
content: [{
type: "text",
text: `Error: Could not auto-fetch ABI (${fetchError instanceof Error ? fetchError.message : String(fetchError)}). Please provide the contract ABI using the abiJson parameter, or use get_contract_abi to fetch it first.`
}],
isError: true
};
}
}
// Validate that this is not a view/pure function
if (functionAbi.stateMutability === 'view' || functionAbi.stateMutability === 'pure') {
return {
content: [{
type: "text",
text: `Error: Function '${functionName}' is a ${functionAbi.stateMutability} function and cannot modify state. Use read_contract instead.`
}],
isError: true
};
}
// Prepare write parameters
const writeParams: any = {
address: contractAddress as Address,
abi: [functionAbi],
functionName: functionName,
args: args as any
};
// Add value if provided (for payable functions)
if (value) {
const { parseEther } = await import('viem');
writeParams.value = parseEther(value);
}
// Execute the write operation
const txHash = await services.writeContract(privateKey, writeParams, network);
return {
content: [{
type: "text",
text: JSON.stringify({
network,
contractAddress,
function: functionName,
args: args.length > 0 ? args : undefined,
value: value || undefined,
from: senderAddress,
txHash,
abiSource: abiJson ? 'provided' : 'auto-fetched',
message: "Transaction sent. Use get_transaction_receipt or wait_for_transaction to check confirmation."
}, null, 2)
}]
};
} catch (error) {
return {
content: [{ type: "text", text: `Error writing to contract: ${error instanceof Error ? error.message : String(error)}` }],
isError: true
};
}
}
);
server.registerTool(
"multicall",
{
description: "Batch multiple contract read calls into a single RPC request. Significantly reduces latency and RPC usage when querying multiple functions. Uses the Multicall3 contract deployed on all major networks. Perfect for portfolio analysis, price aggregation, and querying multiple contract states efficiently.",
inputSchema: {
calls: z.array(z.object({
contractAddress: z.string().describe("The contract address"),
functionName: z.string().describe("Function name to call"),
args: z.array(z.string()).optional().describe("Function arguments as strings"),
abiJson: z.string().optional().describe("Contract ABI as JSON string (optional - will auto-fetch if not provided)")
})).describe("Array of contract calls to batch together"),
allowFailure: z.boolean().optional().describe("If true, returns partial results even if some calls fail. Defaults to true."),
network: z.string().optional().describe("Network name or chain ID. Defaults to Ethereum mainnet.")
},
annotations: {
title: "Multicall (Batch Read)",
readOnlyHint: true,
destructiveHint: false,
idempotentHint: true,
openWorldHint: true
}
},
async ({ calls, allowFailure = true, network = "ethereum" }) => {
try {
// Build contracts array with ABIs
const contractsWithAbis = await Promise.all(
calls.map(async (call) => {
let abi: any[];
let functionAbi: any;
// If ABI is provided, use it
if (call.abiJson) {
try {
abi = services.parseABI(call.abiJson);
functionAbi = services.getFunctionFromABI(abi, call.functionName);
} catch (error) {
throw new Error(`Error parsing ABI for ${call.contractAddress}: ${error instanceof Error ? error.message : String(error)}`);
}
} else {
// Try to auto-fetch ABI
try {
const fetchedAbi = await services.fetchContractABI(call.contractAddress as Address, network);
abi = services.parseABI(fetchedAbi);
functionAbi = services.getFunctionFromABI(abi, call.functionName);
} catch (fetchError) {
// Fall back to common function signatures
const commonFunctions: { [key: string]: any } = {
'name': { inputs: [], outputs: [{ type: 'string' }], stateMutability: 'view' },
'symbol': { inputs: [], outputs: [{ type: 'string' }], stateMutability: 'view' },
'decimals': { inputs: [], outputs: [{ type: 'uint8' }], stateMutability: 'view' },
'totalSupply': { inputs: [], outputs: [{ type: 'uint256' }], stateMutability: 'view' },
'balanceOf': { inputs: [{ type: 'address' }], outputs: [{ type: 'uint256' }], stateMutability: 'view' },
'allowance': { inputs: [{ type: 'address' }, { type: 'address' }], outputs: [{ type: 'uint256' }], stateMutability: 'view' },
};
if (!commonFunctions[call.functionName]) {
throw new Error(`Could not auto-fetch ABI for ${call.contractAddress}. Function '${call.functionName}' not in common signatures. Please provide abiJson parameter.`);
}
functionAbi = {
name: call.functionName,
type: 'function',
inputs: commonFunctions[call.functionName].inputs,
outputs: commonFunctions[call.functionName].outputs,
stateMutability: 'view'
};
}
}
return {
address: call.contractAddress as Address,
abi: [functionAbi],
functionName: call.functionName,
args: call.args || []
};
})
);
// Execute multicall
const results = await services.multicall(contractsWithAbis, allowFailure, network);
// Format results
const formattedResults = results.map((result: any, index: number) => {
const call = calls[index];
if (result.status === 'success') {
return {
contractAddress: call.contractAddress,
functionName: call.functionName,
args: call.args,
result: result.result?.toString(),
status: 'success'
};
} else {
return {
contractAddress: call.contractAddress,
functionName: call.functionName,
args: call.args,
error: result.error?.message || 'Unknown error',
status: 'failure'
};
}
});
return {
content: [{
type: "text",
text: JSON.stringify({
network,
totalCalls: calls.length,
successfulCalls: formattedResults.filter((r: any) => r.status === 'success').length,
failedCalls: formattedResults.filter((r: any) => r.status === 'failure').length,
results: formattedResults
}, null, 2)
}]
};
} catch (error) {
return {
content: [{ type: "text", text: `Error executing multicall: ${error instanceof Error ? error.message : String(error)}` }],
isError: true
};
}
}
);
// ============================================================================
// TRANSFER TOOLS (Write operations)
// ============================================================================
server.registerTool(
"transfer_native",
{
description: "Transfer native tokens (ETH, MATIC, etc.) to an address. Uses the configured wallet.",
inputSchema: {
to: z.string().describe("Recipient address or ENS name"),
amount: z.string().describe("Amount to send in ether (e.g., '0.5' for 0.5 ETH)"),
network: z.string().optional().describe("Network name or chain ID. Defaults to Ethereum mainnet.")
},
annotations: {
title: "Transfer Native Tokens",
readOnlyHint: false,
destructiveHint: true,
idempotentHint: false,
openWorldHint: true
}
},
async ({ to, amount, network = "ethereum" }) => {
try {
const privateKey = getConfiguredPrivateKey();
const senderAddress = getWalletAddressFromKey();
const txHash = await services.transferETH(privateKey, to as Address, amount, network);
return {
content: [{
type: "text",
text: JSON.stringify({
network,
from: senderAddress,
to,
amount,
txHash,
message: "Transaction sent. Use get_transaction_receipt to check confirmation."
}, null, 2)
}]
};
} catch (error) {
return {
content: [{ type: "text", text: `Error transferring native tokens: ${error instanceof Error ? error.message : String(error)}` }],
isError: true
};
}
}
);
server.registerTool(
"transfer_erc20",
{
description: "Transfer ERC20 tokens to an address. Uses the configured wallet.",
inputSchema: {
tokenAddress: z.string().describe("The ERC20 token contract address"),
to: z.string().describe("Recipient address or ENS name"),
amount: z.string().describe("Amount to send (in token units, accounting for decimals)"),
network: z.string().optional().describe("Network name or chain ID. Defaults to Ethereum mainnet.")
},
annotations: {
title: "Transfer ERC20 Tokens",
readOnlyHint: false,
destructiveHint: true,
idempotentHint: false,
openWorldHint: true
}
},
async ({ tokenAddress, to, amount, network = "ethereum" }) => {
try {
const privateKey = getConfiguredPrivateKey();
const senderAddress = getWalletAddressFromKey();
const result = await services.transferERC20(tokenAddress as Address, to as Address, amount, privateKey, network);
return {
content: [{
type: "text",
text: JSON.stringify({
network,
tokenAddress,
from: senderAddress,
to,
amount: result.amount.formatted,
symbol: result.token.symbol,
decimals: result.token.decimals,
txHash: result.txHash,
message: "Transaction sent. Use get_transaction_receipt to check confirmation."
}, null, 2)
}]
};
} catch (error) {
return {
content: [{ type: "text", text: `Error transferring ERC20 tokens: ${error instanceof Error ? error.message : String(error)}` }],
isError: true
};
}
}
);
server.registerTool(
"approve_token_spending",
{
description: "Approve a spender (contract) to spend tokens on your behalf. Required before interacting with DEXes, lending protocols, etc.",
inputSchema: {
tokenAddress: z.string().describe("The ERC20 token contract address"),
spenderAddress: z.string().describe("The address that will be allowed to spend tokens (usually a contract)"),
amount: z.string().describe("Amount to approve (in token units). Use '0' to revoke approval."),
network: z.string().optional().describe("Network name or chain ID. Defaults to Ethereum mainnet.")
},
annotations: {
title: "Approve Token Spending",
readOnlyHint: false,
destructiveHint: false,
idempotentHint: false,
openWorldHint: true
}
},
async ({ tokenAddress, spenderAddress, amount, network = "ethereum" }) => {
try {
const privateKey = getConfiguredPrivateKey();
const senderAddress = getWalletAddressFromKey();
const txHash = await services.approveERC20(tokenAddress as Address, spenderAddress as Address, amount, privateKey, network);
return {
content: [{
type: "text",
text: JSON.stringify({
network,
tokenAddress,
owner: senderAddress,
spender: spenderAddress,
approvalAmount: amount,
txHash,
message: "Approval transaction sent. Use get_transaction_receipt to check confirmation."
}, null, 2)
}]
};
} catch (error) {
return {
content: [{ type: "text", text: `Error approving token spending: ${error instanceof Error ? error.message : String(error)}` }],
isError: true
};
}
}
);
// ============================================================================
// NFT TOOLS (Read-only)
// ============================================================================
server.registerTool(
"get_nft_info",
{
description: "Get information about an ERC721 NFT including metadata URI",
inputSchema: {
contractAddress: z.string().describe("The NFT contract address"),
tokenId: z.string().describe("The NFT token ID"),
network: z.string().optional().describe("Network name or chain ID. Defaults to Ethereum mainnet.")
},
annotations: {
title: "Get NFT Info",
readOnlyHint: true,
destructiveHint: false,
idempotentHint: true,
openWorldHint: true
}
},
async ({ contractAddress, tokenId, network = "ethereum" }) => {
try {
const nftInfo = await services.getERC721TokenMetadata(contractAddress as Address, BigInt(tokenId), network);
return {
content: [{
type: "text",
text: JSON.stringify({
network,
contract: contractAddress,
tokenId,
...nftInfo
}, null, 2)
}]
};
} catch (error) {
return {
content: [{ type: "text", text: `Error fetching NFT info: ${error instanceof Error ? error.message : String(error)}` }],
isError: true
};
}
}
);
server.registerTool(
"get_erc1155_balance",
{
description: "Get ERC1155 token balance for an address",
inputSchema: {
contractAddress: z.string().describe("The ERC1155 contract address"),
tokenId: z.string().describe("The token ID"),
address: z.string().describe("The owner address or ENS name"),
network: z.string().optional().describe("Network name or chain ID. Defaults to Ethereum mainnet.")
},
annotations: {
title: "Get ERC1155 Balance",
readOnlyHint: true,
destructiveHint: false,
idempotentHint: true,
openWorldHint: true
}
},
async ({ contractAddress, tokenId, address, network = "ethereum" }) => {
try {
const balance = await services.getERC1155Balance(contractAddress as Address, address as Address, BigInt(tokenId), network);
return {
content: [{
type: "text",
text: JSON.stringify({
network,
contract: contractAddress,
tokenId,
owner: address,
balance: balance.toString()
}, null, 2)
}]
};
} catch (error) {
return {
content: [{ type: "text", text: `Error fetching ERC1155 balance: ${error instanceof Error ? error.message : String(error)}` }],
isError: true
};
}
}
);
// ============================================================================
// MESSAGE SIGNING TOOLS (Write operations)
// ============================================================================
server.registerTool(
"sign_message",
{
description: "Sign an arbitrary message using the configured wallet. Useful for authentication (SIWE), meta-transactions, and off-chain signatures. The signature can be verified on-chain or off-chain.",
inputSchema: {
message: z.string().describe("The message to sign (plain text or hex-encoded data)")
},
annotations: {
title: "Sign Message",
readOnlyHint: false,
destructiveHint: false,
idempotentHint: true,
openWorldHint: false
}
},
async ({ message }) => {
try {
const senderAddress = getWalletAddressFromKey();
const signature = await services.signMessage(message);
return {
content: [{
type: "text",
text: JSON.stringify({
message,
signature,
signer: senderAddress,
messageType: "personal_sign"
}, null, 2)
}]
};
} catch (error) {
return {
content: [{ type: "text", text: `Error signing message: ${error instanceof Error ? error.message : String(error)}` }],
isError: true
};
}
}
);
server.registerTool(
"sign_typed_data",
{
description: "Sign structured data (EIP-712) using the configured wallet. Used for gasless transactions, meta-transactions, permit signatures, and protocol-specific signatures. The signature follows the EIP-712 standard.",
inputSchema: {
domainJson: z.string().describe("EIP-712 domain as JSON string with fields: name, version, chainId, verifyingContract, salt (all optional)"),
typesJson: z.string().describe("EIP-712 types definition as JSON string (exclude EIP712Domain type - it's added automatically)"),
primaryType: z.string().describe("The primary type name (e.g., 'Mail', 'Permit', 'MetaTransaction')"),
messageJson: z.string().describe("The message data to sign as JSON string")
},
annotations: {
title: "Sign Typed Data (EIP-712)",
readOnlyHint: false,
destructiveHint: false,
idempotentHint: true,
openWorldHint: false
}
},
async ({ domainJson, typesJson, primaryType, messageJson }) => {
try {
const senderAddress = getWalletAddressFromKey();
// Parse JSON inputs
let domain, types, message;
try {
domain = JSON.parse(domainJson);
types = JSON.parse(typesJson);
message = JSON.parse(messageJson);
} catch (parseError) {
return {
content: [{
type: "text",
text: `Error parsing JSON inputs: ${parseError instanceof Error ? parseError.message : String(parseError)}`
}],
isError: true
};
}
const signature = await services.signTypedData(domain, types, primaryType, message);
return {
content: [{
type: "text",
text: JSON.stringify({
domain,
types,
primaryType,
message,
signature,
signer: senderAddress,
messageType: "EIP-712"
}, null, 2)
}]
};
} catch (error) {
return {
content: [{ type: "text", text: `Error signing typed data: ${error instanceof Error ? error.message : String(error)}` }],
isError: true
};
}
}
);
}
================================================
FILE: src/index.ts
================================================
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import startServer from "./server/server.js";
// Start the server
async function main() {
try {
const server = await startServer();
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("EVM MCP Server running on stdio");
} catch (error) {
console.error("Error starting MCP server:", error);
process.exit(1);
}
}
main().catch((error) => {
console.error("Fatal error in main():", error);
process.exit(1);
});
================================================
FILE: src/server/http-server.ts
================================================
import { randomUUID } from "node:crypto";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import startServer from "./server.js";
import express, { Request, Response } from "express";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
// Environment variables
const PORT = parseInt(process.env.MCP_PORT || "3001", 10);
const HOST = process.env.MCP_HOST || "0.0.0.0";
console.error(`Configured to listen on ${HOST}:${PORT}`);
// Setup Express
const app = express();
app.use(express.json({ limit: '10mb' })); // Prevent DoS attacks with huge payloads
// Track active transports by session ID with cleanup
const transports = new Map<string, StreamableHTTPServerTransport>();
const sessionTimestamps = new Map<string, number>();
const SESSION_TIMEOUT_MS = 30 * 60 * 1000; // 30 minutes
// Cleanup stale sessions periodically
setInterval(() => {
const now = Date.now();
for (const [sessionId, timestamp] of sessionTimestamps.entries()) {
if (now - timestamp > SESSION_TIMEOUT_MS) {
console.error(`Cleaning up stale session: ${sessionId}`);
const transport = transports.get(sessionId);
if (transport) {
transport.close().catch(err =>
console.error(`Error closing stale session ${sessionId}:`, err)
);
}
transports.delete(sessionId);
sessionTimestamps.delete(sessionId);
}
}
}, 5 * 60 * 1000); // Check every 5 minutes
// Initialize the MCP server
let server: McpServer | null = null;
startServer().then(s => {
server = s;
console.error("MCP Server initialized successfully");
}).catch(error => {
console.error("Failed to initialize server:", error);
process.exit(1);
});
// Handle all MCP requests through POST /mcp
app.post("/mcp", async (req: Request, res: Response) => {
console.error(`Received POST /mcp request from ${req.ip}`);
if (!server) {
console.error("Server not initialized yet");
res.status(503).json({ error: "Server not initialized" });
return;
}
// Check for existing session
const sessionId = req.headers["mcp-session-id"] as string | undefined;
let transport: StreamableHTTPServerTransport;
if (sessionId && transports.has(sessionId)) {
// Reuse existing transport for this session
transport = transports.get(sessionId)!;
sessionTimestamps.set(sessionId, Date.now()); // Update last activity
console.error(`Reusing transport for session: ${sessionId}`);
} else if (!sessionId) {
// New session - create transport with session ID generator
transport = new StreamableHTTPServerTransport({
sessionIdGenerator: () => randomUUID(),
onsessioninitialized: (newSessionId) => {
console.error(`Session initialized: ${newSessionId}`);
transports.set(newSessionId, transport);
sessionTimestamps.set(newSessionId, Date.now());
},
onsessionclosed: (closedSessionId) => {
console.error(`Session closed: ${closedSessionId}`);
transports.delete(closedSessionId);
sessionTimestamps.delete(closedSessionId);
}
});
// Connect the transport to the server
await server.connect(transport);
console.error("New transport connected to server");
} else {
// Invalid session ID provided
console.error(`Invalid session ID: ${sessionId}`);
res.status(404).json({ error: "Session not found" });
return;
}
// Handle the request
try {
await transport.handleRequest(req, res, req.body);
} catch (error) {
console.error(`Error handling request: ${error}`);
if (!res.headersSent) {
res.status(500).json({ error: `Internal server error: ${error}` });
}
}
});
// Handle GET requests for SSE streams (server-to-client notifications)
app.get("/mcp", async (req: Request, res: Response) => {
console.error(`Received GET /mcp request from ${req.ip}`);
if (!server) {
res.status(503).json({ error: "Server not initialized" });
return;
}
const sessionId = req.headers["mcp-session-id"] as string | undefined;
if (!sessionId || !transports.has(sessionId)) {
res.status(400).json({ error: "Invalid or missing session ID" });
return;
}
const transport = transports.get(sessionId)!;
try {
await transport.handleRequest(req, res);
} catch (error) {
console.error(`Error handling SSE request: ${error}`);
if (!res.headersSent) {
res.status(500).json({ error: `Internal server error: ${error}` });
}
}
});
// Handle DELETE requests to close sessions
app.delete("/mcp", async (req: Request, res: Response) => {
const sessionId = req.headers["mcp-session-id"] as string | undefined;
if (!sessionId || !transports.has(sessionId)) {
res.status(404).json({ error: "Session not found" });
return;
}
const transport = transports.get(sessionId)!;
try {
await transport.handleRequest(req, res);
} catch (error) {
console.error(`Error closing session: ${error}`);
if (!res.headersSent) {
res.status(500).json({ error: `Internal server error: ${error}` });
}
}
});
// Health check endpoint
app.get("/health", (_req: Request, res: Response) => {
res.status(200).json({
status: "ok",
server: server ? "initialized" : "initializing",
activeSessions: transports.size,
sessionIds: Array.from(transports.keys())
});
});
// Root endpoint for basic info
app.get("/", (_req: Request, res: Response) => {
res.status(200).json({
name: "EVM MCP Server",
version: "2.0.0",
protocol: "MCP 2025-06-18",
transport: "Streamable HTTP",
endpoints: {
mcp: "/mcp",
health: "/health"
},
status: server ? "ready" : "initializing",
activeSessions: transports.size
});
});
// Handle process termination gracefully
process.on('SIGINT', async () => {
console.error('Shutting down server...');
// Close all active transports
for (const [sessionId, transport] of transports) {
console.error(`Closing transport for session: ${sessionId}`);
await transport.close();
}
transports.clear();
process.exit(0);
});
process.on('SIGTERM', async () => {
console.error('Received SIGTERM, shutting down...');
for (const [sessionId, transport] of transports) {
console.error(`Closing transport for session: ${sessionId}`);
await transport.close();
}
transports.clear();
process.exit(0);
});
// Start the HTTP server
const httpServer = app.listen(PORT, HOST, () => {
console.error(`EVM MCP Server running at http://${HOST}:${PORT}`);
console.error(`MCP endpoint: http://${HOST}:${PORT}/mcp`);
console.error(`Health check: http://${HOST}:${PORT}/health`);
console.error(`Protocol: MCP 2025-06-18 (Streamable HTTP)`);
}).on('error', (err: Error) => {
console.error(`Server error: ${err}`);
process.exit(1);
});
// Set server timeout to prevent hanging connections
httpServer.timeout = 120000; // 2 minutes
httpServer.keepAliveTimeout = 65000; // 65 seconds
================================================
FILE: src/server/server.ts
================================================
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { registerEVMResources } from "../core/resources.js";
import { registerEVMTools } from "../core/tools.js";
import { registerEVMPrompts } from "../core/prompts.js";
import { getSupportedNetworks } from "../core/chains.js";
// Create and start the MCP server
async function startServer() {
try {
// Create a new MCP server instance with capabilities
const server = new McpServer(
{
name: "evm-mcp-server",
version: "2.0.0"
},
{
capabilities: {
tools: {
listChanged: true
},
resources: {
subscribe: false,
listChanged: true
},
prompts: {
listChanged: true
},
logging: {}
}
}
);
// Register all resources, tools, and prompts
registerEVMResources(server);
registerEVMTools(server);
registerEVMPrompts(server);
// Log server information
console.error(`EVM MCP Server v2.0.0 initialized`);
console.error(`Protocol: MCP 2025-06-18`);
console.error(`Supported networks: ${getSupportedNetworks().length} networks`);
console.error("Server is ready to handle requests");
return server;
} catch (error) {
console.error("Failed to initialize server:", error);
process.exit(1);
}
}
// Export the server creation function
export default startServer;
================================================
FILE: tsconfig.json
================================================
{
"compilerOptions": {
"target": "ES2022",
"lib": ["ES2022"],
"module": "NodeNext",
"moduleResolution": "NodeNext",
"esModuleInterop": true,
"strict": true,
"strictNullChecks": true,
"skipLibCheck": true,
"outDir": "dist",
"sourceMap": true,
"declaration": true,
"resolveJsonModule": true,
"forceConsistentCasingInFileNames": true,
"noEmit": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist", "build"]
}
gitextract_kze5jsqh/ ├── .cursor/ │ └── mcp.json ├── .github/ │ └── workflows/ │ └── release-publish.yml ├── .gitignore ├── .npmignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── bin/ │ └── cli.js ├── funding.json ├── package.json ├── src/ │ ├── core/ │ │ ├── chains.ts │ │ ├── prompts.ts │ │ ├── resources.ts │ │ ├── services/ │ │ │ ├── abi.ts │ │ │ ├── balance.ts │ │ │ ├── blocks.ts │ │ │ ├── clients.ts │ │ │ ├── contracts.ts │ │ │ ├── ens.ts │ │ │ ├── index.ts │ │ │ ├── tokens.ts │ │ │ ├── transactions.ts │ │ │ ├── transfer.ts │ │ │ ├── utils.ts │ │ │ └── wallet.ts │ │ └── tools.ts │ ├── index.ts │ └── server/ │ ├── http-server.ts │ └── server.ts └── tsconfig.json
SYMBOL INDEX (49 symbols across 16 files)
FILE: src/core/chains.ts
constant DEFAULT_RPC_URL (line 64) | const DEFAULT_RPC_URL = 'https://eth.llamarpc.com';
constant DEFAULT_CHAIN_ID (line 65) | const DEFAULT_CHAIN_ID = 1;
function resolveChainId (line 290) | function resolveChainId(chainIdentifier: number | string): number {
function getChain (line 320) | function getChain(chainIdentifier: number | string = DEFAULT_CHAIN_ID): ...
function getRpcUrl (line 341) | function getRpcUrl(chainIdentifier: number | string = DEFAULT_CHAIN_ID):...
function getSupportedNetworks (line 353) | function getSupportedNetworks(): string[] {
FILE: src/core/prompts.ts
function registerEVMPrompts (line 18) | function registerEVMPrompts(server: McpServer) {
FILE: src/core/resources.ts
function registerEVMResources (line 16) | function registerEVMResources(server: McpServer) {
FILE: src/core/services/abi.ts
function fetchContractABI (line 12) | async function fetchContractABI(
function parseABI (line 64) | function parseABI(abiJson: string): any[] {
function getReadableFunctions (line 81) | function getReadableFunctions(abi: any[]): string[] {
function getFunctionFromABI (line 97) | function getFunctionFromABI(abi: any[], functionName: string): any {
FILE: src/core/services/balance.ts
function getETHBalance (line 75) | async function getETHBalance(
function getERC20Balance (line 98) | async function getERC20Balance(
function isNFTOwner (line 146) | async function isNFTOwner(
function getERC721Balance (line 178) | async function getERC721Balance(
function getERC1155Balance (line 203) | async function getERC1155Balance(
FILE: src/core/services/blocks.ts
function getBlockNumber (line 10) | async function getBlockNumber(network = 'ethereum'): Promise<bigint> {
function getBlockByNumber (line 18) | async function getBlockByNumber(
function getBlockByHash (line 29) | async function getBlockByHash(
function getLatestBlock (line 40) | async function getLatestBlock(network = 'ethereum'): Promise<Block> {
FILE: src/core/services/clients.ts
function getPublicClient (line 19) | function getPublicClient(network = 'ethereum'): PublicClient {
function getWalletClient (line 45) | function getWalletClient(privateKey: Hex, network = 'ethereum'): WalletC...
function getAddressFromPrivateKey (line 62) | function getAddressFromPrivateKey(privateKey: Hex): Address {
FILE: src/core/services/contracts.ts
function readContract (line 15) | async function readContract(params: ReadContractParameters, network = 'e...
function writeContract (line 23) | async function writeContract(
function getLogs (line 35) | async function getLogs(params: GetLogsParameters, network = 'ethereum'):...
function isContract (line 46) | async function isContract(addressOrEns: string, network = 'ethereum'): P...
function multicall (line 62) | async function multicall(
FILE: src/core/services/ens.ts
function resolveAddress (line 11) | async function resolveAddress(
FILE: src/core/services/tokens.ts
function getERC20TokenInfo (line 81) | async function getERC20TokenInfo(
function getERC721TokenMetadata (line 118) | async function getERC721TokenMetadata(
function getERC1155TokenURI (line 151) | async function getERC1155TokenURI(
FILE: src/core/services/transactions.ts
function getTransaction (line 12) | async function getTransaction(hash: Hash, network = 'ethereum') {
function getTransactionReceipt (line 20) | async function getTransactionReceipt(hash: Hash, network = 'ethereum'): ...
function getTransactionCount (line 28) | async function getTransactionCount(address: Address, network = 'ethereum...
function estimateGas (line 37) | async function estimateGas(params: EstimateGasParameters, network = 'eth...
function getChainId (line 45) | async function getChainId(network = 'ethereum'): Promise<number> {
FILE: src/core/services/transfer.ts
function transferETH (line 125) | async function transferETH(
function transferERC20 (line 159) | async function transferERC20(
function approveERC20 (line 235) | async function approveERC20(
function transferERC721 (line 311) | async function transferERC721(
function transferERC1155 (line 389) | async function transferERC1155(
FILE: src/core/tools.ts
function registerEVMTools (line 25) | function registerEVMTools(server: McpServer) {
FILE: src/index.ts
function main (line 5) | async function main() {
FILE: src/server/http-server.ts
constant PORT (line 8) | const PORT = parseInt(process.env.MCP_PORT || "3001", 10);
constant HOST (line 9) | const HOST = process.env.MCP_HOST || "0.0.0.0";
constant SESSION_TIMEOUT_MS (line 20) | const SESSION_TIMEOUT_MS = 30 * 60 * 1000;
FILE: src/server/server.ts
function startServer (line 8) | async function startServer() {
Condensed preview — 30 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (185K chars).
[
{
"path": ".cursor/mcp.json",
"chars": 307,
"preview": "{\n \"mcpServers\": {\n \"evm-mcp-server\": {\n \"command\": \"npx\",\n \"args\": [\n \"-y\",\n \"@mcpdotdirect"
},
{
"path": ".github/workflows/release-publish.yml",
"chars": 3551,
"preview": "name: Release and Publish\n\non:\n workflow_dispatch:\n inputs:\n version_type:\n description: 'Version type ("
},
{
"path": ".gitignore",
"chars": 305,
"preview": "# dependencies (bun install)\nnode_modules\n\n# output\nout\ndist\n*.tgz\n\n# code coverage\ncoverage\n*.lcov\n\n# logs\nlogs\n_.log\nr"
},
{
"path": ".npmignore",
"chars": 369,
"preview": "# Source code (since we're publishing built files)\nsrc/\n\n# Development files\n.git/\n.github/\n.vscode/\n.idea/\n.cursor/\nmcp"
},
{
"path": "CHANGELOG.md",
"chars": 4646,
"preview": "## [2.0.4](https://github.com/mcpdotdirect/evm-mcp-server/compare/v2.0.3...v2.0.4) (2025-11-26)\n\n\n\n## [2.0.3](https://gi"
},
{
"path": "LICENSE",
"chars": 1069,
"preview": "MIT License\n\nCopyright (c) 2025 mcpdotdirect\n\nPermission is hereby granted, free of charge, to any person obtaining a co"
},
{
"path": "README.md",
"chars": 26203,
"preview": "# EVM MCP Server\n\n\n. The extraction includes 30 files (172.2 KB), approximately 43.3k tokens, and a symbol index with 49 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.