├── .cursor └── mcp.json ├── .github └── workflows │ └── release-publish.yml ├── .gitignore ├── .npmignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── bin └── cli.js ├── bun.lock ├── package.json ├── src ├── core │ ├── chains.ts │ ├── prompts.ts │ ├── resources.ts │ ├── services │ │ ├── balance.ts │ │ ├── blocks.ts │ │ ├── clients.ts │ │ ├── contracts.ts │ │ ├── ens.ts │ │ ├── index.ts │ │ ├── tokens.ts │ │ ├── transactions.ts │ │ ├── transfer.ts │ │ └── utils.ts │ └── tools.ts ├── index.ts └── server │ ├── http-server.ts │ └── server.ts └── tsconfig.json /.cursor/mcp.json: -------------------------------------------------------------------------------- 1 | { 2 | "mcpServers": { 3 | "evm-mcp-server": { 4 | "command": "npx", 5 | "args": [ 6 | "-y", 7 | "@mcpdotdirect/evm-mcp-server" 8 | ] 9 | }, 10 | "evm-mcp-http": { 11 | "command": "npx", 12 | "args": [ 13 | "-y", 14 | "@mcpdotdirect/evm-mcp-server", 15 | "--http" 16 | ] 17 | } 18 | } 19 | } -------------------------------------------------------------------------------- /.github/workflows/release-publish.yml: -------------------------------------------------------------------------------- 1 | name: Release and Publish 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | version_type: 7 | description: 'Version type (prerelease, prepatch, patch, preminor, minor, premajor, major)' 8 | required: true 9 | default: 'patch' 10 | type: choice 11 | options: 12 | - prerelease 13 | - prepatch 14 | - patch 15 | - preminor 16 | - minor 17 | - premajor 18 | - major 19 | custom_version: 20 | description: 'Custom version (leave empty to use version_type)' 21 | required: false 22 | type: string 23 | dist_tag: 24 | description: 'npm distribution tag (latest, next, beta, etc)' 25 | required: false 26 | default: 'latest' 27 | type: string 28 | 29 | jobs: 30 | release-and-publish: 31 | runs-on: ubuntu-latest 32 | permissions: 33 | contents: write 34 | packages: write 35 | id-token: write 36 | steps: 37 | - name: Checkout code 38 | uses: actions/checkout@v4 39 | with: 40 | fetch-depth: 0 41 | token: ${{ secrets.PAT_GITHUB }} 42 | 43 | - name: Setup Node.js 44 | uses: actions/setup-node@v4 45 | with: 46 | node-version: '18.x' 47 | registry-url: 'https://registry.npmjs.org' 48 | 49 | - name: Setup Bun 50 | uses: oven-sh/setup-bun@v1 51 | with: 52 | bun-version: latest 53 | 54 | - name: Install dependencies 55 | run: bun install 56 | 57 | - name: Configure Git 58 | run: | 59 | git config --local user.email "action@github.com" 60 | git config --local user.name "GitHub Action" 61 | git config pull.rebase false 62 | 63 | - name: Bump version 64 | id: bump_version 65 | run: | 66 | if [ -n "${{ github.event.inputs.custom_version }}" ]; then 67 | echo "Using custom version ${{ github.event.inputs.custom_version }}" 68 | npm version ${{ github.event.inputs.custom_version }} --no-git-tag-version 69 | echo "VERSION=${{ github.event.inputs.custom_version }}" >> $GITHUB_ENV 70 | else 71 | echo "Bumping ${{ github.event.inputs.version_type }} version" 72 | NEW_VERSION=$(npm version ${{ github.event.inputs.version_type }} --no-git-tag-version) 73 | echo "VERSION=${NEW_VERSION:1}" >> $GITHUB_ENV 74 | fi 75 | echo "New version: ${{ env.VERSION }}" 76 | 77 | - name: Generate Changelog 78 | run: | 79 | # Generate the full changelog 80 | npm run changelog 81 | # Generate release notes for just this version 82 | npm run changelog:latest 83 | 84 | - name: Build project 85 | run: bun run build && bun run build:http 86 | 87 | - name: Commit and push changes 88 | run: | 89 | git pull origin main --no-edit 90 | git add package.json CHANGELOG.md 91 | git commit -m "Bump version to v${{ env.VERSION }}" 92 | git push --force-with-lease 93 | 94 | - name: Create and push tag 95 | run: | 96 | # Delete the tag if it already exists locally 97 | git tag -d "v${{ env.VERSION }}" 2>/dev/null || true 98 | # Delete the tag if it exists on the remote 99 | git push origin --delete "v${{ env.VERSION }}" 2>/dev/null || true 100 | # Create and push the new tag 101 | git tag -a "v${{ env.VERSION }}" -m "Release v${{ env.VERSION }}" 102 | git push origin "v${{ env.VERSION }}" 103 | 104 | - name: Create GitHub Release 105 | uses: softprops/action-gh-release@v1 106 | with: 107 | tag_name: v${{ env.VERSION }} 108 | name: Release v${{ env.VERSION }} 109 | body_path: RELEASE_NOTES.md 110 | generate_release_notes: false 111 | env: 112 | GITHUB_TOKEN: ${{ secrets.PAT_GITHUB }} 113 | 114 | - name: Publish to npm 115 | run: npm publish --access public --provenance --tag ${{ github.event.inputs.dist_tag || 'latest' }} 116 | env: 117 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # dependencies (bun install) 2 | node_modules 3 | 4 | # output 5 | out 6 | dist 7 | *.tgz 8 | 9 | # code coverage 10 | coverage 11 | *.lcov 12 | 13 | # logs 14 | logs 15 | _.log 16 | report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json 17 | 18 | # caches 19 | .eslintcache 20 | .cache 21 | *.tsbuildinfo 22 | 23 | # IntelliJ based IDEs 24 | .idea 25 | 26 | # Context 27 | mcp-context/ 28 | build/ 29 | 30 | # Finder (MacOS) folder config 31 | .DS_Store 32 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # Source code (since we're publishing built files) 2 | src/ 3 | 4 | # Development files 5 | .git/ 6 | .github/ 7 | .vscode/ 8 | .idea/ 9 | .cursor/ 10 | mcp-context/ 11 | *.tsbuildinfo 12 | 13 | # Temp files 14 | tmp/ 15 | temp/ 16 | 17 | # Test files 18 | test/ 19 | tests/ 20 | __tests__/ 21 | *.spec.ts 22 | *.test.ts 23 | 24 | # Docs (except README.md which is included in "files") 25 | docs/ 26 | 27 | # Build artifacts 28 | node_modules/ 29 | coverage/ 30 | bun.lock 31 | yarn.lock 32 | package-lock.json -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # [1.2.0](https://github.com/mcpdotdirect/evm-mcp-server/compare/v1.1.3...v1.2.0) (2025-05-23) 2 | 3 | 4 | ### Features 5 | 6 | * add Filecoin Calibration network to chains and update mappings ([9790118](https://github.com/mcpdotdirect/evm-mcp-server/commit/97901181139a8574f688179864331777c7fda422)) 7 | 8 | 9 | 10 | ## [1.1.3](https://github.com/mcpdotdirect/evm-mcp-server/compare/v1.1.2...v1.1.3) (2025-03-22) 11 | 12 | 13 | 14 | ## [1.1.2](https://github.com/mcpdotdirect/evm-mcp-server/compare/v1.1.1...v1.1.2) (2025-03-22) 15 | 16 | 17 | 18 | ## [1.1.1](https://github.com/mcpdotdirect/evm-mcp-server/compare/v1.1.0...v1.1.1) (2025-03-22) 19 | 20 | 21 | ### Bug Fixes 22 | 23 | * fixed naming of github secret ([4300dad](https://github.com/mcpdotdirect/evm-mcp-server/commit/4300dad343dc696c9e345d9b18e37bbb481db961)) 24 | 25 | 26 | 27 | # [1.1.0](https://github.com/mcpdotdirect/evm-mcp-server/compare/db4d20f0aeb0b34f67b4be3b38c6bb662682bfb6...v1.1.0) (2025-03-22) 28 | 29 | 30 | ### Bug Fixes 31 | 32 | * fixed tools names to be callable by Cursor ([3938549](https://github.com/mcpdotdirect/evm-mcp-server/commit/3938549381d2b1abb406d25ccda365a53ef3555d)) 33 | * following standard naming, fixed SSE server ([b20a12d](https://github.com/mcpdotdirect/evm-mcp-server/commit/b20a12d81c25a262389bd8781d73095ec69d265b)) 34 | 35 | 36 | ### Features 37 | 38 | * add Lumia mainnet and testnet support in chains.ts and update README ([ee55fa7](https://github.com/mcpdotdirect/evm-mcp-server/commit/ee55fa750d4759d5d4e7254ce811f62a4fd5c6e9)) 39 | * adding ENS support ([4f19f12](https://github.com/mcpdotdirect/evm-mcp-server/commit/4f19f12c0df163fbade10f2334f2690d735831ea)) 40 | * adding get_address_from_private_key tool ([befc357](https://github.com/mcpdotdirect/evm-mcp-server/commit/befc35769dd21cfa031c084115ea59eeeecbf5b4)) 41 | * implemented v0 of EVM MCP server, needs testing ([db4d20f](https://github.com/mcpdotdirect/evm-mcp-server/commit/db4d20f0aeb0b34f67b4be3b38c6bb662682bfb6)) 42 | * npm public release ([df6d52d](https://github.com/mcpdotdirect/evm-mcp-server/commit/df6d52db01e0b290f0da7ea1a087243484ce4e5c)) 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 mcpdotdirect 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # EVM MCP Server 2 | 3 | ![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg) 4 | ![EVM Networks](https://img.shields.io/badge/Networks-30+-green) 5 | ![TypeScript](https://img.shields.io/badge/TypeScript-5.0+-3178C6) 6 | ![Viem](https://img.shields.io/badge/Viem-1.0+-green) 7 | 8 | A comprehensive Model Context Protocol (MCP) server that provides blockchain services across multiple 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. 9 | 10 | ## 📋 Contents 11 | 12 | - [Overview](#overview) 13 | - [Features](#features) 14 | - [Supported Networks](#supported-networks) 15 | - [Prerequisites](#prerequisites) 16 | - [Installation](#installation) 17 | - [Server Configuration](#server-configuration) 18 | - [Usage](#usage) 19 | - [API Reference](#api-reference) 20 | - [Tools](#tools) 21 | - [Resources](#resources) 22 | - [Security Considerations](#security-considerations) 23 | - [Project Structure](#project-structure) 24 | - [Development](#development) 25 | - [License](#license) 26 | 27 | ## 🔭 Overview 28 | 29 | The MCP EVM Server leverages the Model Context Protocol to provide blockchain services to AI agents. It supports a wide range of services including: 30 | 31 | - Reading blockchain state (balances, transactions, blocks, etc.) 32 | - Interacting with smart contracts 33 | - Transferring tokens (native, ERC20, ERC721, ERC1155) 34 | - Querying token metadata and balances 35 | - Chain-specific services across 30+ EVM networks 36 | - **ENS name resolution** for all address parameters (use human-readable names like 'vitalik.eth' instead of addresses) 37 | 38 | All services are exposed through a consistent interface of MCP tools and resources, 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. 39 | 40 | ## ✨ Features 41 | 42 | ### Blockchain Data Access 43 | 44 | - **Multi-chain support** for 30+ EVM-compatible networks 45 | - **Chain information** including blockNumber, chainId, and RPCs 46 | - **Block data** access by number, hash, or latest 47 | - **Transaction details** and receipts with decoded logs 48 | - **Address balances** for native tokens and all token standards 49 | - **ENS resolution** for human-readable Ethereum addresses (use 'vitalik.eth' instead of '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045') 50 | 51 | ### Token services 52 | 53 | - **ERC20 Tokens** 54 | - Get token metadata (name, symbol, decimals, supply) 55 | - Check token balances 56 | - Transfer tokens between addresses 57 | - Approve spending allowances 58 | 59 | - **NFTs (ERC721)** 60 | - Get collection and token metadata 61 | - Verify token ownership 62 | - Transfer NFTs between addresses 63 | - Retrieve token URIs and count holdings 64 | 65 | - **Multi-tokens (ERC1155)** 66 | - Get token balances and metadata 67 | - Transfer tokens with quantity 68 | - Access token URIs 69 | 70 | ### Smart Contract Interactions 71 | 72 | - **Read contract state** through view/pure functions 73 | - **Write services** with private key signing 74 | - **Contract verification** to distinguish from EOAs 75 | - **Event logs** retrieval and filtering 76 | 77 | ### Comprehensive Transaction Support 78 | 79 | - **Native token transfers** across all supported networks 80 | - **Gas estimation** for transaction planning 81 | - **Transaction status** and receipt information 82 | - **Error handling** with descriptive messages 83 | 84 | ## 🌐 Supported Networks 85 | 86 | ### Mainnets 87 | - Ethereum (ETH) 88 | - Optimism (OP) 89 | - Arbitrum (ARB) 90 | - Arbitrum Nova 91 | - Base 92 | - Polygon (MATIC) 93 | - Polygon zkEVM 94 | - Avalanche (AVAX) 95 | - Binance Smart Chain (BSC) 96 | - zkSync Era 97 | - Linea 98 | - Celo 99 | - Gnosis (xDai) 100 | - Fantom (FTM) 101 | - Filecoin (FIL) 102 | - Moonbeam 103 | - Moonriver 104 | - Cronos 105 | - Scroll 106 | - Mantle 107 | - Manta 108 | - Blast 109 | - Fraxtal 110 | - Mode 111 | - Metis 112 | - Kroma 113 | - Zora 114 | - Aurora 115 | - Canto 116 | - Flow 117 | - Lumia 118 | 119 | ### Testnets 120 | - Sepolia 121 | - Optimism Sepolia 122 | - Arbitrum Sepolia 123 | - Base Sepolia 124 | - Polygon Amoy 125 | - Avalanche Fuji 126 | - BSC Testnet 127 | - zkSync Sepolia 128 | - Linea Sepolia 129 | - Scroll Sepolia 130 | - Mantle Sepolia 131 | - Manta Sepolia 132 | - Blast Sepolia 133 | - Fraxtal Testnet 134 | - Mode Testnet 135 | - Metis Sepolia 136 | - Kroma Sepolia 137 | - Zora Sepolia 138 | - Celo Alfajores 139 | - Goerli 140 | - Holesky 141 | - Flow Testnet 142 | - Filecoin Calibration 143 | - Lumia Testnet 144 | 145 | ## 🛠️ Prerequisites 146 | 147 | - [Bun](https://bun.sh/) 1.0.0 or higher 148 | - Node.js 18.0.0 or higher (if not using Bun) 149 | 150 | ## 📦 Installation 151 | 152 | ```bash 153 | # Clone the repository 154 | git clone https://github.com/mcpdotdirect/mcp-evm-server.git 155 | cd mcp-evm-server 156 | 157 | # Install dependencies with Bun 158 | bun install 159 | 160 | # Or with npm 161 | npm install 162 | ``` 163 | 164 | ## ⚙️ Server Configuration 165 | 166 | The server uses the following default configuration: 167 | 168 | - **Default Chain ID**: 1 (Ethereum Mainnet) 169 | - **Server Port**: 3001 170 | - **Server Host**: 0.0.0.0 (accessible from any network interface) 171 | 172 | These values are hardcoded in the application. If you need to modify them, you can edit the following files: 173 | 174 | - For chain configuration: `src/core/chains.ts` 175 | - For server configuration: `src/server/http-server.ts` 176 | 177 | ## 🚀 Usage 178 | 179 | ### Using npx (No Installation Required) 180 | 181 | You can run the MCP EVM Server directly without installation using npx: 182 | 183 | ```bash 184 | # Run the server in stdio mode (for CLI tools) 185 | npx @mcpdotdirect/evm-mcp-server 186 | 187 | # Run the server in HTTP mode (for web applications) 188 | npx @mcpdotdirect/evm-mcp-server --http 189 | ``` 190 | 191 | ### Running the Server Locally 192 | 193 | Start the server using stdio (for embedding in CLI tools): 194 | 195 | ```bash 196 | # Start the stdio server 197 | bun start 198 | 199 | # Development mode with auto-reload 200 | bun dev 201 | ``` 202 | 203 | Or start the HTTP server with SSE for web applications: 204 | 205 | ```bash 206 | # Start the HTTP server 207 | bun start:http 208 | 209 | # Development mode with auto-reload 210 | bun dev:http 211 | ``` 212 | 213 | ### Connecting to the Server 214 | 215 | 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). 216 | 217 | ### Connecting from Cursor 218 | 219 | To connect to the MCP server from Cursor: 220 | 221 | 1. Open Cursor and go to Settings (gear icon in the bottom left) 222 | 2. Click on "Features" in the left sidebar 223 | 3. Scroll down to "MCP Servers" section 224 | 4. Click "Add new MCP server" 225 | 5. Enter the following details: 226 | - Server name: `evm-mcp-server` 227 | - Type: `command` 228 | - Command: `npx @mcpdotdirect/evm-mcp-server` 229 | 230 | 6. Click "Save" 231 | 232 | 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. 233 | 234 | ### Using mcp.json with Cursor 235 | 236 | 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: 237 | 238 | ```json 239 | { 240 | "mcpServers": { 241 | "evm-mcp-server": { 242 | "command": "npx", 243 | "args": [ 244 | "-y", 245 | "@mcpdotdirect/evm-mcp-server" 246 | ] 247 | }, 248 | "evm-mcp-http": { 249 | "command": "npx", 250 | "args": [ 251 | "-y", 252 | "@mcpdotdirect/evm-mcp-server", 253 | "--http" 254 | ] 255 | } 256 | } 257 | } 258 | ``` 259 | 260 | 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: 261 | 262 | 1. Share MCP configurations with your team 263 | 2. Version control your MCP setup 264 | 3. Use different server configurations for different projects 265 | 266 | ### Example: HTTP Mode with SSE 267 | 268 | 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: 269 | 270 | ```json 271 | { 272 | "mcpServers": { 273 | "evm-mcp-sse": { 274 | "url": "http://localhost:3001/sse" 275 | } 276 | } 277 | } 278 | ``` 279 | 280 | This connects directly to the HTTP server's SSE endpoint, which is useful for: 281 | - Web applications that need to connect to the MCP server from the browser 282 | - Environments where running local commands isn't ideal 283 | - Sharing a single MCP server instance among multiple users or applications 284 | 285 | To use this configuration: 286 | 1. Create a `.cursor` directory in your project root if it doesn't exist 287 | 2. Save the above JSON as `mcp.json` in the `.cursor` directory 288 | 3. Restart Cursor or open your project 289 | 4. Cursor will detect the configuration and offer to enable the server(s) 290 | 291 | ### Example: Using the MCP Server in Cursor 292 | 293 | After configuring the MCP server with `mcp.json`, you can easily use it in Cursor. Here's an example workflow: 294 | 295 | 1. Create a new JavaScript/TypeScript file in your project: 296 | 297 | ```javascript 298 | // blockchain-example.js 299 | async function main() { 300 | try { 301 | // Get ETH balance for an address using ENS 302 | console.log("Getting ETH balance for vitalik.eth..."); 303 | 304 | // When using with Cursor, you can simply ask Cursor to: 305 | // "Check the ETH balance of vitalik.eth on mainnet" 306 | // Or "Transfer 0.1 ETH from my wallet to vitalik.eth" 307 | 308 | // Cursor will use the MCP server to execute these operations 309 | // without requiring any additional code from you 310 | 311 | // This is the power of the MCP integration - your AI assistant 312 | // can directly interact with blockchain data and operations 313 | } catch (error) { 314 | console.error("Error:", error.message); 315 | } 316 | } 317 | 318 | main(); 319 | ``` 320 | 321 | 2. With the file open in Cursor, you can ask Cursor to: 322 | 323 | - "Check the current ETH balance of vitalik.eth" 324 | - "Look up the price of USDC on Ethereum" 325 | - "Show me the latest block on Optimism" 326 | - "Check if 0x1234... is a contract address" 327 | 328 | 3. Cursor will use the MCP server to execute these operations and return the results directly in your conversation. 329 | 330 | The MCP server handles all the blockchain communication while allowing Cursor to understand and execute blockchain-related tasks through natural language. 331 | 332 | ### Connecting using Claude CLI 333 | 334 | If you're using Claude CLI, you can connect to the MCP server with just two commands: 335 | 336 | ```bash 337 | # Add the MCP server 338 | claude mcp add evm-mcp-server npx @mcpdotdirect/evm-mcp-server 339 | 340 | # Start Claude with the MCP server enabled 341 | claude 342 | ``` 343 | 344 | ### Example: Getting a Token Balance with ENS 345 | 346 | ```javascript 347 | // Example of using the MCP client to check a token balance using ENS 348 | const mcp = new McpClient("http://localhost:3000"); 349 | 350 | const result = await mcp.invokeTool("get-token-balance", { 351 | tokenAddress: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", // USDC on Ethereum 352 | ownerAddress: "vitalik.eth", // ENS name instead of address 353 | network: "ethereum" 354 | }); 355 | 356 | console.log(result); 357 | // { 358 | // tokenAddress: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", 359 | // owner: "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045", 360 | // network: "ethereum", 361 | // raw: "1000000000", 362 | // formatted: "1000", 363 | // symbol: "USDC", 364 | // decimals: 6 365 | // } 366 | ``` 367 | 368 | ### Example: Resolving an ENS Name 369 | 370 | ```javascript 371 | // Example of using the MCP client to resolve an ENS name to an address 372 | const mcp = new McpClient("http://localhost:3000"); 373 | 374 | const result = await mcp.invokeTool("resolve-ens", { 375 | ensName: "vitalik.eth", 376 | network: "ethereum" 377 | }); 378 | 379 | console.log(result); 380 | // { 381 | // ensName: "vitalik.eth", 382 | // normalizedName: "vitalik.eth", 383 | // resolvedAddress: "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045", 384 | // network: "ethereum" 385 | // } 386 | ``` 387 | 388 | ## 📚 API Reference 389 | 390 | ### Tools 391 | 392 | The server provides the following MCP tools for agents. **All tools that accept address parameters support both Ethereum addresses and ENS names.** 393 | 394 | #### Token services 395 | 396 | | Tool Name | Description | Key Parameters | 397 | |-----------|-------------|----------------| 398 | | `get-token-info` | Get ERC20 token metadata | `tokenAddress` (address/ENS), `network` | 399 | | `get-token-balance` | Check ERC20 token balance | `tokenAddress` (address/ENS), `ownerAddress` (address/ENS), `network` | 400 | | `transfer-token` | Transfer ERC20 tokens | `privateKey`, `tokenAddress` (address/ENS), `toAddress` (address/ENS), `amount`, `network` | 401 | | `approve-token-spending` | Approve token allowances | `privateKey`, `tokenAddress` (address/ENS), `spenderAddress` (address/ENS), `amount`, `network` | 402 | | `get-nft-info` | Get NFT metadata | `tokenAddress` (address/ENS), `tokenId`, `network` | 403 | | `check-nft-ownership` | Verify NFT ownership | `tokenAddress` (address/ENS), `tokenId`, `ownerAddress` (address/ENS), `network` | 404 | | `transfer-nft` | Transfer an NFT | `privateKey`, `tokenAddress` (address/ENS), `tokenId`, `toAddress` (address/ENS), `network` | 405 | | `get-nft-balance` | Count NFTs owned | `tokenAddress` (address/ENS), `ownerAddress` (address/ENS), `network` | 406 | | `get-erc1155-token-uri` | Get ERC1155 metadata | `tokenAddress` (address/ENS), `tokenId`, `network` | 407 | | `get-erc1155-balance` | Check ERC1155 balance | `tokenAddress` (address/ENS), `tokenId`, `ownerAddress` (address/ENS), `network` | 408 | | `transfer-erc1155` | Transfer ERC1155 tokens | `privateKey`, `tokenAddress` (address/ENS), `tokenId`, `amount`, `toAddress` (address/ENS), `network` | 409 | 410 | #### Blockchain services 411 | 412 | | Tool Name | Description | Key Parameters | 413 | |-----------|-------------|----------------| 414 | | `get-chain-info` | Get network information | `network` | 415 | | `get-balance` | Get native token balance | `address` (address/ENS), `network` | 416 | | `transfer-eth` | Send native tokens | `privateKey`, `to` (address/ENS), `amount`, `network` | 417 | | `get-transaction` | Get transaction details | `txHash`, `network` | 418 | | `read-contract` | Read smart contract state | `contractAddress` (address/ENS), `abi`, `functionName`, `args`, `network` | 419 | | `write-contract` | Write to smart contract | `contractAddress` (address/ENS), `abi`, `functionName`, `args`, `privateKey`, `network` | 420 | | `is-contract` | Check if address is a contract | `address` (address/ENS), `network` | 421 | | `resolve-ens` | Resolve ENS name to address | `ensName`, `network` | 422 | 423 | ### Resources 424 | 425 | 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. 426 | 427 | #### Blockchain Resources 428 | 429 | | Resource URI Pattern | Description | 430 | |-----------|-------------| 431 | | `evm://{network}/chain` | Chain information for a specific network | 432 | | `evm://chain` | Ethereum mainnet chain information | 433 | | `evm://{network}/block/{blockNumber}` | Block data by number | 434 | | `evm://{network}/block/latest` | Latest block data | 435 | | `evm://{network}/address/{address}/balance` | Native token balance | 436 | | `evm://{network}/tx/{txHash}` | Transaction details | 437 | | `evm://{network}/tx/{txHash}/receipt` | Transaction receipt with logs | 438 | 439 | #### Token Resources 440 | 441 | | Resource URI Pattern | Description | 442 | |-----------|-------------| 443 | | `evm://{network}/token/{tokenAddress}` | ERC20 token information | 444 | | `evm://{network}/token/{tokenAddress}/balanceOf/{address}` | ERC20 token balance | 445 | | `evm://{network}/nft/{tokenAddress}/{tokenId}` | NFT (ERC721) token information | 446 | | `evm://{network}/nft/{tokenAddress}/{tokenId}/isOwnedBy/{address}` | NFT ownership verification | 447 | | `evm://{network}/erc1155/{tokenAddress}/{tokenId}/uri` | ERC1155 token URI | 448 | | `evm://{network}/erc1155/{tokenAddress}/{tokenId}/balanceOf/{address}` | ERC1155 token balance | 449 | 450 | ## 🔒 Security Considerations 451 | 452 | - **Private keys** are used only for transaction signing and are never stored by the server 453 | - Consider implementing additional authentication mechanisms for production use 454 | - Use HTTPS for the HTTP server in production environments 455 | - Implement rate limiting to prevent abuse 456 | - For high-value services, consider adding confirmation steps 457 | 458 | ## 📁 Project Structure 459 | 460 | ``` 461 | mcp-evm-server/ 462 | ├── src/ 463 | │ ├── index.ts # Main stdio server entry point 464 | │ ├── server/ # Server-related files 465 | │ │ ├── http-server.ts # HTTP server with SSE 466 | │ │ └── server.ts # General server setup 467 | │ ├── core/ 468 | │ │ ├── chains.ts # Chain definitions and utilities 469 | │ │ ├── resources.ts # MCP resources implementation 470 | │ │ ├── tools.ts # MCP tools implementation 471 | │ │ ├── prompts.ts # MCP prompts implementation 472 | │ │ └── services/ # Core blockchain services 473 | │ │ ├── index.ts # Operation exports 474 | │ │ ├── balance.ts # Balance services 475 | │ │ ├── transfer.ts # Token transfer services 476 | │ │ ├── utils.ts # Utility functions 477 | │ │ ├── tokens.ts # Token metadata services 478 | │ │ ├── contracts.ts # Contract interactions 479 | │ │ ├── transactions.ts # Transaction services 480 | │ │ └── blocks.ts # Block services 481 | │ │ └── clients.ts # RPC client utilities 482 | ├── package.json 483 | ├── tsconfig.json 484 | └── README.md 485 | ``` 486 | 487 | ## 🛠️ Development 488 | 489 | To modify or extend the server: 490 | 491 | 1. Add new services in the appropriate file under `src/core/services/` 492 | 2. Register new tools in `src/core/tools.ts` 493 | 3. Register new resources in `src/core/resources.ts` 494 | 4. Add new network support in `src/core/chains.ts` 495 | 5. To change server configuration, edit the hardcoded values in `src/server/http-server.ts` 496 | 497 | ## 📄 License 498 | 499 | This project is licensed under the terms of the [MIT License](./LICENSE). 500 | -------------------------------------------------------------------------------- /bin/cli.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import { fileURLToPath } from 'url'; 4 | import { dirname, resolve } from 'path'; 5 | import { spawn } from 'child_process'; 6 | import { createRequire } from 'module'; 7 | 8 | const __filename = fileURLToPath(import.meta.url); 9 | const __dirname = dirname(__filename); 10 | const require = createRequire(import.meta.url); 11 | 12 | // Parse command line arguments 13 | const args = process.argv.slice(2); 14 | const httpMode = args.includes('--http') || args.includes('-h'); 15 | 16 | console.log(`Starting EVM MCP Server in ${httpMode ? 'HTTP' : 'stdio'} mode...`); 17 | 18 | // Determine which file to execute 19 | const scriptPath = resolve(__dirname, '../build', httpMode ? 'http-server.js' : 'index.js'); 20 | 21 | try { 22 | // Check if the built files exist 23 | require.resolve(scriptPath); 24 | 25 | // Execute the server 26 | const server = spawn('node', [scriptPath], { 27 | stdio: 'inherit', 28 | shell: false 29 | }); 30 | 31 | server.on('error', (err) => { 32 | console.error('Failed to start server:', err); 33 | process.exit(1); 34 | }); 35 | 36 | // Handle clean shutdown 37 | const cleanup = () => { 38 | if (!server.killed) { 39 | server.kill(); 40 | } 41 | }; 42 | 43 | process.on('SIGINT', cleanup); 44 | process.on('SIGTERM', cleanup); 45 | process.on('exit', cleanup); 46 | 47 | } catch (error) { 48 | console.error('Error: Server files not found. The package may not be built correctly.'); 49 | console.error('Please try reinstalling the package or contact the maintainers.'); 50 | console.error(error); 51 | process.exit(1); 52 | } -------------------------------------------------------------------------------- /bun.lock: -------------------------------------------------------------------------------- 1 | { 2 | "lockfileVersion": 1, 3 | "workspaces": { 4 | "": { 5 | "name": "mcp-evm-server", 6 | "dependencies": { 7 | "@modelcontextprotocol/sdk": "^1.6.1", 8 | "cors": "^2.8.5", 9 | "express": "^4.21.2", 10 | "viem": "^2.23.8", 11 | "zod": "^3.24.2", 12 | }, 13 | "devDependencies": { 14 | "@types/bun": "latest", 15 | "@types/cors": "^2.8.17", 16 | "@types/express": "^5.0.0", 17 | }, 18 | "peerDependencies": { 19 | "typescript": "^5.8.2", 20 | }, 21 | }, 22 | }, 23 | "packages": { 24 | "@adraffy/ens-normalize": ["@adraffy/ens-normalize@1.11.0", "", {}, "sha512-/3DDPKHqqIqxUULp8yP4zODUY1i+2xvVWsv8A79xGWdCAG+8sb0hRh0Rk2QyOJUnnbyPUAZYcpBuRe3nS2OIUg=="], 25 | 26 | "@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.7.0", "", { "dependencies": { "content-type": "^1.0.5", "cors": "^2.8.5", "eventsource": "^3.0.2", "express": "^5.0.1", "express-rate-limit": "^7.5.0", "pkce-challenge": "^4.1.0", "raw-body": "^3.0.0", "zod": "^3.23.8", "zod-to-json-schema": "^3.24.1" } }, "sha512-IYPe/FLpvF3IZrd/f5p5ffmWhMc3aEMuM2wGJASDqC2Ge7qatVCdbfPx3n/5xFeb19xN0j/911M2AaFuircsWA=="], 27 | 28 | "@noble/curves": ["@noble/curves@1.8.1", "", { "dependencies": { "@noble/hashes": "1.7.1" } }, "sha512-warwspo+UYUPep0Q+vtdVB4Ugn8GGQj8iyB3gnRWsztmUHTI3S1nhdiWNsPUGL0vud7JlRRk1XEu7Lq1KGTnMQ=="], 29 | 30 | "@noble/hashes": ["@noble/hashes@1.7.1", "", {}, "sha512-B8XBPsn4vT/KJAGqDzbwztd+6Yte3P4V7iafm24bxgDe/mlRuK6xmWPuCNrKt2vDafZ8MfJLlchDG/vYafQEjQ=="], 31 | 32 | "@scure/base": ["@scure/base@1.2.4", "", {}, "sha512-5Yy9czTO47mqz+/J8GM6GIId4umdCk1wc1q8rKERQulIoc8VP9pzDcghv10Tl2E7R96ZUx/PhND3ESYUQX8NuQ=="], 33 | 34 | "@scure/bip32": ["@scure/bip32@1.6.2", "", { "dependencies": { "@noble/curves": "~1.8.1", "@noble/hashes": "~1.7.1", "@scure/base": "~1.2.2" } }, "sha512-t96EPDMbtGgtb7onKKqxRLfE5g05k7uHnHRM2xdE6BP/ZmxaLtPek4J4KfVn/90IQNrU1IOAqMgiDtUdtbe3nw=="], 35 | 36 | "@scure/bip39": ["@scure/bip39@1.5.4", "", { "dependencies": { "@noble/hashes": "~1.7.1", "@scure/base": "~1.2.4" } }, "sha512-TFM4ni0vKvCfBpohoh+/lY05i9gRbSwXWngAsF4CABQxoaOHijxuaZ2R6cStDQ5CHtHO9aGJTr4ksVJASRRyMA=="], 37 | 38 | "@types/body-parser": ["@types/body-parser@1.19.5", "", { "dependencies": { "@types/connect": "*", "@types/node": "*" } }, "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg=="], 39 | 40 | "@types/bun": ["@types/bun@1.2.5", "", { "dependencies": { "bun-types": "1.2.5" } }, "sha512-w2OZTzrZTVtbnJew1pdFmgV99H0/L+Pvw+z1P67HaR18MHOzYnTYOi6qzErhK8HyT+DB782ADVPPE92Xu2/Opg=="], 41 | 42 | "@types/connect": ["@types/connect@3.4.38", "", { "dependencies": { "@types/node": "*" } }, "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug=="], 43 | 44 | "@types/cors": ["@types/cors@2.8.17", "", { "dependencies": { "@types/node": "*" } }, "sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA=="], 45 | 46 | "@types/express": ["@types/express@5.0.0", "", { "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^5.0.0", "@types/qs": "*", "@types/serve-static": "*" } }, "sha512-DvZriSMehGHL1ZNLzi6MidnsDhUZM/x2pRdDIKdwbUNqqwHxMlRdkxtn6/EPKyqKpHqTl/4nRZsRNLpZxZRpPQ=="], 47 | 48 | "@types/express-serve-static-core": ["@types/express-serve-static-core@5.0.6", "", { "dependencies": { "@types/node": "*", "@types/qs": "*", "@types/range-parser": "*", "@types/send": "*" } }, "sha512-3xhRnjJPkULekpSzgtoNYYcTWgEZkp4myc+Saevii5JPnHNvHMRlBSHDbs7Bh1iPPoVTERHEZXyhyLbMEsExsA=="], 49 | 50 | "@types/http-errors": ["@types/http-errors@2.0.4", "", {}, "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA=="], 51 | 52 | "@types/mime": ["@types/mime@1.3.5", "", {}, "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w=="], 53 | 54 | "@types/node": ["@types/node@22.13.10", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-I6LPUvlRH+O6VRUqYOcMudhaIdUVWfsjnZavnsraHvpBwaEyMN29ry+0UVJhImYL16xsscu0aske3yA+uPOWfw=="], 55 | 56 | "@types/qs": ["@types/qs@6.9.18", "", {}, "sha512-kK7dgTYDyGqS+e2Q4aK9X3D7q234CIZ1Bv0q/7Z5IwRDoADNU81xXJK/YVyLbLTZCoIwUoDoffFeF+p/eIklAA=="], 57 | 58 | "@types/range-parser": ["@types/range-parser@1.2.7", "", {}, "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ=="], 59 | 60 | "@types/send": ["@types/send@0.17.4", "", { "dependencies": { "@types/mime": "^1", "@types/node": "*" } }, "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA=="], 61 | 62 | "@types/serve-static": ["@types/serve-static@1.15.7", "", { "dependencies": { "@types/http-errors": "*", "@types/node": "*", "@types/send": "*" } }, "sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw=="], 63 | 64 | "@types/ws": ["@types/ws@8.5.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-bd/YFLW+URhBzMXurx7lWByOu+xzU9+kb3RboOteXYDfW+tr+JZa99OyNmPINEGB/ahzKrEuc8rcv4gnpJmxTw=="], 65 | 66 | "abitype": ["abitype@1.0.8", "", { "peerDependencies": { "typescript": ">=5.0.4", "zod": "^3 >=3.22.0" }, "optionalPeers": ["typescript", "zod"] }, "sha512-ZeiI6h3GnW06uYDLx0etQtX/p8E24UaHHBj57RSjK7YBFe7iuVn07EDpOeP451D06sF27VOz9JJPlIKJmXgkEg=="], 67 | 68 | "accepts": ["accepts@1.3.8", "", { "dependencies": { "mime-types": "~2.1.34", "negotiator": "0.6.3" } }, "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw=="], 69 | 70 | "array-flatten": ["array-flatten@1.1.1", "", {}, "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg=="], 71 | 72 | "body-parser": ["body-parser@1.20.3", "", { "dependencies": { "bytes": "3.1.2", "content-type": "~1.0.5", "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", "http-errors": "2.0.0", "iconv-lite": "0.4.24", "on-finished": "2.4.1", "qs": "6.13.0", "raw-body": "2.5.2", "type-is": "~1.6.18", "unpipe": "1.0.0" } }, "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g=="], 73 | 74 | "bun-types": ["bun-types@1.2.5", "", { "dependencies": { "@types/node": "*", "@types/ws": "~8.5.10" } }, "sha512-3oO6LVGGRRKI4kHINx5PIdIgnLRb7l/SprhzqXapmoYkFl5m4j6EvALvbDVuuBFaamB46Ap6HCUxIXNLCGy+tg=="], 75 | 76 | "bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="], 77 | 78 | "call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="], 79 | 80 | "call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="], 81 | 82 | "content-disposition": ["content-disposition@0.5.4", "", { "dependencies": { "safe-buffer": "5.2.1" } }, "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ=="], 83 | 84 | "content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="], 85 | 86 | "cookie": ["cookie@0.7.1", "", {}, "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w=="], 87 | 88 | "cookie-signature": ["cookie-signature@1.0.6", "", {}, "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ=="], 89 | 90 | "cors": ["cors@2.8.5", "", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g=="], 91 | 92 | "debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], 93 | 94 | "depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="], 95 | 96 | "destroy": ["destroy@1.2.0", "", {}, "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg=="], 97 | 98 | "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], 99 | 100 | "ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="], 101 | 102 | "encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="], 103 | 104 | "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="], 105 | 106 | "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="], 107 | 108 | "es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="], 109 | 110 | "escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="], 111 | 112 | "etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="], 113 | 114 | "eventemitter3": ["eventemitter3@5.0.1", "", {}, "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA=="], 115 | 116 | "eventsource": ["eventsource@3.0.5", "", { "dependencies": { "eventsource-parser": "^3.0.0" } }, "sha512-LT/5J605bx5SNyE+ITBDiM3FxffBiq9un7Vx0EwMDM3vg8sWKx/tO2zC+LMqZ+smAM0F2hblaDZUVZF0te2pSw=="], 117 | 118 | "eventsource-parser": ["eventsource-parser@3.0.0", "", {}, "sha512-T1C0XCUimhxVQzW4zFipdx0SficT651NnkR0ZSH3yQwh+mFMdLfgjABVi4YtMTtaL4s168593DaoaRLMqryavA=="], 119 | 120 | "express": ["express@4.21.2", "", { "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", "body-parser": "1.20.3", "content-disposition": "0.5.4", "content-type": "~1.0.4", "cookie": "0.7.1", "cookie-signature": "1.0.6", "debug": "2.6.9", "depd": "2.0.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", "finalhandler": "1.3.1", "fresh": "0.5.2", "http-errors": "2.0.0", "merge-descriptors": "1.0.3", "methods": "~1.1.2", "on-finished": "2.4.1", "parseurl": "~1.3.3", "path-to-regexp": "0.1.12", "proxy-addr": "~2.0.7", "qs": "6.13.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", "send": "0.19.0", "serve-static": "1.16.2", "setprototypeof": "1.2.0", "statuses": "2.0.1", "type-is": "~1.6.18", "utils-merge": "1.0.1", "vary": "~1.1.2" } }, "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA=="], 121 | 122 | "express-rate-limit": ["express-rate-limit@7.5.0", "", { "peerDependencies": { "express": "^4.11 || 5 || ^5.0.0-beta.1" } }, "sha512-eB5zbQh5h+VenMPM3fh+nw1YExi5nMr6HUCR62ELSP11huvxm/Uir1H1QEyTkk5QX6A58pX6NmaTMceKZ0Eodg=="], 123 | 124 | "finalhandler": ["finalhandler@1.3.1", "", { "dependencies": { "debug": "2.6.9", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "on-finished": "2.4.1", "parseurl": "~1.3.3", "statuses": "2.0.1", "unpipe": "~1.0.0" } }, "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ=="], 125 | 126 | "forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="], 127 | 128 | "fresh": ["fresh@0.5.2", "", {}, "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q=="], 129 | 130 | "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], 131 | 132 | "get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="], 133 | 134 | "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="], 135 | 136 | "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="], 137 | 138 | "has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="], 139 | 140 | "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="], 141 | 142 | "http-errors": ["http-errors@2.0.0", "", { "dependencies": { "depd": "2.0.0", "inherits": "2.0.4", "setprototypeof": "1.2.0", "statuses": "2.0.1", "toidentifier": "1.0.1" } }, "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ=="], 143 | 144 | "iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="], 145 | 146 | "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], 147 | 148 | "ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="], 149 | 150 | "is-promise": ["is-promise@4.0.0", "", {}, "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="], 151 | 152 | "isows": ["isows@1.0.6", "", { "peerDependencies": { "ws": "*" } }, "sha512-lPHCayd40oW98/I0uvgaHKWCSvkzY27LjWLbtzOm64yQ+G3Q5npjjbdppU65iZXkK1Zt+kH9pfegli0AYfwYYw=="], 153 | 154 | "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], 155 | 156 | "media-typer": ["media-typer@0.3.0", "", {}, "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ=="], 157 | 158 | "merge-descriptors": ["merge-descriptors@1.0.3", "", {}, "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ=="], 159 | 160 | "methods": ["methods@1.1.2", "", {}, "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w=="], 161 | 162 | "mime": ["mime@1.6.0", "", { "bin": { "mime": "cli.js" } }, "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg=="], 163 | 164 | "mime-db": ["mime-db@1.53.0", "", {}, "sha512-oHlN/w+3MQ3rba9rqFr6V/ypF10LSkdwUysQL7GkXoTgIWeV+tcXGA852TBxH+gsh8UWoyhR1hKcoMJTuWflpg=="], 165 | 166 | "mime-types": ["mime-types@3.0.0", "", { "dependencies": { "mime-db": "^1.53.0" } }, "sha512-XqoSHeCGjVClAmoGFG3lVFqQFRIrTVw2OH3axRqAcfaw+gHWIfnASS92AV+Rl/mk0MupgZTRHQOjxY6YVnzK5w=="], 167 | 168 | "ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], 169 | 170 | "negotiator": ["negotiator@0.6.3", "", {}, "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="], 171 | 172 | "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], 173 | 174 | "object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="], 175 | 176 | "on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="], 177 | 178 | "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], 179 | 180 | "ox": ["ox@0.6.9", "", { "dependencies": { "@adraffy/ens-normalize": "^1.10.1", "@noble/curves": "^1.6.0", "@noble/hashes": "^1.5.0", "@scure/bip32": "^1.5.0", "@scure/bip39": "^1.4.0", "abitype": "^1.0.6", "eventemitter3": "5.0.1" }, "peerDependencies": { "typescript": ">=5.4.0" }, "optionalPeers": ["typescript"] }, "sha512-wi5ShvzE4eOcTwQVsIPdFr+8ycyX+5le/96iAJutaZAvCes1J0+RvpEPg5QDPDiaR0XQQAvZVl7AwqQcINuUug=="], 181 | 182 | "parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="], 183 | 184 | "path-to-regexp": ["path-to-regexp@0.1.12", "", {}, "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ=="], 185 | 186 | "pkce-challenge": ["pkce-challenge@4.1.0", "", {}, "sha512-ZBmhE1C9LcPoH9XZSdwiPtbPHZROwAnMy+kIFQVrnMCxY4Cudlz3gBOpzilgc0jOgRaiT3sIWfpMomW2ar2orQ=="], 187 | 188 | "proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="], 189 | 190 | "qs": ["qs@6.13.0", "", { "dependencies": { "side-channel": "^1.0.6" } }, "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg=="], 191 | 192 | "range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="], 193 | 194 | "raw-body": ["raw-body@3.0.0", "", { "dependencies": { "bytes": "3.1.2", "http-errors": "2.0.0", "iconv-lite": "0.6.3", "unpipe": "1.0.0" } }, "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g=="], 195 | 196 | "router": ["router@2.1.0", "", { "dependencies": { "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-/m/NSLxeYEgWNtyC+WtNHCF7jbGxOibVWKnn+1Psff4dJGOfoXP+MuC/f2CwSmyiHdOIzYnYFp4W6GxWfekaLA=="], 197 | 198 | "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], 199 | 200 | "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], 201 | 202 | "send": ["send@0.19.0", "", { "dependencies": { "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", "encodeurl": "~1.0.2", "escape-html": "~1.0.3", "etag": "~1.8.1", "fresh": "0.5.2", "http-errors": "2.0.0", "mime": "1.6.0", "ms": "2.1.3", "on-finished": "2.4.1", "range-parser": "~1.2.1", "statuses": "2.0.1" } }, "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw=="], 203 | 204 | "serve-static": ["serve-static@1.16.2", "", { "dependencies": { "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "parseurl": "~1.3.3", "send": "0.19.0" } }, "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw=="], 205 | 206 | "setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="], 207 | 208 | "side-channel": ["side-channel@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", "side-channel-list": "^1.0.0", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="], 209 | 210 | "side-channel-list": ["side-channel-list@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3" } }, "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA=="], 211 | 212 | "side-channel-map": ["side-channel-map@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3" } }, "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA=="], 213 | 214 | "side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="], 215 | 216 | "statuses": ["statuses@2.0.1", "", {}, "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ=="], 217 | 218 | "toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="], 219 | 220 | "type-is": ["type-is@1.6.18", "", { "dependencies": { "media-typer": "0.3.0", "mime-types": "~2.1.24" } }, "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g=="], 221 | 222 | "typescript": ["typescript@5.8.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ=="], 223 | 224 | "undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="], 225 | 226 | "unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="], 227 | 228 | "utils-merge": ["utils-merge@1.0.1", "", {}, "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA=="], 229 | 230 | "vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="], 231 | 232 | "viem": ["viem@2.23.9", "", { "dependencies": { "@noble/curves": "1.8.1", "@noble/hashes": "1.7.1", "@scure/bip32": "1.6.2", "@scure/bip39": "1.5.4", "abitype": "1.0.8", "isows": "1.0.6", "ox": "0.6.9", "ws": "8.18.1" }, "peerDependencies": { "typescript": ">=5.0.4" }, "optionalPeers": ["typescript"] }, "sha512-y8VLPfKukrstZKTerS9bm45ajZ22wUyStF+VquK3I2OovWLOyXSbQmJWei8syMFhp1uwhxh1tb0fAdx0WSRZWg=="], 233 | 234 | "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], 235 | 236 | "ws": ["ws@8.18.1", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-RKW2aJZMXeMxVpnZ6bck+RswznaxmzdULiBr6KY7XkTnW8uvt0iT9H5DkHUChXrc+uurzwa0rVI16n/Xzjdz1w=="], 237 | 238 | "zod": ["zod@3.24.2", "", {}, "sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ=="], 239 | 240 | "zod-to-json-schema": ["zod-to-json-schema@3.24.3", "", { "peerDependencies": { "zod": "^3.24.1" } }, "sha512-HIAfWdYIt1sssHfYZFCXp4rU1w2r8hVVXYIlmoa0r0gABLs5di3RCqPU5DDROogVz1pAdYBaz7HK5n9pSUNs3A=="], 241 | 242 | "@modelcontextprotocol/sdk/express": ["express@5.0.1", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.0.1", "content-disposition": "^1.0.0", "content-type": "~1.0.4", "cookie": "0.7.1", "cookie-signature": "^1.2.1", "debug": "4.3.6", "depd": "2.0.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", "finalhandler": "^2.0.0", "fresh": "2.0.0", "http-errors": "2.0.0", "merge-descriptors": "^2.0.0", "methods": "~1.1.2", "mime-types": "^3.0.0", "on-finished": "2.4.1", "once": "1.4.0", "parseurl": "~1.3.3", "proxy-addr": "~2.0.7", "qs": "6.13.0", "range-parser": "~1.2.1", "router": "^2.0.0", "safe-buffer": "5.2.1", "send": "^1.1.0", "serve-static": "^2.1.0", "setprototypeof": "1.2.0", "statuses": "2.0.1", "type-is": "^2.0.0", "utils-merge": "1.0.1", "vary": "~1.1.2" } }, "sha512-ORF7g6qGnD+YtUG9yx4DFoqCShNMmUKiXuT5oWMHiOvt/4WFbHC6yCwQMTSBMno7AqntNCAzzcnnjowRkTL9eQ=="], 243 | 244 | "accepts/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], 245 | 246 | "body-parser/iconv-lite": ["iconv-lite@0.4.24", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="], 247 | 248 | "body-parser/raw-body": ["raw-body@2.5.2", "", { "dependencies": { "bytes": "3.1.2", "http-errors": "2.0.0", "iconv-lite": "0.4.24", "unpipe": "1.0.0" } }, "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA=="], 249 | 250 | "router/path-to-regexp": ["path-to-regexp@8.2.0", "", {}, "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ=="], 251 | 252 | "send/encodeurl": ["encodeurl@1.0.2", "", {}, "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w=="], 253 | 254 | "send/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], 255 | 256 | "type-is/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], 257 | 258 | "@modelcontextprotocol/sdk/express/accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="], 259 | 260 | "@modelcontextprotocol/sdk/express/body-parser": ["body-parser@2.1.0", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.0", "http-errors": "^2.0.0", "iconv-lite": "^0.5.2", "on-finished": "^2.4.1", "qs": "^6.14.0", "raw-body": "^3.0.0", "type-is": "^2.0.0" } }, "sha512-/hPxh61E+ll0Ujp24Ilm64cykicul1ypfwjVttduAiEdtnJFvLePSrIPk+HMImtNv5270wOGCb1Tns2rybMkoQ=="], 261 | 262 | "@modelcontextprotocol/sdk/express/content-disposition": ["content-disposition@1.0.0", "", { "dependencies": { "safe-buffer": "5.2.1" } }, "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg=="], 263 | 264 | "@modelcontextprotocol/sdk/express/cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="], 265 | 266 | "@modelcontextprotocol/sdk/express/debug": ["debug@4.3.6", "", { "dependencies": { "ms": "2.1.2" } }, "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg=="], 267 | 268 | "@modelcontextprotocol/sdk/express/finalhandler": ["finalhandler@2.1.0", "", { "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "on-finished": "^2.4.1", "parseurl": "^1.3.3", "statuses": "^2.0.1" } }, "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q=="], 269 | 270 | "@modelcontextprotocol/sdk/express/fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="], 271 | 272 | "@modelcontextprotocol/sdk/express/merge-descriptors": ["merge-descriptors@2.0.0", "", {}, "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g=="], 273 | 274 | "@modelcontextprotocol/sdk/express/send": ["send@1.1.0", "", { "dependencies": { "debug": "^4.3.5", "destroy": "^1.2.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^0.5.2", "http-errors": "^2.0.0", "mime-types": "^2.1.35", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "statuses": "^2.0.1" } }, "sha512-v67WcEouB5GxbTWL/4NeToqcZiAWEq90N888fczVArY8A79J0L4FD7vj5hm3eUMua5EpoQ59wa/oovY6TLvRUA=="], 275 | 276 | "@modelcontextprotocol/sdk/express/serve-static": ["serve-static@2.1.0", "", { "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "parseurl": "^1.3.3", "send": "^1.0.0" } }, "sha512-A3We5UfEjG8Z7VkDv6uItWw6HY2bBSBJT1KtVESn6EOoOr2jAxNhxWCLY3jDE2WcuHXByWju74ck3ZgLwL8xmA=="], 277 | 278 | "@modelcontextprotocol/sdk/express/type-is": ["type-is@2.0.0", "", { "dependencies": { "content-type": "^1.0.5", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-gd0sGezQYCbWSbkZr75mln4YBidWUN60+devscpLF5mtRDUpiaTvKpBNrdaCvel1NdR2k6vclXybU5fBd2i+nw=="], 279 | 280 | "accepts/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], 281 | 282 | "type-is/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], 283 | 284 | "@modelcontextprotocol/sdk/express/accepts/negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="], 285 | 286 | "@modelcontextprotocol/sdk/express/body-parser/debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="], 287 | 288 | "@modelcontextprotocol/sdk/express/body-parser/iconv-lite": ["iconv-lite@0.5.2", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-kERHXvpSaB4aU3eANwidg79K8FlrN77m8G9V+0vOR3HYaRifrlwMEpT7ZBJqLSEIHnEgJTHcWK82wwLwwKwtag=="], 289 | 290 | "@modelcontextprotocol/sdk/express/body-parser/qs": ["qs@6.14.0", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w=="], 291 | 292 | "@modelcontextprotocol/sdk/express/debug/ms": ["ms@2.1.2", "", {}, "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="], 293 | 294 | "@modelcontextprotocol/sdk/express/finalhandler/debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="], 295 | 296 | "@modelcontextprotocol/sdk/express/send/fresh": ["fresh@0.5.2", "", {}, "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q=="], 297 | 298 | "@modelcontextprotocol/sdk/express/send/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], 299 | 300 | "@modelcontextprotocol/sdk/express/send/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], 301 | 302 | "@modelcontextprotocol/sdk/express/type-is/media-typer": ["media-typer@1.1.0", "", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="], 303 | 304 | "@modelcontextprotocol/sdk/express/body-parser/debug/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], 305 | 306 | "@modelcontextprotocol/sdk/express/finalhandler/debug/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], 307 | 308 | "@modelcontextprotocol/sdk/express/send/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], 309 | } 310 | } 311 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@mcpdotdirect/evm-mcp-server", 3 | "module": "src/index.ts", 4 | "type": "module", 5 | "version": "1.2.0", 6 | "description": "Model Context Protocol (MCP) server for interacting with EVM-compatible networks", 7 | "bin": { 8 | "evm-mcp-server": "./bin/cli.js" 9 | }, 10 | "main": "build/index.js", 11 | "files": [ 12 | "build/", 13 | "bin/", 14 | "README.md", 15 | "LICENSE" 16 | ], 17 | "scripts": { 18 | "start": "bun run src/index.ts", 19 | "build": "bun build src/index.ts --outdir build --target node", 20 | "build:http": "bun build src/server/http-server.ts --outdir build --target node --outfile http-server.js", 21 | "dev": "bun --watch src/index.ts", 22 | "start:http": "bun run src/server/http-server.ts", 23 | "dev:http": "bun --watch src/server/http-server.ts", 24 | "prepublishOnly": "bun run build && bun run build:http", 25 | "version:patch": "npm version patch", 26 | "version:minor": "npm version minor", 27 | "version:major": "npm version major", 28 | "release": "npm publish", 29 | "changelog": "conventional-changelog -p angular -i CHANGELOG.md -s -r 0", 30 | "changelog:latest": "conventional-changelog -p angular -r 1 > RELEASE_NOTES.md" 31 | }, 32 | "devDependencies": { 33 | "@types/bun": "latest", 34 | "@types/cors": "^2.8.17", 35 | "@types/express": "^5.0.0", 36 | "conventional-changelog-cli": "^5.0.0" 37 | }, 38 | "peerDependencies": { 39 | "typescript": "^5.8.2" 40 | }, 41 | "dependencies": { 42 | "@modelcontextprotocol/sdk": "^1.7.0", 43 | "cors": "^2.8.5", 44 | "express": "^4.21.2", 45 | "viem": "^2.23.9", 46 | "zod": "^3.24.2" 47 | }, 48 | "keywords": [ 49 | "mcp", 50 | "model-context-protocol", 51 | "evm", 52 | "blockchain", 53 | "ethereum", 54 | "web3", 55 | "smart-contracts", 56 | "ai", 57 | "agent" 58 | ], 59 | "author": "Etheral ", 60 | "license": "MIT", 61 | "engines": { 62 | "node": ">=18.0.0" 63 | }, 64 | "repository": { 65 | "type": "git", 66 | "url": "https://github.com/mcpdotdirect/evm-mcp-server" 67 | }, 68 | "bugs": { 69 | "url": "https://github.com/mcpdotdirect/evm-mcp-server/issues" 70 | }, 71 | "homepage": "https://github.com/mcpdotdirect/evm-mcp-server#readme", 72 | "publishConfig": { 73 | "access": "public" 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/core/chains.ts: -------------------------------------------------------------------------------- 1 | import { type Chain } from 'viem'; 2 | import { 3 | // Mainnets 4 | mainnet, 5 | optimism, 6 | arbitrum, 7 | arbitrumNova, 8 | base, 9 | polygon, 10 | polygonZkEvm, 11 | avalanche, 12 | bsc, 13 | zksync, 14 | linea, 15 | celo, 16 | gnosis, 17 | fantom, 18 | filecoin, 19 | moonbeam, 20 | moonriver, 21 | cronos, 22 | lumiaMainnet, 23 | scroll, 24 | mantle, 25 | manta, 26 | blast, 27 | fraxtal, 28 | mode, 29 | metis, 30 | kroma, 31 | zora, 32 | aurora, 33 | canto, 34 | flowMainnet, 35 | 36 | // Testnets 37 | sepolia, 38 | optimismSepolia, 39 | arbitrumSepolia, 40 | baseSepolia, 41 | polygonAmoy, 42 | avalancheFuji, 43 | bscTestnet, 44 | zksyncSepoliaTestnet, 45 | lineaSepolia, 46 | lumiaTestnet, 47 | scrollSepolia, 48 | mantleSepoliaTestnet, 49 | mantaSepoliaTestnet, 50 | blastSepolia, 51 | fraxtalTestnet, 52 | modeTestnet, 53 | metisSepolia, 54 | kromaSepolia, 55 | zoraSepolia, 56 | celoAlfajores, 57 | goerli, 58 | holesky, 59 | flowTestnet, 60 | filecoinCalibration 61 | } from 'viem/chains'; 62 | 63 | // Default configuration values 64 | export const DEFAULT_RPC_URL = 'https://eth.llamarpc.com'; 65 | export const DEFAULT_CHAIN_ID = 1; 66 | 67 | // Map chain IDs to chains 68 | export const chainMap: Record = { 69 | // Mainnets 70 | 1: mainnet, 71 | 10: optimism, 72 | 42161: arbitrum, 73 | 42170: arbitrumNova, 74 | 8453: base, 75 | 137: polygon, 76 | 1101: polygonZkEvm, 77 | 43114: avalanche, 78 | 56: bsc, 79 | 324: zksync, 80 | 59144: linea, 81 | 42220: celo, 82 | 100: gnosis, 83 | 250: fantom, 84 | 314: filecoin, 85 | 1284: moonbeam, 86 | 1285: moonriver, 87 | 25: cronos, 88 | 534352: scroll, 89 | 5000: mantle, 90 | 169: manta, 91 | 994873017: lumiaMainnet, 92 | 81457: blast, 93 | 252: fraxtal, 94 | 34443: mode, 95 | 1088: metis, 96 | 255: kroma, 97 | 7777777: zora, 98 | 1313161554: aurora, 99 | 7700: canto, 100 | 747: flowMainnet, 101 | 102 | // Testnets 103 | 11155111: sepolia, 104 | 11155420: optimismSepolia, 105 | 421614: arbitrumSepolia, 106 | 84532: baseSepolia, 107 | 80002: polygonAmoy, 108 | 43113: avalancheFuji, 109 | 97: bscTestnet, 110 | 300: zksyncSepoliaTestnet, 111 | 59141: lineaSepolia, 112 | 1952959480: lumiaTestnet, 113 | 534351: scrollSepolia, 114 | 5003: mantleSepoliaTestnet, 115 | 3441006: mantaSepoliaTestnet, 116 | 168587773: blastSepolia, 117 | 2522: fraxtalTestnet, 118 | 919: modeTestnet, 119 | 59902: metisSepolia, 120 | 2358: kromaSepolia, 121 | 999999999: zoraSepolia, 122 | 44787: celoAlfajores, 123 | 5: goerli, 124 | 17000: holesky, 125 | 545: flowTestnet, 126 | 314159: filecoinCalibration, 127 | }; 128 | 129 | // Map network names to chain IDs for easier reference 130 | export const networkNameMap: Record = { 131 | // Mainnets 132 | 'ethereum': 1, 133 | 'mainnet': 1, 134 | 'eth': 1, 135 | 'optimism': 10, 136 | 'op': 10, 137 | 'arbitrum': 42161, 138 | 'arb': 42161, 139 | 'arbitrum-nova': 42170, 140 | 'arbitrumnova': 42170, 141 | 'base': 8453, 142 | 'polygon': 137, 143 | 'matic': 137, 144 | 'polygon-zkevm': 1101, 145 | 'polygonzkevm': 1101, 146 | 'avalanche': 43114, 147 | 'avax': 43114, 148 | 'binance': 56, 149 | 'bsc': 56, 150 | 'zksync': 324, 151 | 'linea': 59144, 152 | 'celo': 42220, 153 | 'gnosis': 100, 154 | 'xdai': 100, 155 | 'fantom': 250, 156 | 'ftm': 250, 157 | 'filecoin': 314, 158 | 'fil': 314, 159 | 'moonbeam': 1284, 160 | 'moonriver': 1285, 161 | 'cronos': 25, 162 | 'scroll': 534352, 163 | 'mantle': 5000, 164 | 'manta': 169, 165 | 'lumia': 994873017, 166 | 'blast': 81457, 167 | 'fraxtal': 252, 168 | 'mode': 34443, 169 | 'metis': 1088, 170 | 'kroma': 255, 171 | 'zora': 7777777, 172 | 'aurora': 1313161554, 173 | 'canto': 7700, 174 | 'flow': 747, 175 | 176 | // Testnets 177 | 'sepolia': 11155111, 178 | 'optimism-sepolia': 11155420, 179 | 'optimismsepolia': 11155420, 180 | 'arbitrum-sepolia': 421614, 181 | 'arbitrumsepolia': 421614, 182 | 'base-sepolia': 84532, 183 | 'basesepolia': 84532, 184 | 'polygon-amoy': 80002, 185 | 'polygonamoy': 80002, 186 | 'avalanche-fuji': 43113, 187 | 'avalanchefuji': 43113, 188 | 'fuji': 43113, 189 | 'bsc-testnet': 97, 190 | 'bsctestnet': 97, 191 | 'zksync-sepolia': 300, 192 | 'zksyncsepolia': 300, 193 | 'linea-sepolia': 59141, 194 | 'lineasepolia': 59141, 195 | 'lumia-testnet': 1952959480, 196 | 'scroll-sepolia': 534351, 197 | 'scrollsepolia': 534351, 198 | 'mantle-sepolia': 5003, 199 | 'mantlesepolia': 5003, 200 | 'manta-sepolia': 3441006, 201 | 'mantasepolia': 3441006, 202 | 'blast-sepolia': 168587773, 203 | 'blastsepolia': 168587773, 204 | 'fraxtal-testnet': 2522, 205 | 'fraxtaltestnet': 2522, 206 | 'mode-testnet': 919, 207 | 'modetestnet': 919, 208 | 'metis-sepolia': 59902, 209 | 'metissepolia': 59902, 210 | 'kroma-sepolia': 2358, 211 | 'kromasepolia': 2358, 212 | 'zora-sepolia': 999999999, 213 | 'zorasepolia': 999999999, 214 | 'celo-alfajores': 44787, 215 | 'celoalfajores': 44787, 216 | 'alfajores': 44787, 217 | 'goerli': 5, 218 | 'holesky': 17000, 219 | 'flow-testnet': 545, 220 | 'filecoin-calibration': 314159, 221 | }; 222 | 223 | // Map chain IDs to RPC URLs 224 | export const rpcUrlMap: Record = { 225 | // Mainnets 226 | 1: 'https://eth.llamarpc.com', 227 | 10: 'https://mainnet.optimism.io', 228 | 42161: 'https://arb1.arbitrum.io/rpc', 229 | 42170: 'https://nova.arbitrum.io/rpc', 230 | 8453: 'https://mainnet.base.org', 231 | 137: 'https://polygon-rpc.com', 232 | 1101: 'https://zkevm-rpc.com', 233 | 43114: 'https://api.avax.network/ext/bc/C/rpc', 234 | 56: 'https://bsc-dataseed.binance.org', 235 | 324: 'https://mainnet.era.zksync.io', 236 | 59144: 'https://rpc.linea.build', 237 | 42220: 'https://forno.celo.org', 238 | 100: 'https://rpc.gnosischain.com', 239 | 250: 'https://rpc.ftm.tools', 240 | 314: 'https://api.node.glif.io/rpc/v1', 241 | 1284: 'https://rpc.api.moonbeam.network', 242 | 1285: 'https://rpc.api.moonriver.moonbeam.network', 243 | 25: 'https://evm.cronos.org', 244 | 534352: 'https://rpc.scroll.io', 245 | 5000: 'https://rpc.mantle.xyz', 246 | 169: 'https://pacific-rpc.manta.network/http', 247 | 81457: 'https://rpc.blast.io', 248 | 252: 'https://rpc.frax.com', 249 | 994873017: 'https://mainnet-rpc.lumia.org', 250 | 34443: 'https://mainnet.mode.network', 251 | 1088: 'https://andromeda.metis.io/?owner=1088', 252 | 255: 'https://api.kroma.network', 253 | 7777777: 'https://rpc.zora.energy', 254 | 1313161554: 'https://mainnet.aurora.dev', 255 | 7700: 'https://canto.gravitychain.io', 256 | 747: 'https://mainnet.evm.nodes.onflow.org', 257 | 258 | // Testnets 259 | 11155111: 'https://sepolia.drpc.org', 260 | 11155420: 'https://sepolia.optimism.io', 261 | 421614: 'https://sepolia-rpc.arbitrum.io/rpc', 262 | 84532: 'https://sepolia.base.org', 263 | 80002: 'https://rpc-amoy.polygon.technology', 264 | 43113: 'https://api.avax-test.network/ext/bc/C/rpc', 265 | 97: 'https://data-seed-prebsc-1-s1.binance.org:8545', 266 | 300: 'https://sepolia.era.zksync.dev', 267 | 59141: 'https://rpc.sepolia.linea.build', 268 | 534351: 'https://sepolia-rpc.scroll.io', 269 | 5003: 'https://rpc.sepolia.mantle.xyz', 270 | 3441006: 'https://pacific-rpc.sepolia.manta.network/http', 271 | 1952959480: 'https://testnet-rpc.lumia.org', 272 | 168587773: 'https://sepolia.blast.io', 273 | 2522: 'https://rpc.testnet.frax.com', 274 | 919: 'https://sepolia.mode.network', 275 | 59902: 'https://sepolia.metis.io/?owner=59902', 276 | 2358: 'https://api.sepolia.kroma.network', 277 | 999999999: 'https://sepolia.rpc.zora.energy', 278 | 44787: 'https://alfajores-forno.celo-testnet.org', 279 | 5: 'https://rpc.ankr.com/eth_goerli', 280 | 17000: 'https://ethereum-holesky.publicnode.com', 281 | 545: 'https://testnet.evm.nodes.onflow.org', 282 | 314159: 'https://api.calibration.node.glif.io/rpc/v1', 283 | }; 284 | 285 | /** 286 | * Resolves a chain identifier (number or string) to a chain ID 287 | * @param chainIdentifier Chain ID (number) or network name (string) 288 | * @returns The resolved chain ID 289 | */ 290 | export function resolveChainId(chainIdentifier: number | string): number { 291 | if (typeof chainIdentifier === 'number') { 292 | return chainIdentifier; 293 | } 294 | 295 | // Convert to lowercase for case-insensitive matching 296 | const networkName = chainIdentifier.toLowerCase(); 297 | 298 | // Check if the network name is in our map 299 | if (networkName in networkNameMap) { 300 | return networkNameMap[networkName]; 301 | } 302 | 303 | // Try parsing as a number 304 | const parsedId = parseInt(networkName); 305 | if (!isNaN(parsedId)) { 306 | return parsedId; 307 | } 308 | 309 | // Default to mainnet if not found 310 | return DEFAULT_CHAIN_ID; 311 | } 312 | 313 | /** 314 | * Returns the chain configuration for the specified chain ID or network name 315 | * @param chainIdentifier Chain ID (number) or network name (string) 316 | * @returns The chain configuration 317 | * @throws Error if the network is not supported (when string is provided) 318 | */ 319 | export function getChain(chainIdentifier: number | string = DEFAULT_CHAIN_ID): Chain { 320 | if (typeof chainIdentifier === 'string') { 321 | const networkName = chainIdentifier.toLowerCase(); 322 | // Try to get from direct network name mapping first 323 | if (networkNameMap[networkName]) { 324 | return chainMap[networkNameMap[networkName]] || mainnet; 325 | } 326 | 327 | // If not found, throw an error 328 | throw new Error(`Unsupported network: ${chainIdentifier}`); 329 | } 330 | 331 | // If it's a number, return the chain from chainMap 332 | return chainMap[chainIdentifier] || mainnet; 333 | } 334 | 335 | /** 336 | * Gets the appropriate RPC URL for the specified chain ID or network name 337 | * @param chainIdentifier Chain ID (number) or network name (string) 338 | * @returns The RPC URL for the specified chain 339 | */ 340 | export function getRpcUrl(chainIdentifier: number | string = DEFAULT_CHAIN_ID): string { 341 | const chainId = typeof chainIdentifier === 'string' 342 | ? resolveChainId(chainIdentifier) 343 | : chainIdentifier; 344 | 345 | return rpcUrlMap[chainId] || DEFAULT_RPC_URL; 346 | } 347 | 348 | /** 349 | * Get a list of supported networks 350 | * @returns Array of supported network names (excluding short aliases) 351 | */ 352 | export function getSupportedNetworks(): string[] { 353 | return Object.keys(networkNameMap) 354 | .filter(name => name.length > 2) // Filter out short aliases 355 | .sort(); 356 | } 357 | -------------------------------------------------------------------------------- /src/core/prompts.ts: -------------------------------------------------------------------------------- 1 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; 2 | import { z } from "zod"; 3 | 4 | /** 5 | * Register all EVM-related prompts with the MCP server 6 | * @param server The MCP server instance 7 | */ 8 | export function registerEVMPrompts(server: McpServer) { 9 | // Basic block explorer prompt 10 | server.prompt( 11 | "explore_block", 12 | "Explore information about a specific block", 13 | { 14 | blockNumber: z.string().optional().describe("Block number to explore. If not provided, latest block will be used."), 15 | network: z.string().optional().describe("Network name (e.g., 'ethereum', 'optimism', 'arbitrum', 'base', etc.) or chain ID. Supports all EVM-compatible networks. Defaults to Ethereum mainnet.") 16 | }, 17 | ({ blockNumber, network = "ethereum" }) => ({ 18 | messages: [{ 19 | role: "user", 20 | content: { 21 | type: "text", 22 | text: blockNumber 23 | ? `Please analyze block #${blockNumber} on the ${network} network and provide information about its key metrics, transactions, and significance.` 24 | : `Please analyze the latest block on the ${network} network and provide information about its key metrics, transactions, and significance.` 25 | } 26 | }] 27 | }) 28 | ); 29 | 30 | // Transaction analysis prompt 31 | server.prompt( 32 | "analyze_transaction", 33 | "Analyze a specific transaction", 34 | { 35 | txHash: z.string().describe("Transaction hash to analyze"), 36 | network: z.string().optional().describe("Network name (e.g., 'ethereum', 'optimism', 'arbitrum', 'base', etc.) or chain ID. Supports all EVM-compatible networks. Defaults to Ethereum mainnet.") 37 | }, 38 | ({ txHash, network = "ethereum" }) => ({ 39 | messages: [{ 40 | role: "user", 41 | content: { 42 | type: "text", 43 | text: `Please analyze transaction ${txHash} on the ${network} network and provide a detailed explanation of what this transaction does, who the parties involved are, the amount transferred (if applicable), gas used, and any other relevant information.` 44 | } 45 | }] 46 | }) 47 | ); 48 | 49 | // Address analysis prompt 50 | server.prompt( 51 | "analyze_address", 52 | "Analyze an EVM address", 53 | { 54 | address: z.string().describe("Ethereum address to analyze"), 55 | network: z.string().optional().describe("Network name (e.g., 'ethereum', 'optimism', 'arbitrum', 'base', etc.) or chain ID. Supports all EVM-compatible networks. Defaults to Ethereum mainnet.") 56 | }, 57 | ({ address, network = "ethereum" }) => ({ 58 | messages: [{ 59 | role: "user", 60 | content: { 61 | type: "text", 62 | text: `Please analyze the address ${address} on the ${network} network. Provide information about its balance, transaction count, and any other relevant information you can find.` 63 | } 64 | }] 65 | }) 66 | ); 67 | 68 | // Smart contract interaction guidance 69 | server.prompt( 70 | "interact_with_contract", 71 | "Get guidance on interacting with a smart contract", 72 | { 73 | contractAddress: z.string().describe("The contract address"), 74 | abiJson: z.string().optional().describe("The contract ABI as a JSON string"), 75 | network: z.string().optional().describe("Network name or chain ID. Defaults to Ethereum mainnet.") 76 | }, 77 | ({ contractAddress, abiJson, network = "ethereum" }) => ({ 78 | messages: [{ 79 | role: "user", 80 | content: { 81 | type: "text", 82 | text: abiJson 83 | ? `I need to interact with the smart contract at address ${contractAddress} on the ${network} network. Here's the ABI:\n\n${abiJson}\n\nPlease analyze this contract's functions and provide guidance on how to interact with it safely. Explain what each function does and what parameters it requires.` 84 | : `I need to interact with the smart contract at address ${contractAddress} on the ${network} network. Please help me understand what this contract does and how I can interact with it safely.` 85 | } 86 | }] 87 | }) 88 | ); 89 | 90 | // EVM concept explanation 91 | server.prompt( 92 | "explain_evm_concept", 93 | "Get an explanation of an EVM concept", 94 | { 95 | concept: z.string().describe("The EVM concept to explain (e.g., gas, nonce, etc.)") 96 | }, 97 | ({ concept }) => ({ 98 | messages: [{ 99 | role: "user", 100 | content: { 101 | type: "text", 102 | text: `Please explain the EVM Blockchain concept of "${concept}" in detail. Include how it works, why it's important, and provide examples if applicable.` 103 | } 104 | }] 105 | }) 106 | ); 107 | 108 | // Network comparison 109 | server.prompt( 110 | "compare_networks", 111 | "Compare different EVM-compatible networks", 112 | { 113 | networkList: z.string().describe("Comma-separated list of networks to compare (e.g., 'ethereum,optimism,arbitrum')") 114 | }, 115 | ({ networkList }) => { 116 | const networks = networkList.split(',').map(n => n.trim()); 117 | return { 118 | messages: [{ 119 | role: "user", 120 | content: { 121 | type: "text", 122 | text: `Please compare the following EVM-compatible networks: ${networks.join(', ')}. Include information about their architecture, gas fees, transaction speed, security, and any other relevant differences.` 123 | } 124 | }] 125 | }; 126 | } 127 | ); 128 | 129 | // Token analysis prompt 130 | server.prompt( 131 | "analyze_token", 132 | "Analyze an ERC20 or NFT token", 133 | { 134 | tokenAddress: z.string().describe("Token contract address to analyze"), 135 | tokenType: z.string().optional().describe("Type of token to analyze (erc20, erc721/nft, or auto-detect). Defaults to auto."), 136 | tokenId: z.string().optional().describe("Token ID (required for NFT analysis)"), 137 | network: z.string().optional().describe("Network name (e.g., 'ethereum', 'optimism', 'arbitrum', 'base', etc.) or chain ID. Supports all EVM-compatible networks. Defaults to Ethereum mainnet.") 138 | }, 139 | ({ tokenAddress, tokenType = "auto", tokenId, network = "ethereum" }) => { 140 | let promptText = ""; 141 | 142 | if (tokenType === "erc20" || tokenType === "auto") { 143 | promptText = `Please analyze the ERC20 token at address ${tokenAddress} on the ${network} network. Provide information about its name, symbol, total supply, and any other relevant details. If possible, explain the token's purpose, utility, and market context.`; 144 | } else if ((tokenType === "erc721" || tokenType === "nft") && tokenId) { 145 | promptText = `Please analyze the NFT with token ID ${tokenId} from the collection at address ${tokenAddress} on the ${network} network. Provide information about the collection name, token details, ownership history if available, and any other relevant information about this specific NFT.`; 146 | } else if (tokenType === "nft" || tokenType === "erc721") { 147 | promptText = `Please analyze the NFT collection at address ${tokenAddress} on the ${network} network. Provide information about the collection name, symbol, total supply if available, floor price if available, and any other relevant details about this NFT collection.`; 148 | } 149 | 150 | return { 151 | messages: [{ 152 | role: "user", 153 | content: { 154 | type: "text", 155 | text: promptText 156 | } 157 | }] 158 | }; 159 | } 160 | ); 161 | 162 | } -------------------------------------------------------------------------------- /src/core/resources.ts: -------------------------------------------------------------------------------- 1 | import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js"; 2 | import { getSupportedNetworks, getRpcUrl } from "./chains.js"; 3 | import * as services from "./services/index.js"; 4 | import type { Address, Hash } from "viem"; 5 | 6 | /** 7 | * Register all EVM-related resources 8 | * @param server The MCP server instance 9 | */ 10 | export function registerEVMResources(server: McpServer) { 11 | // Get EVM info for a specific network 12 | server.resource( 13 | "chain_info_by_network", 14 | new ResourceTemplate("evm://{network}/chain", { list: undefined }), 15 | async (uri, params) => { 16 | try { 17 | const network = params.network as string; 18 | const chainId = await services.getChainId(network); 19 | const blockNumber = await services.getBlockNumber(network); 20 | const rpcUrl = getRpcUrl(network); 21 | 22 | return { 23 | contents: [{ 24 | uri: uri.href, 25 | text: JSON.stringify({ 26 | network, 27 | chainId, 28 | blockNumber: blockNumber.toString(), 29 | rpcUrl 30 | }, null, 2) 31 | }] 32 | }; 33 | } catch (error) { 34 | return { 35 | contents: [{ 36 | uri: uri.href, 37 | text: `Error fetching chain info: ${error instanceof Error ? error.message : String(error)}` 38 | }] 39 | }; 40 | } 41 | } 42 | ); 43 | 44 | // Default chain info (Ethereum mainnet) 45 | server.resource( 46 | "ethereum_chain_info", 47 | "evm://chain", 48 | async (uri) => { 49 | try { 50 | const network = "ethereum"; 51 | const chainId = await services.getChainId(network); 52 | const blockNumber = await services.getBlockNumber(network); 53 | const rpcUrl = getRpcUrl(network); 54 | 55 | return { 56 | contents: [{ 57 | uri: uri.href, 58 | text: JSON.stringify({ 59 | network, 60 | chainId, 61 | blockNumber: blockNumber.toString(), 62 | rpcUrl 63 | }, null, 2) 64 | }] 65 | }; 66 | } catch (error) { 67 | return { 68 | contents: [{ 69 | uri: uri.href, 70 | text: `Error fetching chain info: ${error instanceof Error ? error.message : String(error)}` 71 | }] 72 | }; 73 | } 74 | } 75 | ); 76 | 77 | // Get block by number for a specific network 78 | server.resource( 79 | "evm_block_by_number", 80 | new ResourceTemplate("evm://{network}/block/{blockNumber}", { list: undefined }), 81 | async (uri, params) => { 82 | try { 83 | const network = params.network as string; 84 | const blockNumber = params.blockNumber as string; 85 | const block = await services.getBlockByNumber(parseInt(blockNumber), network); 86 | 87 | return { 88 | contents: [{ 89 | uri: uri.href, 90 | text: services.helpers.formatJson(block) 91 | }] 92 | }; 93 | } catch (error) { 94 | return { 95 | contents: [{ 96 | uri: uri.href, 97 | text: `Error fetching block: ${error instanceof Error ? error.message : String(error)}` 98 | }] 99 | }; 100 | } 101 | } 102 | ); 103 | 104 | // Get block by hash for a specific network 105 | server.resource( 106 | "block_by_hash", 107 | new ResourceTemplate("evm://{network}/block/hash/{blockHash}", { list: undefined }), 108 | async (uri, params) => { 109 | try { 110 | const network = params.network as string; 111 | const blockHash = params.blockHash as string; 112 | const block = await services.getBlockByHash(blockHash as Hash, network); 113 | 114 | return { 115 | contents: [{ 116 | uri: uri.href, 117 | text: services.helpers.formatJson(block) 118 | }] 119 | }; 120 | } catch (error) { 121 | return { 122 | contents: [{ 123 | uri: uri.href, 124 | text: `Error fetching block with hash: ${error instanceof Error ? error.message : String(error)}` 125 | }] 126 | }; 127 | } 128 | } 129 | ); 130 | 131 | // Get latest block for a specific network 132 | server.resource( 133 | "evm_latest_block", 134 | new ResourceTemplate("evm://{network}/block/latest", { list: undefined }), 135 | async (uri, params) => { 136 | try { 137 | const network = params.network as string; 138 | const block = await services.getLatestBlock(network); 139 | 140 | return { 141 | contents: [{ 142 | uri: uri.href, 143 | text: services.helpers.formatJson(block) 144 | }] 145 | }; 146 | } catch (error) { 147 | return { 148 | contents: [{ 149 | uri: uri.href, 150 | text: `Error fetching latest block: ${error instanceof Error ? error.message : String(error)}` 151 | }] 152 | }; 153 | } 154 | } 155 | ); 156 | 157 | // Default latest block (Ethereum mainnet) 158 | server.resource( 159 | "default_latest_block", 160 | "evm://block/latest", 161 | async (uri) => { 162 | try { 163 | const network = "ethereum"; 164 | const block = await services.getLatestBlock(network); 165 | 166 | return { 167 | contents: [{ 168 | uri: uri.href, 169 | text: services.helpers.formatJson(block) 170 | }] 171 | }; 172 | } catch (error) { 173 | return { 174 | contents: [{ 175 | uri: uri.href, 176 | text: `Error fetching latest block: ${error instanceof Error ? error.message : String(error)}` 177 | }] 178 | }; 179 | } 180 | } 181 | ); 182 | 183 | // Get ETH balance for a specific network 184 | server.resource( 185 | "evm_address_native_balance", 186 | new ResourceTemplate("evm://{network}/address/{address}/balance", { list: undefined }), 187 | async (uri, params) => { 188 | try { 189 | const network = params.network as string; 190 | const address = params.address as string; 191 | const balance = await services.getETHBalance(address as Address, network); 192 | 193 | return { 194 | contents: [{ 195 | uri: uri.href, 196 | text: JSON.stringify({ 197 | network, 198 | address, 199 | balance: { 200 | wei: balance.wei.toString(), 201 | ether: balance.ether 202 | } 203 | }, null, 2) 204 | }] 205 | }; 206 | } catch (error) { 207 | return { 208 | contents: [{ 209 | uri: uri.href, 210 | text: `Error fetching ETH balance: ${error instanceof Error ? error.message : String(error)}` 211 | }] 212 | }; 213 | } 214 | } 215 | ); 216 | 217 | // Default ETH balance (Ethereum mainnet) 218 | server.resource( 219 | "default_eth_balance", 220 | new ResourceTemplate("evm://address/{address}/eth-balance", { list: undefined }), 221 | async (uri, params) => { 222 | try { 223 | const network = "ethereum"; 224 | const address = params.address as string; 225 | const balance = await services.getETHBalance(address as Address, network); 226 | 227 | return { 228 | contents: [{ 229 | uri: uri.href, 230 | text: JSON.stringify({ 231 | network, 232 | address, 233 | balance: { 234 | wei: balance.wei.toString(), 235 | ether: balance.ether 236 | } 237 | }, null, 2) 238 | }] 239 | }; 240 | } catch (error) { 241 | return { 242 | contents: [{ 243 | uri: uri.href, 244 | text: `Error fetching ETH balance: ${error instanceof Error ? error.message : String(error)}` 245 | }] 246 | }; 247 | } 248 | } 249 | ); 250 | 251 | // Get ERC20 balance for a specific network 252 | server.resource( 253 | "erc20_balance", 254 | new ResourceTemplate("evm://{network}/address/{address}/token/{tokenAddress}/balance", { list: undefined }), 255 | async (uri, params) => { 256 | try { 257 | const network = params.network as string; 258 | const address = params.address as string; 259 | const tokenAddress = params.tokenAddress as string; 260 | 261 | const balance = await services.getERC20Balance( 262 | tokenAddress as Address, 263 | address as Address, 264 | network 265 | ); 266 | 267 | return { 268 | contents: [{ 269 | uri: uri.href, 270 | text: JSON.stringify({ 271 | network, 272 | address, 273 | tokenAddress, 274 | balance: { 275 | raw: balance.raw.toString(), 276 | formatted: balance.formatted, 277 | decimals: balance.token.decimals 278 | } 279 | }, null, 2) 280 | }] 281 | }; 282 | } catch (error) { 283 | return { 284 | contents: [{ 285 | uri: uri.href, 286 | text: `Error fetching ERC20 balance: ${error instanceof Error ? error.message : String(error)}` 287 | }] 288 | }; 289 | } 290 | } 291 | ); 292 | 293 | // Default ERC20 balance (Ethereum mainnet) 294 | server.resource( 295 | "default_erc20_balance", 296 | new ResourceTemplate("evm://address/{address}/token/{tokenAddress}/balance", { list: undefined }), 297 | async (uri, params) => { 298 | try { 299 | const network = "ethereum"; 300 | const address = params.address as string; 301 | const tokenAddress = params.tokenAddress as string; 302 | 303 | const balance = await services.getERC20Balance( 304 | tokenAddress as Address, 305 | address as Address, 306 | network 307 | ); 308 | 309 | return { 310 | contents: [{ 311 | uri: uri.href, 312 | text: JSON.stringify({ 313 | network, 314 | address, 315 | tokenAddress, 316 | balance: { 317 | raw: balance.raw.toString(), 318 | formatted: balance.formatted, 319 | decimals: balance.token.decimals 320 | } 321 | }, null, 2) 322 | }] 323 | }; 324 | } catch (error) { 325 | return { 326 | contents: [{ 327 | uri: uri.href, 328 | text: `Error fetching ERC20 balance: ${error instanceof Error ? error.message : String(error)}` 329 | }] 330 | }; 331 | } 332 | } 333 | ); 334 | 335 | // Get transaction by hash for a specific network 336 | server.resource( 337 | "evm_transaction_details", 338 | new ResourceTemplate("evm://{network}/tx/{txHash}", { list: undefined }), 339 | async (uri, params) => { 340 | try { 341 | const network = params.network as string; 342 | const txHash = params.txHash as string; 343 | const tx = await services.getTransaction(txHash as Hash, network); 344 | 345 | return { 346 | contents: [{ 347 | uri: uri.href, 348 | text: services.helpers.formatJson(tx) 349 | }] 350 | }; 351 | } catch (error) { 352 | return { 353 | contents: [{ 354 | uri: uri.href, 355 | text: `Error fetching transaction: ${error instanceof Error ? error.message : String(error)}` 356 | }] 357 | }; 358 | } 359 | } 360 | ); 361 | 362 | // Default transaction by hash (Ethereum mainnet) 363 | server.resource( 364 | "default_transaction_by_hash", 365 | new ResourceTemplate("evm://tx/{txHash}", { list: undefined }), 366 | async (uri, params) => { 367 | try { 368 | const network = "ethereum"; 369 | const txHash = params.txHash as string; 370 | const tx = await services.getTransaction(txHash as Hash, network); 371 | 372 | return { 373 | contents: [{ 374 | uri: uri.href, 375 | text: services.helpers.formatJson(tx) 376 | }] 377 | }; 378 | } catch (error) { 379 | return { 380 | contents: [{ 381 | uri: uri.href, 382 | text: `Error fetching transaction: ${error instanceof Error ? error.message : String(error)}` 383 | }] 384 | }; 385 | } 386 | } 387 | ); 388 | 389 | // Get supported networks 390 | server.resource( 391 | "supported_networks", 392 | "evm://networks", 393 | async (uri) => { 394 | try { 395 | const networks = getSupportedNetworks(); 396 | 397 | return { 398 | contents: [{ 399 | uri: uri.href, 400 | text: JSON.stringify({ 401 | supportedNetworks: networks 402 | }, null, 2) 403 | }] 404 | }; 405 | } catch (error) { 406 | return { 407 | contents: [{ 408 | uri: uri.href, 409 | text: `Error fetching supported networks: ${error instanceof Error ? error.message : String(error)}` 410 | }] 411 | }; 412 | } 413 | } 414 | ); 415 | 416 | // Add ERC20 token info resource 417 | server.resource( 418 | "erc20_token_details", 419 | new ResourceTemplate("evm://{network}/token/{tokenAddress}", { list: undefined }), 420 | async (uri, params) => { 421 | try { 422 | const network = params.network as string; 423 | const tokenAddress = params.tokenAddress as Address; 424 | 425 | const tokenInfo = await services.getERC20TokenInfo(tokenAddress, network); 426 | 427 | return { 428 | contents: [{ 429 | uri: uri.href, 430 | text: JSON.stringify({ 431 | address: tokenAddress, 432 | network, 433 | ...tokenInfo 434 | }, null, 2) 435 | }] 436 | }; 437 | } catch (error) { 438 | return { 439 | contents: [{ 440 | uri: uri.href, 441 | text: `Error fetching ERC20 token info: ${error instanceof Error ? error.message : String(error)}` 442 | }] 443 | }; 444 | } 445 | } 446 | ); 447 | 448 | // Add ERC20 token balance resource 449 | server.resource( 450 | "erc20_token_address_balance", 451 | new ResourceTemplate("evm://{network}/token/{tokenAddress}/balanceOf/{address}", { list: undefined }), 452 | async (uri, params) => { 453 | try { 454 | const network = params.network as string; 455 | const tokenAddress = params.tokenAddress as Address; 456 | const address = params.address as Address; 457 | 458 | const balance = await services.getERC20Balance(tokenAddress, address, network); 459 | 460 | return { 461 | contents: [{ 462 | uri: uri.href, 463 | text: JSON.stringify({ 464 | tokenAddress, 465 | owner: address, 466 | network, 467 | raw: balance.raw.toString(), 468 | formatted: balance.formatted, 469 | symbol: balance.token.symbol, 470 | decimals: balance.token.decimals 471 | }, null, 2) 472 | }] 473 | }; 474 | } catch (error) { 475 | return { 476 | contents: [{ 477 | uri: uri.href, 478 | text: `Error fetching ERC20 token balance: ${error instanceof Error ? error.message : String(error)}` 479 | }] 480 | }; 481 | } 482 | } 483 | ); 484 | 485 | // Add NFT (ERC721) token info resource 486 | server.resource( 487 | "erc721_nft_token_details", 488 | new ResourceTemplate("evm://{network}/nft/{tokenAddress}/{tokenId}", { list: undefined }), 489 | async (uri, params) => { 490 | try { 491 | const network = params.network as string; 492 | const tokenAddress = params.tokenAddress as Address; 493 | const tokenId = BigInt(params.tokenId as string); 494 | 495 | const nftInfo = await services.getERC721TokenMetadata(tokenAddress, tokenId, network); 496 | 497 | // Get owner separately 498 | let owner = "Unknown"; 499 | try { 500 | const isOwner = await services.isNFTOwner(tokenAddress, params.address as Address, tokenId, network); 501 | if (isOwner) { 502 | owner = params.address as string; 503 | } 504 | } catch (e) { 505 | // Owner info not available 506 | } 507 | 508 | return { 509 | contents: [{ 510 | uri: uri.href, 511 | text: JSON.stringify({ 512 | contract: tokenAddress, 513 | tokenId: tokenId.toString(), 514 | network, 515 | ...nftInfo, 516 | owner 517 | }, null, 2) 518 | }] 519 | }; 520 | } catch (error) { 521 | return { 522 | contents: [{ 523 | uri: uri.href, 524 | text: `Error fetching NFT info: ${error instanceof Error ? error.message : String(error)}` 525 | }] 526 | }; 527 | } 528 | } 529 | ); 530 | 531 | // Add NFT ownership check resource 532 | server.resource( 533 | "erc721_nft_ownership_check", 534 | new ResourceTemplate("evm://{network}/nft/{tokenAddress}/{tokenId}/isOwnedBy/{address}", { list: undefined }), 535 | async (uri, params) => { 536 | try { 537 | const network = params.network as string; 538 | const tokenAddress = params.tokenAddress as Address; 539 | const tokenId = BigInt(params.tokenId as string); 540 | const address = params.address as Address; 541 | 542 | const isOwner = await services.isNFTOwner(tokenAddress, address, tokenId, network); 543 | 544 | return { 545 | contents: [{ 546 | uri: uri.href, 547 | text: JSON.stringify({ 548 | contract: tokenAddress, 549 | tokenId: tokenId.toString(), 550 | owner: address, 551 | network, 552 | isOwner 553 | }, null, 2) 554 | }] 555 | }; 556 | } catch (error) { 557 | return { 558 | contents: [{ 559 | uri: uri.href, 560 | text: `Error checking NFT ownership: ${error instanceof Error ? error.message : String(error)}` 561 | }] 562 | }; 563 | } 564 | } 565 | ); 566 | 567 | // Add ERC1155 token URI resource 568 | server.resource( 569 | "erc1155_token_metadata_uri", 570 | new ResourceTemplate("evm://{network}/erc1155/{tokenAddress}/{tokenId}/uri", { list: undefined }), 571 | async (uri, params) => { 572 | try { 573 | const network = params.network as string; 574 | const tokenAddress = params.tokenAddress as Address; 575 | const tokenId = BigInt(params.tokenId as string); 576 | 577 | const tokenURI = await services.getERC1155TokenURI(tokenAddress, tokenId, network); 578 | 579 | return { 580 | contents: [{ 581 | uri: uri.href, 582 | text: JSON.stringify({ 583 | contract: tokenAddress, 584 | tokenId: tokenId.toString(), 585 | network, 586 | uri: tokenURI 587 | }, null, 2) 588 | }] 589 | }; 590 | } catch (error) { 591 | return { 592 | contents: [{ 593 | uri: uri.href, 594 | text: `Error fetching ERC1155 token URI: ${error instanceof Error ? error.message : String(error)}` 595 | }] 596 | }; 597 | } 598 | } 599 | ); 600 | 601 | // Add ERC1155 token balance resource 602 | server.resource( 603 | "erc1155_token_address_balance", 604 | new ResourceTemplate("evm://{network}/erc1155/{tokenAddress}/{tokenId}/balanceOf/{address}", { list: undefined }), 605 | async (uri, params) => { 606 | try { 607 | const network = params.network as string; 608 | const tokenAddress = params.tokenAddress as Address; 609 | const tokenId = BigInt(params.tokenId as string); 610 | const address = params.address as Address; 611 | 612 | const balance = await services.getERC1155Balance(tokenAddress, address, tokenId, network); 613 | 614 | return { 615 | contents: [{ 616 | uri: uri.href, 617 | text: JSON.stringify({ 618 | contract: tokenAddress, 619 | tokenId: tokenId.toString(), 620 | owner: address, 621 | network, 622 | balance: balance.toString() 623 | }, null, 2) 624 | }] 625 | }; 626 | } catch (error) { 627 | return { 628 | contents: [{ 629 | uri: uri.href, 630 | text: `Error fetching ERC1155 token balance: ${error instanceof Error ? error.message : String(error)}` 631 | }] 632 | }; 633 | } 634 | } 635 | ); 636 | } -------------------------------------------------------------------------------- /src/core/services/balance.ts: -------------------------------------------------------------------------------- 1 | import { 2 | formatEther, 3 | formatUnits, 4 | type Address, 5 | type Abi, 6 | getContract 7 | } from 'viem'; 8 | import { getPublicClient } from './clients.js'; 9 | import { readContract } from './contracts.js'; 10 | import { resolveAddress } from './ens.js'; 11 | 12 | // Standard ERC20 ABI (minimal for reading) 13 | const erc20Abi = [ 14 | { 15 | inputs: [], 16 | name: 'symbol', 17 | outputs: [{ type: 'string' }], 18 | stateMutability: 'view', 19 | type: 'function' 20 | }, 21 | { 22 | inputs: [], 23 | name: 'decimals', 24 | outputs: [{ type: 'uint8' }], 25 | stateMutability: 'view', 26 | type: 'function' 27 | }, 28 | { 29 | inputs: [{ type: 'address', name: 'account' }], 30 | name: 'balanceOf', 31 | outputs: [{ type: 'uint256' }], 32 | stateMutability: 'view', 33 | type: 'function' 34 | } 35 | ] as const; 36 | 37 | // Standard ERC721 ABI (minimal for reading) 38 | const erc721Abi = [ 39 | { 40 | inputs: [{ type: 'address', name: 'owner' }], 41 | name: 'balanceOf', 42 | outputs: [{ type: 'uint256' }], 43 | stateMutability: 'view', 44 | type: 'function' 45 | }, 46 | { 47 | inputs: [{ type: 'uint256', name: 'tokenId' }], 48 | name: 'ownerOf', 49 | outputs: [{ type: 'address' }], 50 | stateMutability: 'view', 51 | type: 'function' 52 | } 53 | ] as const; 54 | 55 | // Standard ERC1155 ABI (minimal for reading) 56 | const erc1155Abi = [ 57 | { 58 | inputs: [ 59 | { type: 'address', name: 'account' }, 60 | { type: 'uint256', name: 'id' } 61 | ], 62 | name: 'balanceOf', 63 | outputs: [{ type: 'uint256' }], 64 | stateMutability: 'view', 65 | type: 'function' 66 | } 67 | ] as const; 68 | 69 | /** 70 | * Get the ETH balance for an address 71 | * @param addressOrEns Ethereum address or ENS name 72 | * @param network Network name or chain ID 73 | * @returns Balance in wei and ether 74 | */ 75 | export async function getETHBalance( 76 | addressOrEns: string, 77 | network = 'ethereum' 78 | ): Promise<{ wei: bigint; ether: string }> { 79 | // Resolve ENS name to address if needed 80 | const address = await resolveAddress(addressOrEns, network); 81 | 82 | const client = getPublicClient(network); 83 | const balance = await client.getBalance({ address }); 84 | 85 | return { 86 | wei: balance, 87 | ether: formatEther(balance) 88 | }; 89 | } 90 | 91 | /** 92 | * Get the balance of an ERC20 token for an address 93 | * @param tokenAddressOrEns Token contract address or ENS name 94 | * @param ownerAddressOrEns Owner address or ENS name 95 | * @param network Network name or chain ID 96 | * @returns Token balance with formatting information 97 | */ 98 | export async function getERC20Balance( 99 | tokenAddressOrEns: string, 100 | ownerAddressOrEns: string, 101 | network = 'ethereum' 102 | ): Promise<{ 103 | raw: bigint; 104 | formatted: string; 105 | token: { 106 | symbol: string; 107 | decimals: number; 108 | } 109 | }> { 110 | // Resolve ENS names to addresses if needed 111 | const tokenAddress = await resolveAddress(tokenAddressOrEns, network); 112 | const ownerAddress = await resolveAddress(ownerAddressOrEns, network); 113 | 114 | const publicClient = getPublicClient(network); 115 | 116 | const contract = getContract({ 117 | address: tokenAddress, 118 | abi: erc20Abi, 119 | client: publicClient, 120 | }); 121 | 122 | const [balance, symbol, decimals] = await Promise.all([ 123 | contract.read.balanceOf([ownerAddress]), 124 | contract.read.symbol(), 125 | contract.read.decimals() 126 | ]); 127 | 128 | return { 129 | raw: balance, 130 | formatted: formatUnits(balance, decimals), 131 | token: { 132 | symbol, 133 | decimals 134 | } 135 | }; 136 | } 137 | 138 | /** 139 | * Check if an address owns a specific NFT 140 | * @param tokenAddressOrEns NFT contract address or ENS name 141 | * @param ownerAddressOrEns Owner address or ENS name 142 | * @param tokenId Token ID to check 143 | * @param network Network name or chain ID 144 | * @returns True if the address owns the NFT 145 | */ 146 | export async function isNFTOwner( 147 | tokenAddressOrEns: string, 148 | ownerAddressOrEns: string, 149 | tokenId: bigint, 150 | network = 'ethereum' 151 | ): Promise { 152 | // Resolve ENS names to addresses if needed 153 | const tokenAddress = await resolveAddress(tokenAddressOrEns, network); 154 | const ownerAddress = await resolveAddress(ownerAddressOrEns, network); 155 | 156 | try { 157 | const actualOwner = await readContract({ 158 | address: tokenAddress, 159 | abi: erc721Abi, 160 | functionName: 'ownerOf', 161 | args: [tokenId] 162 | }, network) as Address; 163 | 164 | return actualOwner.toLowerCase() === ownerAddress.toLowerCase(); 165 | } catch (error: any) { 166 | console.error(`Error checking NFT ownership: ${error.message}`); 167 | return false; 168 | } 169 | } 170 | 171 | /** 172 | * Get the number of NFTs owned by an address for a specific collection 173 | * @param tokenAddressOrEns NFT contract address or ENS name 174 | * @param ownerAddressOrEns Owner address or ENS name 175 | * @param network Network name or chain ID 176 | * @returns Number of NFTs owned 177 | */ 178 | export async function getERC721Balance( 179 | tokenAddressOrEns: string, 180 | ownerAddressOrEns: string, 181 | network = 'ethereum' 182 | ): Promise { 183 | // Resolve ENS names to addresses if needed 184 | const tokenAddress = await resolveAddress(tokenAddressOrEns, network); 185 | const ownerAddress = await resolveAddress(ownerAddressOrEns, network); 186 | 187 | return readContract({ 188 | address: tokenAddress, 189 | abi: erc721Abi, 190 | functionName: 'balanceOf', 191 | args: [ownerAddress] 192 | }, network) as Promise; 193 | } 194 | 195 | /** 196 | * Get the balance of an ERC1155 token for an address 197 | * @param tokenAddressOrEns ERC1155 contract address or ENS name 198 | * @param ownerAddressOrEns Owner address or ENS name 199 | * @param tokenId Token ID to check 200 | * @param network Network name or chain ID 201 | * @returns Token balance 202 | */ 203 | export async function getERC1155Balance( 204 | tokenAddressOrEns: string, 205 | ownerAddressOrEns: string, 206 | tokenId: bigint, 207 | network = 'ethereum' 208 | ): Promise { 209 | // Resolve ENS names to addresses if needed 210 | const tokenAddress = await resolveAddress(tokenAddressOrEns, network); 211 | const ownerAddress = await resolveAddress(ownerAddressOrEns, network); 212 | 213 | return readContract({ 214 | address: tokenAddress, 215 | abi: erc1155Abi, 216 | functionName: 'balanceOf', 217 | args: [ownerAddress, tokenId] 218 | }, network) as Promise; 219 | } -------------------------------------------------------------------------------- /src/core/services/blocks.ts: -------------------------------------------------------------------------------- 1 | import { 2 | type Hash, 3 | type Block 4 | } from 'viem'; 5 | import { getPublicClient } from './clients.js'; 6 | 7 | /** 8 | * Get the current block number for a specific network 9 | */ 10 | export async function getBlockNumber(network = 'ethereum'): Promise { 11 | const client = getPublicClient(network); 12 | return await client.getBlockNumber(); 13 | } 14 | 15 | /** 16 | * Get a block by number for a specific network 17 | */ 18 | export async function getBlockByNumber( 19 | blockNumber: number, 20 | network = 'ethereum' 21 | ): Promise { 22 | const client = getPublicClient(network); 23 | return await client.getBlock({ blockNumber: BigInt(blockNumber) }); 24 | } 25 | 26 | /** 27 | * Get a block by hash for a specific network 28 | */ 29 | export async function getBlockByHash( 30 | blockHash: Hash, 31 | network = 'ethereum' 32 | ): Promise { 33 | const client = getPublicClient(network); 34 | return await client.getBlock({ blockHash }); 35 | } 36 | 37 | /** 38 | * Get the latest block for a specific network 39 | */ 40 | export async function getLatestBlock(network = 'ethereum'): Promise { 41 | const client = getPublicClient(network); 42 | return await client.getBlock(); 43 | } -------------------------------------------------------------------------------- /src/core/services/clients.ts: -------------------------------------------------------------------------------- 1 | import { 2 | createPublicClient, 3 | createWalletClient, 4 | http, 5 | type PublicClient, 6 | type WalletClient, 7 | type Hex, 8 | type Address 9 | } from 'viem'; 10 | import { privateKeyToAccount } from 'viem/accounts'; 11 | import { getChain, getRpcUrl } from '../chains.js'; 12 | 13 | // Cache for clients to avoid recreating them for each request 14 | const clientCache = new Map(); 15 | 16 | /** 17 | * Get a public client for a specific network 18 | */ 19 | export function getPublicClient(network = 'ethereum'): PublicClient { 20 | const cacheKey = String(network); 21 | 22 | // Return cached client if available 23 | if (clientCache.has(cacheKey)) { 24 | return clientCache.get(cacheKey)!; 25 | } 26 | 27 | // Create a new client 28 | const chain = getChain(network); 29 | const rpcUrl = getRpcUrl(network); 30 | 31 | const client = createPublicClient({ 32 | chain, 33 | transport: http(rpcUrl) 34 | }); 35 | 36 | // Cache the client 37 | clientCache.set(cacheKey, client); 38 | 39 | return client; 40 | } 41 | 42 | /** 43 | * Create a wallet client for a specific network and private key 44 | */ 45 | export function getWalletClient(privateKey: Hex, network = 'ethereum'): WalletClient { 46 | const chain = getChain(network); 47 | const rpcUrl = getRpcUrl(network); 48 | const account = privateKeyToAccount(privateKey); 49 | 50 | return createWalletClient({ 51 | account, 52 | chain, 53 | transport: http(rpcUrl) 54 | }); 55 | } 56 | 57 | /** 58 | * Get an Ethereum address from a private key 59 | * @param privateKey The private key in hex format (with or without 0x prefix) 60 | * @returns The Ethereum address derived from the private key 61 | */ 62 | export function getAddressFromPrivateKey(privateKey: Hex): Address { 63 | const account = privateKeyToAccount(privateKey); 64 | return account.address; 65 | } -------------------------------------------------------------------------------- /src/core/services/contracts.ts: -------------------------------------------------------------------------------- 1 | import { 2 | type Address, 3 | type Hash, 4 | type Hex, 5 | type ReadContractParameters, 6 | type GetLogsParameters, 7 | type Log 8 | } from 'viem'; 9 | import { getPublicClient, getWalletClient } from './clients.js'; 10 | import { resolveAddress } from './ens.js'; 11 | 12 | /** 13 | * Read from a contract for a specific network 14 | */ 15 | export async function readContract(params: ReadContractParameters, network = 'ethereum') { 16 | const client = getPublicClient(network); 17 | return await client.readContract(params); 18 | } 19 | 20 | /** 21 | * Write to a contract for a specific network 22 | */ 23 | export async function writeContract( 24 | privateKey: Hex, 25 | params: Record, 26 | network = 'ethereum' 27 | ): Promise { 28 | const client = getWalletClient(privateKey, network); 29 | return await client.writeContract(params as any); 30 | } 31 | 32 | /** 33 | * Get logs for a specific network 34 | */ 35 | export async function getLogs(params: GetLogsParameters, network = 'ethereum'): Promise { 36 | const client = getPublicClient(network); 37 | return await client.getLogs(params); 38 | } 39 | 40 | /** 41 | * Check if an address is a contract 42 | * @param addressOrEns Address or ENS name to check 43 | * @param network Network name or chain ID 44 | * @returns True if the address is a contract, false if it's an EOA 45 | */ 46 | export async function isContract(addressOrEns: string, network = 'ethereum'): Promise { 47 | // Resolve ENS name to address if needed 48 | const address = await resolveAddress(addressOrEns, network); 49 | 50 | const client = getPublicClient(network); 51 | const code = await client.getBytecode({ address }); 52 | return code !== undefined && code !== '0x'; 53 | } -------------------------------------------------------------------------------- /src/core/services/ens.ts: -------------------------------------------------------------------------------- 1 | import { normalize } from 'viem/ens'; 2 | import { getPublicClient } from './clients.js'; 3 | import { type Address } from 'viem'; 4 | 5 | /** 6 | * Resolves an ENS name to an Ethereum address or returns the original address if it's already valid 7 | * @param addressOrEns An Ethereum address or ENS name 8 | * @param network The network to use for ENS resolution (defaults to Ethereum mainnet) 9 | * @returns The resolved Ethereum address 10 | */ 11 | export async function resolveAddress( 12 | addressOrEns: string, 13 | network = 'ethereum' 14 | ): Promise
{ 15 | // If it's already a valid Ethereum address (0x followed by 40 hex chars), return it 16 | if (/^0x[a-fA-F0-9]{40}$/.test(addressOrEns)) { 17 | return addressOrEns as Address; 18 | } 19 | 20 | // If it looks like an ENS name (contains a dot), try to resolve it 21 | if (addressOrEns.includes('.')) { 22 | try { 23 | // Normalize the ENS name first 24 | const normalizedEns = normalize(addressOrEns); 25 | 26 | // Get the public client for the network 27 | const publicClient = getPublicClient(network); 28 | 29 | // Resolve the ENS name to an address 30 | const address = await publicClient.getEnsAddress({ 31 | name: normalizedEns, 32 | }); 33 | 34 | if (!address) { 35 | throw new Error(`ENS name ${addressOrEns} could not be resolved to an address`); 36 | } 37 | 38 | return address; 39 | } catch (error: any) { 40 | throw new Error(`Failed to resolve ENS name ${addressOrEns}: ${error.message}`); 41 | } 42 | } 43 | 44 | // If it's neither a valid address nor an ENS name, throw an error 45 | throw new Error(`Invalid address or ENS name: ${addressOrEns}`); 46 | } -------------------------------------------------------------------------------- /src/core/services/index.ts: -------------------------------------------------------------------------------- 1 | // Export all services 2 | export * from './clients.js'; 3 | export * from './balance.js'; 4 | export * from './transfer.js'; 5 | export * from './blocks.js'; 6 | export * from './transactions.js'; 7 | export * from './contracts.js'; 8 | export * from './tokens.js'; 9 | export * from './ens.js'; 10 | export { utils as helpers } from './utils.js'; 11 | 12 | // Re-export common types for convenience 13 | export type { 14 | Address, 15 | Hash, 16 | Hex, 17 | Block, 18 | TransactionReceipt, 19 | Log 20 | } from 'viem'; -------------------------------------------------------------------------------- /src/core/services/tokens.ts: -------------------------------------------------------------------------------- 1 | import { 2 | type Address, 3 | type Hex, 4 | type Hash, 5 | formatUnits, 6 | getContract 7 | } from 'viem'; 8 | import { getPublicClient } from './clients.js'; 9 | 10 | // Standard ERC20 ABI (minimal for reading) 11 | const erc20Abi = [ 12 | { 13 | inputs: [], 14 | name: 'name', 15 | outputs: [{ type: 'string' }], 16 | stateMutability: 'view', 17 | type: 'function' 18 | }, 19 | { 20 | inputs: [], 21 | name: 'symbol', 22 | outputs: [{ type: 'string' }], 23 | stateMutability: 'view', 24 | type: 'function' 25 | }, 26 | { 27 | inputs: [], 28 | name: 'decimals', 29 | outputs: [{ type: 'uint8' }], 30 | stateMutability: 'view', 31 | type: 'function' 32 | }, 33 | { 34 | inputs: [], 35 | name: 'totalSupply', 36 | outputs: [{ type: 'uint256' }], 37 | stateMutability: 'view', 38 | type: 'function' 39 | } 40 | ] as const; 41 | 42 | // Standard ERC721 ABI (minimal for reading) 43 | const erc721Abi = [ 44 | { 45 | inputs: [], 46 | name: 'name', 47 | outputs: [{ type: 'string' }], 48 | stateMutability: 'view', 49 | type: 'function' 50 | }, 51 | { 52 | inputs: [], 53 | name: 'symbol', 54 | outputs: [{ type: 'string' }], 55 | stateMutability: 'view', 56 | type: 'function' 57 | }, 58 | { 59 | inputs: [{ type: 'uint256', name: 'tokenId' }], 60 | name: 'tokenURI', 61 | outputs: [{ type: 'string' }], 62 | stateMutability: 'view', 63 | type: 'function' 64 | } 65 | ] as const; 66 | 67 | // Standard ERC1155 ABI (minimal for reading) 68 | const erc1155Abi = [ 69 | { 70 | inputs: [{ type: 'uint256', name: 'id' }], 71 | name: 'uri', 72 | outputs: [{ type: 'string' }], 73 | stateMutability: 'view', 74 | type: 'function' 75 | } 76 | ] as const; 77 | 78 | /** 79 | * Get ERC20 token information 80 | */ 81 | export async function getERC20TokenInfo( 82 | tokenAddress: Address, 83 | network: string = 'ethereum' 84 | ): Promise<{ 85 | name: string; 86 | symbol: string; 87 | decimals: number; 88 | totalSupply: bigint; 89 | formattedTotalSupply: string; 90 | }> { 91 | const publicClient = getPublicClient(network); 92 | 93 | const contract = getContract({ 94 | address: tokenAddress, 95 | abi: erc20Abi, 96 | client: publicClient, 97 | }); 98 | 99 | const [name, symbol, decimals, totalSupply] = await Promise.all([ 100 | contract.read.name(), 101 | contract.read.symbol(), 102 | contract.read.decimals(), 103 | contract.read.totalSupply() 104 | ]); 105 | 106 | return { 107 | name, 108 | symbol, 109 | decimals, 110 | totalSupply, 111 | formattedTotalSupply: formatUnits(totalSupply, decimals) 112 | }; 113 | } 114 | 115 | /** 116 | * Get ERC721 token metadata 117 | */ 118 | export async function getERC721TokenMetadata( 119 | tokenAddress: Address, 120 | tokenId: bigint, 121 | network: string = 'ethereum' 122 | ): Promise<{ 123 | name: string; 124 | symbol: string; 125 | tokenURI: string; 126 | }> { 127 | const publicClient = getPublicClient(network); 128 | 129 | const contract = getContract({ 130 | address: tokenAddress, 131 | abi: erc721Abi, 132 | client: publicClient, 133 | }); 134 | 135 | const [name, symbol, tokenURI] = await Promise.all([ 136 | contract.read.name(), 137 | contract.read.symbol(), 138 | contract.read.tokenURI([tokenId]) 139 | ]); 140 | 141 | return { 142 | name, 143 | symbol, 144 | tokenURI 145 | }; 146 | } 147 | 148 | /** 149 | * Get ERC1155 token URI 150 | */ 151 | export async function getERC1155TokenURI( 152 | tokenAddress: Address, 153 | tokenId: bigint, 154 | network: string = 'ethereum' 155 | ): Promise { 156 | const publicClient = getPublicClient(network); 157 | 158 | const contract = getContract({ 159 | address: tokenAddress, 160 | abi: erc1155Abi, 161 | client: publicClient, 162 | }); 163 | 164 | return contract.read.uri([tokenId]); 165 | } -------------------------------------------------------------------------------- /src/core/services/transactions.ts: -------------------------------------------------------------------------------- 1 | import { 2 | type Address, 3 | type Hash, 4 | type TransactionReceipt, 5 | type EstimateGasParameters 6 | } from 'viem'; 7 | import { getPublicClient } from './clients.js'; 8 | 9 | /** 10 | * Get a transaction by hash for a specific network 11 | */ 12 | export async function getTransaction(hash: Hash, network = 'ethereum') { 13 | const client = getPublicClient(network); 14 | return await client.getTransaction({ hash }); 15 | } 16 | 17 | /** 18 | * Get a transaction receipt by hash for a specific network 19 | */ 20 | export async function getTransactionReceipt(hash: Hash, network = 'ethereum'): Promise { 21 | const client = getPublicClient(network); 22 | return await client.getTransactionReceipt({ hash }); 23 | } 24 | 25 | /** 26 | * Get the transaction count for an address for a specific network 27 | */ 28 | export async function getTransactionCount(address: Address, network = 'ethereum'): Promise { 29 | const client = getPublicClient(network); 30 | const count = await client.getTransactionCount({ address }); 31 | return Number(count); 32 | } 33 | 34 | /** 35 | * Estimate gas for a transaction for a specific network 36 | */ 37 | export async function estimateGas(params: EstimateGasParameters, network = 'ethereum'): Promise { 38 | const client = getPublicClient(network); 39 | return await client.estimateGas(params); 40 | } 41 | 42 | /** 43 | * Get the chain ID for a specific network 44 | */ 45 | export async function getChainId(network = 'ethereum'): Promise { 46 | const client = getPublicClient(network); 47 | const chainId = await client.getChainId(); 48 | return Number(chainId); 49 | } -------------------------------------------------------------------------------- /src/core/services/transfer.ts: -------------------------------------------------------------------------------- 1 | import { 2 | parseEther, 3 | parseUnits, 4 | formatUnits, 5 | type Address, 6 | type Hash, 7 | type Hex, 8 | type Abi, 9 | getContract, 10 | type Account 11 | } from 'viem'; 12 | import { getPublicClient, getWalletClient } from './clients.js'; 13 | import { getChain } from '../chains.js'; 14 | import { resolveAddress } from './ens.js'; 15 | 16 | // Standard ERC20 ABI for transfers 17 | const erc20TransferAbi = [ 18 | { 19 | inputs: [ 20 | { type: 'address', name: 'to' }, 21 | { type: 'uint256', name: 'amount' } 22 | ], 23 | name: 'transfer', 24 | outputs: [{ type: 'bool' }], 25 | stateMutability: 'nonpayable', 26 | type: 'function' 27 | }, 28 | { 29 | inputs: [ 30 | { type: 'address', name: 'spender' }, 31 | { type: 'uint256', name: 'amount' } 32 | ], 33 | name: 'approve', 34 | outputs: [{ type: 'bool' }], 35 | stateMutability: 'nonpayable', 36 | type: 'function' 37 | }, 38 | { 39 | inputs: [], 40 | name: 'decimals', 41 | outputs: [{ type: 'uint8' }], 42 | stateMutability: 'view', 43 | type: 'function' 44 | }, 45 | { 46 | inputs: [], 47 | name: 'symbol', 48 | outputs: [{ type: 'string' }], 49 | stateMutability: 'view', 50 | type: 'function' 51 | } 52 | ] as const; 53 | 54 | // Standard ERC721 ABI for transfers 55 | const erc721TransferAbi = [ 56 | { 57 | inputs: [ 58 | { type: 'address', name: 'from' }, 59 | { type: 'address', name: 'to' }, 60 | { type: 'uint256', name: 'tokenId' } 61 | ], 62 | name: 'transferFrom', 63 | outputs: [], 64 | stateMutability: 'nonpayable', 65 | type: 'function' 66 | }, 67 | { 68 | inputs: [], 69 | name: 'name', 70 | outputs: [{ type: 'string' }], 71 | stateMutability: 'view', 72 | type: 'function' 73 | }, 74 | { 75 | inputs: [], 76 | name: 'symbol', 77 | outputs: [{ type: 'string' }], 78 | stateMutability: 'view', 79 | type: 'function' 80 | }, 81 | { 82 | inputs: [{ type: 'uint256', name: 'tokenId' }], 83 | name: 'ownerOf', 84 | outputs: [{ type: 'address' }], 85 | stateMutability: 'view', 86 | type: 'function' 87 | } 88 | ] as const; 89 | 90 | // ERC1155 ABI for transfers 91 | const erc1155TransferAbi = [ 92 | { 93 | inputs: [ 94 | { type: 'address', name: 'from' }, 95 | { type: 'address', name: 'to' }, 96 | { type: 'uint256', name: 'id' }, 97 | { type: 'uint256', name: 'amount' }, 98 | { type: 'bytes', name: 'data' } 99 | ], 100 | name: 'safeTransferFrom', 101 | outputs: [], 102 | stateMutability: 'nonpayable', 103 | type: 'function' 104 | }, 105 | { 106 | inputs: [ 107 | { type: 'address', name: 'account' }, 108 | { type: 'uint256', name: 'id' } 109 | ], 110 | name: 'balanceOf', 111 | outputs: [{ type: 'uint256' }], 112 | stateMutability: 'view', 113 | type: 'function' 114 | } 115 | ] as const; 116 | 117 | /** 118 | * Transfer ETH to an address 119 | * @param privateKey Sender's private key 120 | * @param toAddressOrEns Recipient address or ENS name 121 | * @param amount Amount to send in ETH 122 | * @param network Network name or chain ID 123 | * @returns Transaction hash 124 | */ 125 | export async function transferETH( 126 | privateKey: string | Hex, 127 | toAddressOrEns: string, 128 | amount: string, // in ether 129 | network = 'ethereum' 130 | ): Promise { 131 | // Resolve ENS name to address if needed 132 | const toAddress = await resolveAddress(toAddressOrEns, network); 133 | 134 | // Ensure the private key has 0x prefix 135 | const formattedKey = typeof privateKey === 'string' && !privateKey.startsWith('0x') 136 | ? `0x${privateKey}` as Hex 137 | : privateKey as Hex; 138 | 139 | const client = getWalletClient(formattedKey, network); 140 | const amountWei = parseEther(amount); 141 | 142 | return client.sendTransaction({ 143 | to: toAddress, 144 | value: amountWei, 145 | account: client.account!, 146 | chain: client.chain 147 | }); 148 | } 149 | 150 | /** 151 | * Transfer ERC20 tokens to an address 152 | * @param tokenAddressOrEns Token contract address or ENS name 153 | * @param toAddressOrEns Recipient address or ENS name 154 | * @param amount Amount to send (in token units) 155 | * @param privateKey Sender's private key 156 | * @param network Network name or chain ID 157 | * @returns Transaction details 158 | */ 159 | export async function transferERC20( 160 | tokenAddressOrEns: string, 161 | toAddressOrEns: string, 162 | amount: string, 163 | privateKey: string | `0x${string}`, 164 | network: string = 'ethereum' 165 | ): Promise<{ 166 | txHash: Hash; 167 | amount: { 168 | raw: bigint; 169 | formatted: string; 170 | }; 171 | token: { 172 | symbol: string; 173 | decimals: number; 174 | }; 175 | }> { 176 | // Resolve ENS names to addresses if needed 177 | const tokenAddress = await resolveAddress(tokenAddressOrEns, network) as Address; 178 | const toAddress = await resolveAddress(toAddressOrEns, network) as Address; 179 | 180 | // Ensure the private key has 0x prefix 181 | const formattedKey = typeof privateKey === 'string' && !privateKey.startsWith('0x') 182 | ? `0x${privateKey}` as `0x${string}` 183 | : privateKey as `0x${string}`; 184 | 185 | // Get token details 186 | const publicClient = getPublicClient(network); 187 | const contract = getContract({ 188 | address: tokenAddress, 189 | abi: erc20TransferAbi, 190 | client: publicClient, 191 | }); 192 | 193 | // Get token decimals and symbol 194 | const decimals = await contract.read.decimals(); 195 | const symbol = await contract.read.symbol(); 196 | 197 | // Parse the amount with the correct number of decimals 198 | const rawAmount = parseUnits(amount, decimals); 199 | 200 | // Create wallet client for sending the transaction 201 | const walletClient = getWalletClient(formattedKey, network); 202 | 203 | // Send the transaction 204 | const hash = await walletClient.writeContract({ 205 | address: tokenAddress, 206 | abi: erc20TransferAbi, 207 | functionName: 'transfer', 208 | args: [toAddress, rawAmount], 209 | account: walletClient.account!, 210 | chain: walletClient.chain 211 | }); 212 | 213 | return { 214 | txHash: hash, 215 | amount: { 216 | raw: rawAmount, 217 | formatted: amount 218 | }, 219 | token: { 220 | symbol, 221 | decimals 222 | } 223 | }; 224 | } 225 | 226 | /** 227 | * Approve ERC20 token spending 228 | * @param tokenAddressOrEns Token contract address or ENS name 229 | * @param spenderAddressOrEns Spender address or ENS name 230 | * @param amount Amount to approve (in token units) 231 | * @param privateKey Owner's private key 232 | * @param network Network name or chain ID 233 | * @returns Transaction details 234 | */ 235 | export async function approveERC20( 236 | tokenAddressOrEns: string, 237 | spenderAddressOrEns: string, 238 | amount: string, 239 | privateKey: string | `0x${string}`, 240 | network: string = 'ethereum' 241 | ): Promise<{ 242 | txHash: Hash; 243 | amount: { 244 | raw: bigint; 245 | formatted: string; 246 | }; 247 | token: { 248 | symbol: string; 249 | decimals: number; 250 | }; 251 | }> { 252 | // Resolve ENS names to addresses if needed 253 | const tokenAddress = await resolveAddress(tokenAddressOrEns, network) as Address; 254 | const spenderAddress = await resolveAddress(spenderAddressOrEns, network) as Address; 255 | 256 | // Ensure the private key has 0x prefix 257 | const formattedKey = typeof privateKey === 'string' && !privateKey.startsWith('0x') 258 | ? `0x${privateKey}` as `0x${string}` 259 | : privateKey as `0x${string}`; 260 | 261 | // Get token details 262 | const publicClient = getPublicClient(network); 263 | const contract = getContract({ 264 | address: tokenAddress, 265 | abi: erc20TransferAbi, 266 | client: publicClient, 267 | }); 268 | 269 | // Get token decimals and symbol 270 | const decimals = await contract.read.decimals(); 271 | const symbol = await contract.read.symbol(); 272 | 273 | // Parse the amount with the correct number of decimals 274 | const rawAmount = parseUnits(amount, decimals); 275 | 276 | // Create wallet client for sending the transaction 277 | const walletClient = getWalletClient(formattedKey, network); 278 | 279 | // Send the transaction 280 | const hash = await walletClient.writeContract({ 281 | address: tokenAddress, 282 | abi: erc20TransferAbi, 283 | functionName: 'approve', 284 | args: [spenderAddress, rawAmount], 285 | account: walletClient.account!, 286 | chain: walletClient.chain 287 | }); 288 | 289 | return { 290 | txHash: hash, 291 | amount: { 292 | raw: rawAmount, 293 | formatted: amount 294 | }, 295 | token: { 296 | symbol, 297 | decimals 298 | } 299 | }; 300 | } 301 | 302 | /** 303 | * Transfer an NFT (ERC721) to an address 304 | * @param tokenAddressOrEns NFT contract address or ENS name 305 | * @param toAddressOrEns Recipient address or ENS name 306 | * @param tokenId Token ID to transfer 307 | * @param privateKey Owner's private key 308 | * @param network Network name or chain ID 309 | * @returns Transaction details 310 | */ 311 | export async function transferERC721( 312 | tokenAddressOrEns: string, 313 | toAddressOrEns: string, 314 | tokenId: bigint, 315 | privateKey: string | `0x${string}`, 316 | network: string = 'ethereum' 317 | ): Promise<{ 318 | txHash: Hash; 319 | tokenId: string; 320 | token: { 321 | name: string; 322 | symbol: string; 323 | }; 324 | }> { 325 | // Resolve ENS names to addresses if needed 326 | const tokenAddress = await resolveAddress(tokenAddressOrEns, network) as Address; 327 | const toAddress = await resolveAddress(toAddressOrEns, network) as Address; 328 | 329 | // Ensure the private key has 0x prefix 330 | const formattedKey = typeof privateKey === 'string' && !privateKey.startsWith('0x') 331 | ? `0x${privateKey}` as `0x${string}` 332 | : privateKey as `0x${string}`; 333 | 334 | // Create wallet client for sending the transaction 335 | const walletClient = getWalletClient(formattedKey, network); 336 | const fromAddress = walletClient.account!.address; 337 | 338 | // Send the transaction 339 | const hash = await walletClient.writeContract({ 340 | address: tokenAddress, 341 | abi: erc721TransferAbi, 342 | functionName: 'transferFrom', 343 | args: [fromAddress, toAddress, tokenId], 344 | account: walletClient.account!, 345 | chain: walletClient.chain 346 | }); 347 | 348 | // Get token metadata 349 | const publicClient = getPublicClient(network); 350 | const contract = getContract({ 351 | address: tokenAddress, 352 | abi: erc721TransferAbi, 353 | client: publicClient, 354 | }); 355 | 356 | // Get token name and symbol 357 | let name = 'Unknown'; 358 | let symbol = 'NFT'; 359 | 360 | try { 361 | [name, symbol] = await Promise.all([ 362 | contract.read.name(), 363 | contract.read.symbol() 364 | ]); 365 | } catch (error) { 366 | console.error('Error fetching NFT metadata:', error); 367 | } 368 | 369 | return { 370 | txHash: hash, 371 | tokenId: tokenId.toString(), 372 | token: { 373 | name, 374 | symbol 375 | } 376 | }; 377 | } 378 | 379 | /** 380 | * Transfer ERC1155 tokens to an address 381 | * @param tokenAddressOrEns Token contract address or ENS name 382 | * @param toAddressOrEns Recipient address or ENS name 383 | * @param tokenId Token ID to transfer 384 | * @param amount Amount to transfer 385 | * @param privateKey Owner's private key 386 | * @param network Network name or chain ID 387 | * @returns Transaction details 388 | */ 389 | export async function transferERC1155( 390 | tokenAddressOrEns: string, 391 | toAddressOrEns: string, 392 | tokenId: bigint, 393 | amount: string, 394 | privateKey: string | `0x${string}`, 395 | network: string = 'ethereum' 396 | ): Promise<{ 397 | txHash: Hash; 398 | tokenId: string; 399 | amount: string; 400 | }> { 401 | // Resolve ENS names to addresses if needed 402 | const tokenAddress = await resolveAddress(tokenAddressOrEns, network) as Address; 403 | const toAddress = await resolveAddress(toAddressOrEns, network) as Address; 404 | 405 | // Ensure the private key has 0x prefix 406 | const formattedKey = typeof privateKey === 'string' && !privateKey.startsWith('0x') 407 | ? `0x${privateKey}` as `0x${string}` 408 | : privateKey as `0x${string}`; 409 | 410 | // Create wallet client for sending the transaction 411 | const walletClient = getWalletClient(formattedKey, network); 412 | const fromAddress = walletClient.account!.address; 413 | 414 | // Parse amount to bigint 415 | const amountBigInt = BigInt(amount); 416 | 417 | // Send the transaction 418 | const hash = await walletClient.writeContract({ 419 | address: tokenAddress, 420 | abi: erc1155TransferAbi, 421 | functionName: 'safeTransferFrom', 422 | args: [fromAddress, toAddress, tokenId, amountBigInt, '0x'], 423 | account: walletClient.account!, 424 | chain: walletClient.chain 425 | }); 426 | 427 | return { 428 | txHash: hash, 429 | tokenId: tokenId.toString(), 430 | amount 431 | }; 432 | } -------------------------------------------------------------------------------- /src/core/services/utils.ts: -------------------------------------------------------------------------------- 1 | import { 2 | parseEther, 3 | formatEther, 4 | type Account, 5 | type Hash, 6 | type Chain, 7 | type WalletClient, 8 | type Transport, 9 | type HttpTransport 10 | } from 'viem'; 11 | 12 | /** 13 | * Utility functions for formatting and parsing values 14 | */ 15 | export const utils = { 16 | // Convert ether to wei 17 | parseEther, 18 | 19 | // Convert wei to ether 20 | formatEther, 21 | 22 | // Format a bigint to a string 23 | formatBigInt: (value: bigint): string => value.toString(), 24 | 25 | // Format an object to JSON with bigint handling 26 | formatJson: (obj: unknown): string => JSON.stringify(obj, (_, value) => 27 | typeof value === 'bigint' ? value.toString() : value, 2), 28 | 29 | // Format a number with commas 30 | formatNumber: (value: number | string): string => { 31 | return Number(value).toLocaleString(); 32 | }, 33 | 34 | // Convert a hex string to a number 35 | hexToNumber: (hex: string): number => { 36 | return parseInt(hex, 16); 37 | }, 38 | 39 | // Convert a number to a hex string 40 | numberToHex: (num: number): string => { 41 | return '0x' + num.toString(16); 42 | } 43 | }; -------------------------------------------------------------------------------- /src/core/tools.ts: -------------------------------------------------------------------------------- 1 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; 2 | import { z } from "zod"; 3 | import { getSupportedNetworks, getRpcUrl } from "./chains.js"; 4 | import * as services from "./services/index.js"; 5 | import { type Address, type Hex, type Hash } from 'viem'; 6 | import { normalize } from 'viem/ens'; 7 | 8 | /** 9 | * Register all EVM-related tools with the MCP server 10 | * 11 | * All tools that accept Ethereum addresses also support ENS names (e.g., 'vitalik.eth'). 12 | * ENS names are automatically resolved to addresses using the Ethereum Name Service. 13 | * 14 | * @param server The MCP server instance 15 | */ 16 | export function registerEVMTools(server: McpServer) { 17 | // NETWORK INFORMATION TOOLS 18 | 19 | // Get chain information 20 | server.tool( 21 | "get_chain_info", 22 | "Get information about an EVM network", 23 | { 24 | network: z.string().optional().describe("Network name (e.g., 'ethereum', 'optimism', 'arbitrum', 'base', etc.) or chain ID. Supports all EVM-compatible networks. Defaults to Ethereum mainnet.") 25 | }, 26 | async ({ network = "ethereum" }) => { 27 | try { 28 | const chainId = await services.getChainId(network); 29 | const blockNumber = await services.getBlockNumber(network); 30 | const rpcUrl = getRpcUrl(network); 31 | 32 | return { 33 | content: [{ 34 | type: "text", 35 | text: JSON.stringify({ 36 | network, 37 | chainId, 38 | blockNumber: blockNumber.toString(), 39 | rpcUrl 40 | }, null, 2) 41 | }] 42 | }; 43 | } catch (error) { 44 | return { 45 | content: [{ 46 | type: "text", 47 | text: `Error fetching chain info: ${error instanceof Error ? error.message : String(error)}` 48 | }], 49 | isError: true 50 | }; 51 | } 52 | } 53 | ); 54 | 55 | // ENS LOOKUP TOOL 56 | 57 | // Resolve ENS name to address 58 | server.tool( 59 | "resolve_ens", 60 | "Resolve an ENS name to an Ethereum address", 61 | { 62 | ensName: z.string().describe("ENS name to resolve (e.g., 'vitalik.eth')"), 63 | network: z.string().optional().describe("Network name (e.g., 'ethereum', 'optimism', 'arbitrum', 'base', etc.) or chain ID. ENS resolution works best on Ethereum mainnet. Defaults to Ethereum mainnet.") 64 | }, 65 | async ({ ensName, network = "ethereum" }) => { 66 | try { 67 | // Validate that the input is an ENS name 68 | if (!ensName.includes('.')) { 69 | return { 70 | content: [{ 71 | type: "text", 72 | text: `Error: Input "${ensName}" is not a valid ENS name. ENS names must contain a dot (e.g., 'name.eth').` 73 | }], 74 | isError: true 75 | }; 76 | } 77 | 78 | // Normalize the ENS name 79 | const normalizedEns = normalize(ensName); 80 | 81 | // Resolve the ENS name to an address 82 | const address = await services.resolveAddress(ensName, network); 83 | 84 | return { 85 | content: [{ 86 | type: "text", 87 | text: JSON.stringify({ 88 | ensName: ensName, 89 | normalizedName: normalizedEns, 90 | resolvedAddress: address, 91 | network 92 | }, null, 2) 93 | }] 94 | }; 95 | } catch (error) { 96 | return { 97 | content: [{ 98 | type: "text", 99 | text: `Error resolving ENS name: ${error instanceof Error ? error.message : String(error)}` 100 | }], 101 | isError: true 102 | }; 103 | } 104 | } 105 | ); 106 | 107 | // Get supported networks 108 | server.tool( 109 | "get_supported_networks", 110 | "Get a list of supported EVM networks", 111 | {}, 112 | async () => { 113 | try { 114 | const networks = getSupportedNetworks(); 115 | 116 | return { 117 | content: [{ 118 | type: "text", 119 | text: JSON.stringify({ 120 | supportedNetworks: networks 121 | }, null, 2) 122 | }] 123 | }; 124 | } catch (error) { 125 | return { 126 | content: [{ 127 | type: "text", 128 | text: `Error fetching supported networks: ${error instanceof Error ? error.message : String(error)}` 129 | }], 130 | isError: true 131 | }; 132 | } 133 | } 134 | ); 135 | 136 | // BLOCK TOOLS 137 | 138 | // Get block by number 139 | server.tool( 140 | "get_block_by_number", 141 | "Get a block by its block number", 142 | { 143 | blockNumber: z.number().describe("The block number to fetch"), 144 | network: z.string().optional().describe("Network name or chain ID. Defaults to Ethereum mainnet.") 145 | }, 146 | async ({ blockNumber, network = "ethereum" }) => { 147 | try { 148 | const block = await services.getBlockByNumber(blockNumber, network); 149 | 150 | return { 151 | content: [{ 152 | type: "text", 153 | text: services.helpers.formatJson(block) 154 | }] 155 | }; 156 | } catch (error) { 157 | return { 158 | content: [{ 159 | type: "text", 160 | text: `Error fetching block ${blockNumber}: ${error instanceof Error ? error.message : String(error)}` 161 | }], 162 | isError: true 163 | }; 164 | } 165 | } 166 | ); 167 | 168 | // Get latest block 169 | server.tool( 170 | "get_latest_block", 171 | "Get the latest block from the EVM", 172 | { 173 | network: z.string().optional().describe("Network name or chain ID. Defaults to Ethereum mainnet.") 174 | }, 175 | async ({ network = "ethereum" }) => { 176 | try { 177 | const block = await services.getLatestBlock(network); 178 | 179 | return { 180 | content: [{ 181 | type: "text", 182 | text: services.helpers.formatJson(block) 183 | }] 184 | }; 185 | } catch (error) { 186 | return { 187 | content: [{ 188 | type: "text", 189 | text: `Error fetching latest block: ${error instanceof Error ? error.message : String(error)}` 190 | }], 191 | isError: true 192 | }; 193 | } 194 | } 195 | ); 196 | 197 | // BALANCE TOOLS 198 | 199 | // Get ETH balance 200 | server.tool( 201 | "get_balance", 202 | "Get the native token balance (ETH, MATIC, etc.) for an address", 203 | { 204 | address: z.string().describe("The wallet address or ENS name (e.g., '0x1234...' or 'vitalik.eth') to check the balance for"), 205 | network: z.string().optional().describe("Network name (e.g., 'ethereum', 'optimism', 'arbitrum', 'base', etc.) or chain ID. Supports all EVM-compatible networks. Defaults to Ethereum mainnet.") 206 | }, 207 | async ({ address, network = "ethereum" }) => { 208 | try { 209 | const balance = await services.getETHBalance(address, network); 210 | 211 | return { 212 | content: [{ 213 | type: "text", 214 | text: JSON.stringify({ 215 | address, 216 | network, 217 | wei: balance.wei.toString(), 218 | ether: balance.ether 219 | }, null, 2) 220 | }] 221 | }; 222 | } catch (error) { 223 | return { 224 | content: [{ 225 | type: "text", 226 | text: `Error fetching balance: ${error instanceof Error ? error.message : String(error)}` 227 | }], 228 | isError: true 229 | }; 230 | } 231 | } 232 | ); 233 | 234 | // Get ERC20 balance 235 | server.tool( 236 | "get_erc20_balance", 237 | "Get the ERC20 token balance of an Ethereum address", 238 | { 239 | address: z.string().describe("The Ethereum address to check"), 240 | tokenAddress: z.string().describe("The ERC20 token contract address"), 241 | network: z.string().optional().describe("Network name or chain ID. Defaults to Ethereum mainnet.") 242 | }, 243 | async ({ address, tokenAddress, network = "ethereum" }) => { 244 | try { 245 | const balance = await services.getERC20Balance( 246 | tokenAddress as Address, 247 | address as Address, 248 | network 249 | ); 250 | 251 | return { 252 | content: [{ 253 | type: "text", 254 | text: JSON.stringify({ 255 | address, 256 | tokenAddress, 257 | network, 258 | balance: { 259 | raw: balance.raw.toString(), 260 | formatted: balance.formatted, 261 | decimals: balance.token.decimals 262 | } 263 | }, null, 2) 264 | }] 265 | }; 266 | } catch (error) { 267 | return { 268 | content: [{ 269 | type: "text", 270 | text: `Error fetching ERC20 balance for ${address}: ${error instanceof Error ? error.message : String(error)}` 271 | }], 272 | isError: true 273 | }; 274 | } 275 | } 276 | ); 277 | 278 | // Get ERC20 token balance 279 | server.tool( 280 | "get_token_balance", 281 | "Get the balance of an ERC20 token for an address", 282 | { 283 | tokenAddress: z.string().describe("The contract address or ENS name of the ERC20 token (e.g., '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48' for USDC or 'uniswap.eth')"), 284 | ownerAddress: z.string().describe("The wallet address or ENS name to check the balance for (e.g., '0x1234...' or 'vitalik.eth')"), 285 | network: z.string().optional().describe("Network name (e.g., 'ethereum', 'optimism', 'arbitrum', 'base', etc.) or chain ID. Supports all EVM-compatible networks. Defaults to Ethereum mainnet.") 286 | }, 287 | async ({ tokenAddress, ownerAddress, network = "ethereum" }) => { 288 | try { 289 | const balance = await services.getERC20Balance(tokenAddress, ownerAddress, network); 290 | 291 | return { 292 | content: [{ 293 | type: "text", 294 | text: JSON.stringify({ 295 | tokenAddress, 296 | owner: ownerAddress, 297 | network, 298 | raw: balance.raw.toString(), 299 | formatted: balance.formatted, 300 | symbol: balance.token.symbol, 301 | decimals: balance.token.decimals 302 | }, null, 2) 303 | }] 304 | }; 305 | } catch (error) { 306 | return { 307 | content: [{ 308 | type: "text", 309 | text: `Error fetching token balance: ${error instanceof Error ? error.message : String(error)}` 310 | }], 311 | isError: true 312 | }; 313 | } 314 | } 315 | ); 316 | 317 | // TRANSACTION TOOLS 318 | 319 | // Get transaction by hash 320 | server.tool( 321 | "get_transaction", 322 | "Get detailed information about a specific transaction by its hash. Includes sender, recipient, value, data, and more.", 323 | { 324 | txHash: z.string().describe("The transaction hash to look up (e.g., '0x1234...')"), 325 | network: z.string().optional().describe("Network name (e.g., 'ethereum', 'optimism', 'arbitrum', 'base', 'polygon') or chain ID. Defaults to Ethereum mainnet.") 326 | }, 327 | async ({ txHash, network = "ethereum" }) => { 328 | try { 329 | const tx = await services.getTransaction(txHash as Hash, network); 330 | 331 | return { 332 | content: [{ 333 | type: "text", 334 | text: services.helpers.formatJson(tx) 335 | }] 336 | }; 337 | } catch (error) { 338 | return { 339 | content: [{ 340 | type: "text", 341 | text: `Error fetching transaction ${txHash}: ${error instanceof Error ? error.message : String(error)}` 342 | }], 343 | isError: true 344 | }; 345 | } 346 | } 347 | ); 348 | 349 | // Get transaction receipt 350 | server.tool( 351 | "get_transaction_receipt", 352 | "Get a transaction receipt by its hash", 353 | { 354 | txHash: z.string().describe("The transaction hash to look up"), 355 | network: z.string().optional().describe("Network name or chain ID. Defaults to Ethereum mainnet.") 356 | }, 357 | async ({ txHash, network = "ethereum" }) => { 358 | try { 359 | const receipt = await services.getTransactionReceipt(txHash as Hash, network); 360 | 361 | return { 362 | content: [{ 363 | type: "text", 364 | text: services.helpers.formatJson(receipt) 365 | }] 366 | }; 367 | } catch (error) { 368 | return { 369 | content: [{ 370 | type: "text", 371 | text: `Error fetching transaction receipt ${txHash}: ${error instanceof Error ? error.message : String(error)}` 372 | }], 373 | isError: true 374 | }; 375 | } 376 | } 377 | ); 378 | 379 | // Estimate gas 380 | server.tool( 381 | "estimate_gas", 382 | "Estimate the gas cost for a transaction", 383 | { 384 | to: z.string().describe("The recipient address"), 385 | value: z.string().optional().describe("The amount of ETH to send in ether (e.g., '0.1')"), 386 | data: z.string().optional().describe("The transaction data as a hex string"), 387 | network: z.string().optional().describe("Network name or chain ID. Defaults to Ethereum mainnet.") 388 | }, 389 | async ({ to, value, data, network = "ethereum" }) => { 390 | try { 391 | const params: any = { to: to as Address }; 392 | 393 | if (value) { 394 | params.value = services.helpers.parseEther(value); 395 | } 396 | 397 | if (data) { 398 | params.data = data as `0x${string}`; 399 | } 400 | 401 | const gas = await services.estimateGas(params, network); 402 | 403 | return { 404 | content: [{ 405 | type: "text", 406 | text: JSON.stringify({ 407 | network, 408 | estimatedGas: gas.toString() 409 | }, null, 2) 410 | }] 411 | }; 412 | } catch (error) { 413 | return { 414 | content: [{ 415 | type: "text", 416 | text: `Error estimating gas: ${error instanceof Error ? error.message : String(error)}` 417 | }], 418 | isError: true 419 | }; 420 | } 421 | } 422 | ); 423 | 424 | // TRANSFER TOOLS 425 | 426 | // Transfer ETH 427 | server.tool( 428 | "transfer_eth", 429 | "Transfer native tokens (ETH, MATIC, etc.) to an address", 430 | { 431 | privateKey: z.string().describe("Private key of the sender account in hex format (with or without 0x prefix). SECURITY: This is used only for transaction signing and is not stored."), 432 | to: z.string().describe("The recipient address or ENS name (e.g., '0x1234...' or 'vitalik.eth')"), 433 | amount: z.string().describe("Amount to send in ETH (or the native token of the network), as a string (e.g., '0.1')"), 434 | network: z.string().optional().describe("Network name (e.g., 'ethereum', 'optimism', 'arbitrum', 'base', etc.) or chain ID. Supports all EVM-compatible networks. Defaults to Ethereum mainnet.") 435 | }, 436 | async ({ privateKey, to, amount, network = "ethereum" }) => { 437 | try { 438 | const txHash = await services.transferETH(privateKey, to, amount, network); 439 | 440 | return { 441 | content: [{ 442 | type: "text", 443 | text: JSON.stringify({ 444 | success: true, 445 | txHash, 446 | to, 447 | amount, 448 | network 449 | }, null, 2) 450 | }] 451 | }; 452 | } catch (error) { 453 | return { 454 | content: [{ 455 | type: "text", 456 | text: `Error transferring ETH: ${error instanceof Error ? error.message : String(error)}` 457 | }], 458 | isError: true 459 | }; 460 | } 461 | } 462 | ); 463 | 464 | // Transfer ERC20 465 | server.tool( 466 | "transfer_erc20", 467 | "Transfer ERC20 tokens to another address", 468 | { 469 | privateKey: z.string().describe("Private key of the sending account (this is used for signing and is never stored)"), 470 | tokenAddress: z.string().describe("The address of the ERC20 token contract"), 471 | toAddress: z.string().describe("The recipient address"), 472 | amount: z.string().describe("The amount of tokens to send (in token units, e.g., '10' for 10 tokens)"), 473 | network: z.string().optional().describe("Network name (e.g., 'ethereum', 'optimism', 'arbitrum', 'base', etc.) or chain ID. Supports all EVM-compatible networks. Defaults to Ethereum mainnet.") 474 | }, 475 | async ({ privateKey, tokenAddress, toAddress, amount, network = "ethereum" }) => { 476 | try { 477 | // Get the formattedKey with 0x prefix 478 | const formattedKey = privateKey.startsWith('0x') 479 | ? privateKey as `0x${string}` 480 | : `0x${privateKey}` as `0x${string}`; 481 | 482 | const result = await services.transferERC20( 483 | tokenAddress as Address, 484 | toAddress as Address, 485 | amount, 486 | formattedKey, 487 | network 488 | ); 489 | 490 | return { 491 | content: [{ 492 | type: "text", 493 | text: JSON.stringify({ 494 | success: true, 495 | txHash: result.txHash, 496 | network, 497 | tokenAddress, 498 | recipient: toAddress, 499 | amount: result.amount.formatted, 500 | symbol: result.token.symbol 501 | }, null, 2) 502 | }] 503 | }; 504 | } catch (error) { 505 | return { 506 | content: [{ 507 | type: "text", 508 | text: `Error transferring ERC20 tokens: ${error instanceof Error ? error.message : String(error)}` 509 | }], 510 | isError: true 511 | }; 512 | } 513 | } 514 | ); 515 | 516 | // Approve ERC20 token spending 517 | server.tool( 518 | "approve_token_spending", 519 | "Approve another address (like a DeFi protocol or exchange) to spend your ERC20 tokens. This is often required before interacting with DeFi protocols.", 520 | { 521 | privateKey: z.string().describe("Private key of the token owner account in hex format (with or without 0x prefix). SECURITY: This is used only for transaction signing and is not stored."), 522 | tokenAddress: z.string().describe("The contract address of the ERC20 token to approve for spending (e.g., '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48' for USDC on Ethereum)"), 523 | spenderAddress: z.string().describe("The contract address being approved to spend your tokens (e.g., a DEX or lending protocol)"), 524 | amount: z.string().describe("The amount of tokens to approve in token units, not wei (e.g., '1000' to approve spending 1000 tokens). Use a very large number for unlimited approval."), 525 | network: z.string().optional().describe("Network name (e.g., 'ethereum', 'optimism', 'arbitrum', 'base', 'polygon') or chain ID. Defaults to Ethereum mainnet.") 526 | }, 527 | async ({ privateKey, tokenAddress, spenderAddress, amount, network = "ethereum" }) => { 528 | try { 529 | // Get the formattedKey with 0x prefix 530 | const formattedKey = privateKey.startsWith('0x') 531 | ? privateKey as `0x${string}` 532 | : `0x${privateKey}` as `0x${string}`; 533 | 534 | const result = await services.approveERC20( 535 | tokenAddress as Address, 536 | spenderAddress as Address, 537 | amount, 538 | formattedKey, 539 | network 540 | ); 541 | 542 | return { 543 | content: [{ 544 | type: "text", 545 | text: JSON.stringify({ 546 | success: true, 547 | txHash: result.txHash, 548 | network, 549 | tokenAddress, 550 | spender: spenderAddress, 551 | amount: result.amount.formatted, 552 | symbol: result.token.symbol 553 | }, null, 2) 554 | }] 555 | }; 556 | } catch (error) { 557 | return { 558 | content: [{ 559 | type: "text", 560 | text: `Error approving token spending: ${error instanceof Error ? error.message : String(error)}` 561 | }], 562 | isError: true 563 | }; 564 | } 565 | } 566 | ); 567 | 568 | // Transfer NFT (ERC721) 569 | server.tool( 570 | "transfer_nft", 571 | "Transfer an NFT (ERC721 token) from one address to another. Requires the private key of the current owner for signing the transaction.", 572 | { 573 | privateKey: z.string().describe("Private key of the NFT owner account in hex format (with or without 0x prefix). SECURITY: This is used only for transaction signing and is not stored."), 574 | tokenAddress: z.string().describe("The contract address of the NFT collection (e.g., '0xBC4CA0EdA7647A8aB7C2061c2E118A18a936f13D' for Bored Ape Yacht Club)"), 575 | tokenId: z.string().describe("The ID of the specific NFT to transfer (e.g., '1234')"), 576 | toAddress: z.string().describe("The recipient wallet address that will receive the NFT"), 577 | network: z.string().optional().describe("Network name (e.g., 'ethereum', 'optimism', 'arbitrum', 'base', 'polygon') or chain ID. Most NFTs are on Ethereum mainnet, which is the default.") 578 | }, 579 | async ({ privateKey, tokenAddress, tokenId, toAddress, network = "ethereum" }) => { 580 | try { 581 | // Get the formattedKey with 0x prefix 582 | const formattedKey = privateKey.startsWith('0x') 583 | ? privateKey as `0x${string}` 584 | : `0x${privateKey}` as `0x${string}`; 585 | 586 | const result = await services.transferERC721( 587 | tokenAddress as Address, 588 | toAddress as Address, 589 | BigInt(tokenId), 590 | formattedKey, 591 | network 592 | ); 593 | 594 | return { 595 | content: [{ 596 | type: "text", 597 | text: JSON.stringify({ 598 | success: true, 599 | txHash: result.txHash, 600 | network, 601 | collection: tokenAddress, 602 | tokenId: result.tokenId, 603 | recipient: toAddress, 604 | name: result.token.name, 605 | symbol: result.token.symbol 606 | }, null, 2) 607 | }] 608 | }; 609 | } catch (error) { 610 | return { 611 | content: [{ 612 | type: "text", 613 | text: `Error transferring NFT: ${error instanceof Error ? error.message : String(error)}` 614 | }], 615 | isError: true 616 | }; 617 | } 618 | } 619 | ); 620 | 621 | // Transfer ERC1155 token 622 | server.tool( 623 | "transfer_erc1155", 624 | "Transfer ERC1155 tokens to another address. ERC1155 is a multi-token standard that can represent both fungible and non-fungible tokens in a single contract.", 625 | { 626 | privateKey: z.string().describe("Private key of the token owner account in hex format (with or without 0x prefix). SECURITY: This is used only for transaction signing and is not stored."), 627 | tokenAddress: z.string().describe("The contract address of the ERC1155 token collection (e.g., '0x76BE3b62873462d2142405439777e971754E8E77')"), 628 | tokenId: z.string().describe("The ID of the specific token to transfer (e.g., '1234')"), 629 | amount: z.string().describe("The quantity of tokens to send (e.g., '1' for a single NFT or '10' for 10 fungible tokens)"), 630 | toAddress: z.string().describe("The recipient wallet address that will receive the tokens"), 631 | network: z.string().optional().describe("Network name (e.g., 'ethereum', 'optimism', 'arbitrum', 'base', 'polygon') or chain ID. ERC1155 tokens exist across many networks. Defaults to Ethereum mainnet.") 632 | }, 633 | async ({ privateKey, tokenAddress, tokenId, amount, toAddress, network = "ethereum" }) => { 634 | try { 635 | // Get the formattedKey with 0x prefix 636 | const formattedKey = privateKey.startsWith('0x') 637 | ? privateKey as `0x${string}` 638 | : `0x${privateKey}` as `0x${string}`; 639 | 640 | const result = await services.transferERC1155( 641 | tokenAddress as Address, 642 | toAddress as Address, 643 | BigInt(tokenId), 644 | amount, 645 | formattedKey, 646 | network 647 | ); 648 | 649 | return { 650 | content: [{ 651 | type: "text", 652 | text: JSON.stringify({ 653 | success: true, 654 | txHash: result.txHash, 655 | network, 656 | contract: tokenAddress, 657 | tokenId: result.tokenId, 658 | amount: result.amount, 659 | recipient: toAddress 660 | }, null, 2) 661 | }] 662 | }; 663 | } catch (error) { 664 | return { 665 | content: [{ 666 | type: "text", 667 | text: `Error transferring ERC1155 tokens: ${error instanceof Error ? error.message : String(error)}` 668 | }], 669 | isError: true 670 | }; 671 | } 672 | } 673 | ); 674 | 675 | // Transfer ERC20 tokens 676 | server.tool( 677 | "transfer_token", 678 | "Transfer ERC20 tokens to an address", 679 | { 680 | privateKey: z.string().describe("Private key of the sender account in hex format (with or without 0x prefix). SECURITY: This is used only for transaction signing and is not stored."), 681 | tokenAddress: z.string().describe("The contract address or ENS name of the ERC20 token to transfer (e.g., '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48' for USDC or 'uniswap.eth')"), 682 | toAddress: z.string().describe("The recipient address or ENS name that will receive the tokens (e.g., '0x1234...' or 'vitalik.eth')"), 683 | amount: z.string().describe("Amount of tokens to send as a string (e.g., '100' for 100 tokens). This will be adjusted for the token's decimals."), 684 | network: z.string().optional().describe("Network name (e.g., 'ethereum', 'optimism', 'arbitrum', 'base', etc.) or chain ID. Supports all EVM-compatible networks. Defaults to Ethereum mainnet.") 685 | }, 686 | async ({ privateKey, tokenAddress, toAddress, amount, network = "ethereum" }) => { 687 | try { 688 | const result = await services.transferERC20( 689 | tokenAddress, 690 | toAddress, 691 | amount, 692 | privateKey, 693 | network 694 | ); 695 | 696 | return { 697 | content: [{ 698 | type: "text", 699 | text: JSON.stringify({ 700 | success: true, 701 | txHash: result.txHash, 702 | tokenAddress, 703 | toAddress, 704 | amount: result.amount.formatted, 705 | symbol: result.token.symbol, 706 | network 707 | }, null, 2) 708 | }] 709 | }; 710 | } catch (error) { 711 | return { 712 | content: [{ 713 | type: "text", 714 | text: `Error transferring tokens: ${error instanceof Error ? error.message : String(error)}` 715 | }], 716 | isError: true 717 | }; 718 | } 719 | } 720 | ); 721 | 722 | // CONTRACT TOOLS 723 | 724 | // Read contract 725 | server.tool( 726 | "read_contract", 727 | "Read data from a smart contract by calling a view/pure function. This doesn't modify blockchain state and doesn't require gas or signing.", 728 | { 729 | contractAddress: z.string().describe("The address of the smart contract to interact with"), 730 | abi: z.array(z.any()).describe("The ABI (Application Binary Interface) of the smart contract function, as a JSON array"), 731 | functionName: z.string().describe("The name of the function to call on the contract (e.g., 'balanceOf')"), 732 | args: z.array(z.any()).optional().describe("The arguments to pass to the function, as an array (e.g., ['0x1234...'])"), 733 | network: z.string().optional().describe("Network name (e.g., 'ethereum', 'optimism', 'arbitrum', 'base', 'polygon') or chain ID. Defaults to Ethereum mainnet.") 734 | }, 735 | async ({ contractAddress, abi, functionName, args = [], network = "ethereum" }) => { 736 | try { 737 | // Parse ABI if it's a string 738 | const parsedAbi = typeof abi === 'string' ? JSON.parse(abi) : abi; 739 | 740 | const params = { 741 | address: contractAddress as Address, 742 | abi: parsedAbi, 743 | functionName, 744 | args 745 | }; 746 | 747 | const result = await services.readContract(params, network); 748 | 749 | return { 750 | content: [{ 751 | type: "text", 752 | text: services.helpers.formatJson(result) 753 | }] 754 | }; 755 | } catch (error) { 756 | return { 757 | content: [{ 758 | type: "text", 759 | text: `Error reading contract: ${error instanceof Error ? error.message : String(error)}` 760 | }], 761 | isError: true 762 | }; 763 | } 764 | } 765 | ); 766 | 767 | // Write to contract 768 | server.tool( 769 | "write_contract", 770 | "Write data to a smart contract by calling a state-changing function. This modifies blockchain state and requires gas payment and transaction signing.", 771 | { 772 | contractAddress: z.string().describe("The address of the smart contract to interact with"), 773 | abi: z.array(z.any()).describe("The ABI (Application Binary Interface) of the smart contract function, as a JSON array"), 774 | functionName: z.string().describe("The name of the function to call on the contract (e.g., 'transfer')"), 775 | args: z.array(z.any()).describe("The arguments to pass to the function, as an array (e.g., ['0x1234...', '1000000000000000000'])"), 776 | privateKey: z.string().describe("Private key of the sending account in hex format (with or without 0x prefix). SECURITY: This is used only for transaction signing and is not stored."), 777 | network: z.string().optional().describe("Network name (e.g., 'ethereum', 'optimism', 'arbitrum', 'base', 'polygon') or chain ID. Defaults to Ethereum mainnet.") 778 | }, 779 | async ({ contractAddress, abi, functionName, args, privateKey, network = "ethereum" }) => { 780 | try { 781 | // Parse ABI if it's a string 782 | const parsedAbi = typeof abi === 'string' ? JSON.parse(abi) : abi; 783 | 784 | const contractParams: Record = { 785 | address: contractAddress as Address, 786 | abi: parsedAbi, 787 | functionName, 788 | args 789 | }; 790 | 791 | const txHash = await services.writeContract( 792 | privateKey as Hex, 793 | contractParams, 794 | network 795 | ); 796 | 797 | return { 798 | content: [{ 799 | type: "text", 800 | text: JSON.stringify({ 801 | network, 802 | transactionHash: txHash, 803 | message: "Contract write transaction sent successfully" 804 | }, null, 2) 805 | }] 806 | }; 807 | } catch (error) { 808 | return { 809 | content: [{ 810 | type: "text", 811 | text: `Error writing to contract: ${error instanceof Error ? error.message : String(error)}` 812 | }], 813 | isError: true 814 | }; 815 | } 816 | } 817 | ); 818 | 819 | // Check if address is a contract 820 | server.tool( 821 | "is_contract", 822 | "Check if an address is a smart contract or an externally owned account (EOA)", 823 | { 824 | address: z.string().describe("The wallet or contract address or ENS name to check (e.g., '0x1234...' or 'uniswap.eth')"), 825 | network: z.string().optional().describe("Network name (e.g., 'ethereum', 'optimism', 'arbitrum', 'base', etc.) or chain ID. Supports all EVM-compatible networks. Defaults to Ethereum mainnet.") 826 | }, 827 | async ({ address, network = "ethereum" }) => { 828 | try { 829 | const isContract = await services.isContract(address, network); 830 | 831 | return { 832 | content: [{ 833 | type: "text", 834 | text: JSON.stringify({ 835 | address, 836 | network, 837 | isContract, 838 | type: isContract ? "Contract" : "Externally Owned Account (EOA)" 839 | }, null, 2) 840 | }] 841 | }; 842 | } catch (error) { 843 | return { 844 | content: [{ 845 | type: "text", 846 | text: `Error checking if address is a contract: ${error instanceof Error ? error.message : String(error)}` 847 | }], 848 | isError: true 849 | }; 850 | } 851 | } 852 | ); 853 | 854 | // Get ERC20 token information 855 | server.tool( 856 | "get_token_info", 857 | "Get comprehensive information about an ERC20 token including name, symbol, decimals, total supply, and other metadata. Use this to analyze any token on EVM chains.", 858 | { 859 | tokenAddress: z.string().describe("The contract address of the ERC20 token (e.g., '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48' for USDC on Ethereum)"), 860 | network: z.string().optional().describe("Network name (e.g., 'ethereum', 'optimism', 'arbitrum', 'base', 'polygon') or chain ID. Defaults to Ethereum mainnet.") 861 | }, 862 | async ({ tokenAddress, network = "ethereum" }) => { 863 | try { 864 | const tokenInfo = await services.getERC20TokenInfo(tokenAddress as Address, network); 865 | 866 | return { 867 | content: [{ 868 | type: "text", 869 | text: JSON.stringify({ 870 | address: tokenAddress, 871 | network, 872 | ...tokenInfo 873 | }, null, 2) 874 | }] 875 | }; 876 | } catch (error) { 877 | return { 878 | content: [{ 879 | type: "text", 880 | text: `Error fetching token info: ${error instanceof Error ? error.message : String(error)}` 881 | }], 882 | isError: true 883 | }; 884 | } 885 | } 886 | ); 887 | 888 | // Get ERC20 token balance 889 | server.tool( 890 | "get_token_balance_erc20", 891 | "Get ERC20 token balance for an address", 892 | { 893 | address: z.string().describe("The address to check balance for"), 894 | tokenAddress: z.string().describe("The ERC20 token contract address"), 895 | network: z.string().optional().describe("Network name or chain ID. Defaults to Ethereum mainnet.") 896 | }, 897 | async ({ address, tokenAddress, network = "ethereum" }) => { 898 | try { 899 | const balance = await services.getERC20Balance( 900 | tokenAddress as Address, 901 | address as Address, 902 | network 903 | ); 904 | 905 | return { 906 | content: [{ 907 | type: "text", 908 | text: JSON.stringify({ 909 | address, 910 | tokenAddress, 911 | network, 912 | balance: { 913 | raw: balance.raw.toString(), 914 | formatted: balance.formatted, 915 | decimals: balance.token.decimals 916 | } 917 | }, null, 2) 918 | }] 919 | }; 920 | } catch (error) { 921 | return { 922 | content: [{ 923 | type: "text", 924 | text: `Error fetching ERC20 balance for ${address}: ${error instanceof Error ? error.message : String(error)}` 925 | }], 926 | isError: true 927 | }; 928 | } 929 | } 930 | ); 931 | 932 | // Get NFT (ERC721) information 933 | server.tool( 934 | "get_nft_info", 935 | "Get detailed information about a specific NFT (ERC721 token), including collection name, symbol, token URI, and current owner if available.", 936 | { 937 | tokenAddress: z.string().describe("The contract address of the NFT collection (e.g., '0xBC4CA0EdA7647A8aB7C2061c2E118A18a936f13D' for Bored Ape Yacht Club)"), 938 | tokenId: z.string().describe("The ID of the specific NFT token to query (e.g., '1234')"), 939 | network: z.string().optional().describe("Network name (e.g., 'ethereum', 'optimism', 'arbitrum', 'base', 'polygon') or chain ID. Most NFTs are on Ethereum mainnet, which is the default.") 940 | }, 941 | async ({ tokenAddress, tokenId, network = "ethereum" }) => { 942 | try { 943 | const nftInfo = await services.getERC721TokenMetadata( 944 | tokenAddress as Address, 945 | BigInt(tokenId), 946 | network 947 | ); 948 | 949 | // Check ownership separately 950 | let owner = null; 951 | try { 952 | // This may fail if tokenId doesn't exist 953 | owner = await services.getPublicClient(network).readContract({ 954 | address: tokenAddress as Address, 955 | abi: [{ 956 | inputs: [{ type: 'uint256' }], 957 | name: 'ownerOf', 958 | outputs: [{ type: 'address' }], 959 | stateMutability: 'view', 960 | type: 'function' 961 | }], 962 | functionName: 'ownerOf', 963 | args: [BigInt(tokenId)] 964 | }); 965 | } catch (e) { 966 | // Ownership info not available 967 | } 968 | 969 | return { 970 | content: [{ 971 | type: "text", 972 | text: JSON.stringify({ 973 | contract: tokenAddress, 974 | tokenId, 975 | network, 976 | ...nftInfo, 977 | owner: owner || 'Unknown' 978 | }, null, 2) 979 | }] 980 | }; 981 | } catch (error) { 982 | return { 983 | content: [{ 984 | type: "text", 985 | text: `Error fetching NFT info: ${error instanceof Error ? error.message : String(error)}` 986 | }], 987 | isError: true 988 | }; 989 | } 990 | } 991 | ); 992 | 993 | // Check NFT ownership 994 | server.tool( 995 | "check_nft_ownership", 996 | "Check if an address owns a specific NFT", 997 | { 998 | tokenAddress: z.string().describe("The contract address or ENS name of the NFT collection (e.g., '0xBC4CA0EdA7647A8aB7C2061c2E118A18a936f13D' for BAYC or 'boredapeyachtclub.eth')"), 999 | tokenId: z.string().describe("The ID of the NFT to check (e.g., '1234')"), 1000 | ownerAddress: z.string().describe("The wallet address or ENS name to check ownership against (e.g., '0x1234...' or 'vitalik.eth')"), 1001 | network: z.string().optional().describe("Network name (e.g., 'ethereum', 'optimism', 'arbitrum', 'base', etc.) or chain ID. Supports all EVM-compatible networks. Defaults to Ethereum mainnet.") 1002 | }, 1003 | async ({ tokenAddress, tokenId, ownerAddress, network = "ethereum" }) => { 1004 | try { 1005 | const isOwner = await services.isNFTOwner( 1006 | tokenAddress, 1007 | ownerAddress, 1008 | BigInt(tokenId), 1009 | network 1010 | ); 1011 | 1012 | return { 1013 | content: [{ 1014 | type: "text", 1015 | text: JSON.stringify({ 1016 | tokenAddress, 1017 | tokenId, 1018 | ownerAddress, 1019 | network, 1020 | isOwner, 1021 | result: isOwner ? "Address owns this NFT" : "Address does not own this NFT" 1022 | }, null, 2) 1023 | }] 1024 | }; 1025 | } catch (error) { 1026 | return { 1027 | content: [{ 1028 | type: "text", 1029 | text: `Error checking NFT ownership: ${error instanceof Error ? error.message : String(error)}` 1030 | }], 1031 | isError: true 1032 | }; 1033 | } 1034 | } 1035 | ); 1036 | 1037 | // Add tool for getting ERC1155 token URI 1038 | server.tool( 1039 | "get_erc1155_token_uri", 1040 | "Get the metadata URI for an ERC1155 token (multi-token standard used for both fungible and non-fungible tokens). The URI typically points to JSON metadata about the token.", 1041 | { 1042 | tokenAddress: z.string().describe("The contract address of the ERC1155 token collection (e.g., '0x76BE3b62873462d2142405439777e971754E8E77')"), 1043 | tokenId: z.string().describe("The ID of the specific token to query metadata for (e.g., '1234')"), 1044 | network: z.string().optional().describe("Network name (e.g., 'ethereum', 'optimism', 'arbitrum', 'base', 'polygon') or chain ID. ERC1155 tokens exist across many networks. Defaults to Ethereum mainnet.") 1045 | }, 1046 | async ({ tokenAddress, tokenId, network = "ethereum" }) => { 1047 | try { 1048 | const uri = await services.getERC1155TokenURI( 1049 | tokenAddress as Address, 1050 | BigInt(tokenId), 1051 | network 1052 | ); 1053 | 1054 | return { 1055 | content: [{ 1056 | type: "text", 1057 | text: JSON.stringify({ 1058 | contract: tokenAddress, 1059 | tokenId, 1060 | network, 1061 | uri 1062 | }, null, 2) 1063 | }] 1064 | }; 1065 | } catch (error) { 1066 | return { 1067 | content: [{ 1068 | type: "text", 1069 | text: `Error fetching ERC1155 token URI: ${error instanceof Error ? error.message : String(error)}` 1070 | }], 1071 | isError: true 1072 | }; 1073 | } 1074 | } 1075 | ); 1076 | 1077 | // Add tool for getting ERC721 NFT balance 1078 | server.tool( 1079 | "get_nft_balance", 1080 | "Get the total number of NFTs owned by an address from a specific collection. This returns the count of NFTs, not individual token IDs.", 1081 | { 1082 | tokenAddress: z.string().describe("The contract address of the NFT collection (e.g., '0xBC4CA0EdA7647A8aB7C2061c2E118A18a936f13D' for Bored Ape Yacht Club)"), 1083 | ownerAddress: z.string().describe("The wallet address to check the NFT balance for (e.g., '0x1234...')"), 1084 | network: z.string().optional().describe("Network name (e.g., 'ethereum', 'optimism', 'arbitrum', 'base', 'polygon') or chain ID. Most NFTs are on Ethereum mainnet, which is the default.") 1085 | }, 1086 | async ({ tokenAddress, ownerAddress, network = "ethereum" }) => { 1087 | try { 1088 | const balance = await services.getERC721Balance( 1089 | tokenAddress as Address, 1090 | ownerAddress as Address, 1091 | network 1092 | ); 1093 | 1094 | return { 1095 | content: [{ 1096 | type: "text", 1097 | text: JSON.stringify({ 1098 | collection: tokenAddress, 1099 | owner: ownerAddress, 1100 | network, 1101 | balance: balance.toString() 1102 | }, null, 2) 1103 | }] 1104 | }; 1105 | } catch (error) { 1106 | return { 1107 | content: [{ 1108 | type: "text", 1109 | text: `Error fetching NFT balance: ${error instanceof Error ? error.message : String(error)}` 1110 | }], 1111 | isError: true 1112 | }; 1113 | } 1114 | } 1115 | ); 1116 | 1117 | // Add tool for getting ERC1155 token balance 1118 | server.tool( 1119 | "get_erc1155_balance", 1120 | "Get the balance of a specific ERC1155 token ID owned by an address. ERC1155 allows multiple tokens of the same ID, so the balance can be greater than 1.", 1121 | { 1122 | tokenAddress: z.string().describe("The contract address of the ERC1155 token collection (e.g., '0x76BE3b62873462d2142405439777e971754E8E77')"), 1123 | tokenId: z.string().describe("The ID of the specific token to check the balance for (e.g., '1234')"), 1124 | ownerAddress: z.string().describe("The wallet address to check the token balance for (e.g., '0x1234...')"), 1125 | network: z.string().optional().describe("Network name (e.g., 'ethereum', 'optimism', 'arbitrum', 'base', 'polygon') or chain ID. ERC1155 tokens exist across many networks. Defaults to Ethereum mainnet.") 1126 | }, 1127 | async ({ tokenAddress, tokenId, ownerAddress, network = "ethereum" }) => { 1128 | try { 1129 | const balance = await services.getERC1155Balance( 1130 | tokenAddress as Address, 1131 | ownerAddress as Address, 1132 | BigInt(tokenId), 1133 | network 1134 | ); 1135 | 1136 | return { 1137 | content: [{ 1138 | type: "text", 1139 | text: JSON.stringify({ 1140 | contract: tokenAddress, 1141 | tokenId, 1142 | owner: ownerAddress, 1143 | network, 1144 | balance: balance.toString() 1145 | }, null, 2) 1146 | }] 1147 | }; 1148 | } catch (error) { 1149 | return { 1150 | content: [{ 1151 | type: "text", 1152 | text: `Error fetching ERC1155 token balance: ${error instanceof Error ? error.message : String(error)}` 1153 | }], 1154 | isError: true 1155 | }; 1156 | } 1157 | } 1158 | ); 1159 | 1160 | // WALLET TOOLS 1161 | 1162 | // Get address from private key 1163 | server.tool( 1164 | "get_address_from_private_key", 1165 | "Get the EVM address derived from a private key", 1166 | { 1167 | privateKey: z.string().describe("Private key in hex format (with or without 0x prefix). SECURITY: This is used only for address derivation and is not stored.") 1168 | }, 1169 | async ({ privateKey }) => { 1170 | try { 1171 | // Ensure the private key has 0x prefix 1172 | const formattedKey = privateKey.startsWith('0x') ? privateKey as Hex : `0x${privateKey}` as Hex; 1173 | 1174 | const address = services.getAddressFromPrivateKey(formattedKey); 1175 | 1176 | return { 1177 | content: [{ 1178 | type: "text", 1179 | text: JSON.stringify({ 1180 | address, 1181 | privateKey: "0x" + privateKey.replace(/^0x/, '') 1182 | }, null, 2) 1183 | }] 1184 | }; 1185 | } catch (error) { 1186 | return { 1187 | content: [{ 1188 | type: "text", 1189 | text: `Error deriving address from private key: ${error instanceof Error ? error.message : String(error)}` 1190 | }], 1191 | isError: true 1192 | }; 1193 | } 1194 | } 1195 | ); 1196 | } -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; 2 | import startServer from "./server/server.js"; 3 | 4 | // Start the server 5 | async function main() { 6 | try { 7 | const server = await startServer(); 8 | const transport = new StdioServerTransport(); 9 | await server.connect(transport); 10 | console.error("EVM MCP Server running on stdio"); 11 | } catch (error) { 12 | console.error("Error starting MCP server:", error); 13 | process.exit(1); 14 | } 15 | } 16 | 17 | main().catch((error) => { 18 | console.error("Fatal error in main():", error); 19 | process.exit(1); 20 | }); -------------------------------------------------------------------------------- /src/server/http-server.ts: -------------------------------------------------------------------------------- 1 | import { config } from "dotenv"; 2 | import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js"; 3 | import startServer from "./server.js"; 4 | import express, { Request, Response } from "express"; 5 | import cors from "cors"; 6 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; 7 | 8 | // Environment variables - hardcoded values 9 | const PORT = 3001; 10 | const HOST = '0.0.0.0'; 11 | 12 | console.error(`Configured to listen on ${HOST}:${PORT}`); 13 | 14 | // Setup Express 15 | const app = express(); 16 | app.use(express.json()); 17 | app.use(cors({ 18 | origin: '*', 19 | methods: ['GET', 'POST', 'OPTIONS'], 20 | allowedHeaders: ['Content-Type', 'Authorization'], 21 | credentials: true, 22 | exposedHeaders: ['Content-Type', 'Access-Control-Allow-Origin'] 23 | })); 24 | 25 | // Add OPTIONS handling for preflight requests 26 | app.options('*', cors()); 27 | 28 | // Keep track of active connections with session IDs 29 | const connections = new Map(); 30 | 31 | // Initialize the server 32 | let server: McpServer | null = null; 33 | startServer().then(s => { 34 | server = s; 35 | console.error("MCP Server initialized successfully"); 36 | }).catch(error => { 37 | console.error("Failed to initialize server:", error); 38 | process.exit(1); 39 | }); 40 | 41 | // Define routes 42 | // @ts-ignore 43 | app.get("/sse", (req: Request, res: Response) => { 44 | console.error(`Received SSE connection request from ${req.ip}`); 45 | console.error(`Query parameters: ${JSON.stringify(req.query)}`); 46 | 47 | // Set CORS headers explicitly 48 | res.setHeader('Access-Control-Allow-Origin', '*'); 49 | res.setHeader('Access-Control-Allow-Methods', 'GET, OPTIONS'); 50 | res.setHeader('Access-Control-Allow-Headers', 'Content-Type'); 51 | 52 | if (!server) { 53 | console.error("Server not initialized yet, rejecting SSE connection"); 54 | return res.status(503).send("Server not initialized"); 55 | } 56 | 57 | // Generate a unique session ID if one is not provided 58 | // The sessionId is crucial for mapping SSE connections to message handlers 59 | const sessionId = generateSessionId(); 60 | console.error(`Creating SSE session with ID: ${sessionId}`); 61 | 62 | // Set SSE headers 63 | res.setHeader("Content-Type", "text/event-stream"); 64 | res.setHeader("Cache-Control", "no-cache, no-transform"); 65 | res.setHeader("Connection", "keep-alive"); 66 | 67 | // Create transport - handle before writing to response 68 | try { 69 | console.error(`Creating SSE transport for session: ${sessionId}`); 70 | 71 | // Create and store the transport keyed by session ID 72 | // Note: The path must match what the client expects (typically "/messages") 73 | const transport = new SSEServerTransport("/messages", res); 74 | connections.set(sessionId, transport); 75 | 76 | // Handle connection close 77 | req.on("close", () => { 78 | console.error(`SSE connection closed for session: ${sessionId}`); 79 | connections.delete(sessionId); 80 | }); 81 | 82 | // Connect transport to server - this must happen before sending any data 83 | server.connect(transport).then(() => { 84 | // Send an initial event with the session ID for the client to use in messages 85 | // Only send this after the connection is established 86 | console.error(`SSE connection established for session: ${sessionId}`); 87 | 88 | // Send the session ID to the client 89 | res.write(`data: ${JSON.stringify({ type: "session_init", sessionId })}\n\n`); 90 | }).catch((error: Error) => { 91 | console.error(`Error connecting transport to server: ${error}`); 92 | connections.delete(sessionId); 93 | }); 94 | } catch (error) { 95 | console.error(`Error creating SSE transport: ${error}`); 96 | connections.delete(sessionId); 97 | res.status(500).send(`Internal server error: ${error}`); 98 | } 99 | }); 100 | 101 | // @ts-ignore 102 | app.post("/messages", (req: Request, res: Response) => { 103 | // Extract the session ID from the URL query parameters 104 | let sessionId = req.query.sessionId?.toString(); 105 | 106 | // If no sessionId is provided and there's only one connection, use that 107 | if (!sessionId && connections.size === 1) { 108 | sessionId = Array.from(connections.keys())[0]; 109 | console.error(`No sessionId provided, using the only active session: ${sessionId}`); 110 | } 111 | 112 | console.error(`Received message for sessionId ${sessionId}`); 113 | console.error(`Message body: ${JSON.stringify(req.body)}`); 114 | 115 | // Set CORS headers 116 | res.setHeader('Access-Control-Allow-Origin', '*'); 117 | res.setHeader('Access-Control-Allow-Methods', 'POST, OPTIONS'); 118 | res.setHeader('Access-Control-Allow-Headers', 'Content-Type'); 119 | 120 | if (!server) { 121 | console.error("Server not initialized yet"); 122 | return res.status(503).json({ error: "Server not initialized" }); 123 | } 124 | 125 | if (!sessionId) { 126 | console.error("No session ID provided and multiple connections exist"); 127 | return res.status(400).json({ 128 | error: "No session ID provided. Please provide a sessionId query parameter or connect to /sse first.", 129 | activeConnections: connections.size 130 | }); 131 | } 132 | 133 | const transport = connections.get(sessionId); 134 | if (!transport) { 135 | console.error(`Session not found: ${sessionId}`); 136 | return res.status(404).json({ error: "Session not found" }); 137 | } 138 | 139 | console.error(`Handling message for session: ${sessionId}`); 140 | try { 141 | transport.handlePostMessage(req, res).catch((error: Error) => { 142 | console.error(`Error handling post message: ${error}`); 143 | res.status(500).json({ error: `Internal server error: ${error.message}` }); 144 | }); 145 | } catch (error) { 146 | console.error(`Exception handling post message: ${error}`); 147 | res.status(500).json({ error: `Internal server error: ${error}` }); 148 | } 149 | }); 150 | 151 | // Add a simple health check endpoint 152 | app.get("/health", (req: Request, res: Response) => { 153 | res.status(200).json({ 154 | status: "ok", 155 | server: server ? "initialized" : "initializing", 156 | activeConnections: connections.size, 157 | connectedSessionIds: Array.from(connections.keys()) 158 | }); 159 | }); 160 | 161 | // Add a root endpoint for basic info 162 | app.get("/", (req: Request, res: Response) => { 163 | res.status(200).json({ 164 | name: "MCP Server", 165 | version: "1.0.0", 166 | endpoints: { 167 | sse: "/sse", 168 | messages: "/messages", 169 | health: "/health" 170 | }, 171 | status: server ? "ready" : "initializing", 172 | activeConnections: connections.size 173 | }); 174 | }); 175 | 176 | // Helper function to generate a UUID-like session ID 177 | function generateSessionId(): string { 178 | return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { 179 | const r = Math.random() * 16 | 0; 180 | const v = c === 'x' ? r : (r & 0x3 | 0x8); 181 | return v.toString(16); 182 | }); 183 | } 184 | 185 | // Handle process termination gracefully 186 | process.on('SIGINT', () => { 187 | console.error('Shutting down server...'); 188 | connections.forEach((transport, sessionId) => { 189 | console.error(`Closing connection for session: ${sessionId}`); 190 | }); 191 | process.exit(0); 192 | }); 193 | 194 | // Start the HTTP server on a different port (3001) to avoid conflicts 195 | const httpServer = app.listen(PORT, HOST, () => { 196 | console.error(`Template MCP Server running at http://${HOST}:${PORT}`); 197 | console.error(`SSE endpoint: http://${HOST}:${PORT}/sse`); 198 | console.error(`Messages endpoint: http://${HOST}:${PORT}/messages (sessionId optional if only one connection)`); 199 | console.error(`Health check: http://${HOST}:${PORT}/health`); 200 | }).on('error', (err: Error) => { 201 | console.error(`Server error: ${err}`); 202 | }); -------------------------------------------------------------------------------- /src/server/server.ts: -------------------------------------------------------------------------------- 1 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; 2 | import { registerEVMResources } from "../core/resources.js"; 3 | import { registerEVMTools } from "../core/tools.js"; 4 | import { registerEVMPrompts } from "../core/prompts.js"; 5 | import { getSupportedNetworks } from "../core/chains.js"; 6 | 7 | // Create and start the MCP server 8 | async function startServer() { 9 | try { 10 | // Create a new MCP server instance 11 | const server = new McpServer({ 12 | name: "EVM-Server", 13 | version: "1.0.0" 14 | }); 15 | 16 | // Register all resources, tools, and prompts 17 | registerEVMResources(server); 18 | registerEVMTools(server); 19 | registerEVMPrompts(server); 20 | 21 | // Log server information 22 | console.error(`EVM MCP Server initialized`); 23 | console.error(`Supported networks: ${getSupportedNetworks().join(", ")}`); 24 | console.error("Server is ready to handle requests"); 25 | 26 | return server; 27 | } catch (error) { 28 | console.error("Failed to initialize server:", error); 29 | process.exit(1); 30 | } 31 | } 32 | 33 | // Export the server creation function 34 | export default startServer; -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "module": "NodeNext", 5 | "moduleResolution": "NodeNext", 6 | "esModuleInterop": true, 7 | "strict": true, 8 | "skipLibCheck": true, 9 | "outDir": "dist", 10 | "sourceMap": true, 11 | "declaration": true, 12 | "resolveJsonModule": true 13 | }, 14 | "include": ["src/**/*"], 15 | "exclude": ["node_modules", "dist"] 16 | } 17 | --------------------------------------------------------------------------------