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