diff --git a/README.md b/README.md index bf74039..322b31a 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,12 @@ # EVM MCP Server ![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg) -![EVM Networks](https://img.shields.io/badge/Networks-30+-green) -![TypeScript](https://img.shields.io/badge/TypeScript-5.0+-3178C6) -![Viem](https://img.shields.io/badge/Viem-1.0+-green) +![EVM Networks](https://img.shields.io/badge/Networks-60+-green) +![TypeScript](https://img.shields.io/badge/TypeScript-5.8+-3178C6) +![MCP](https://img.shields.io/badge/MCP-1.22.0+-blue) +![Viem](https://img.shields.io/badge/Viem-2.39.3+-green) -A comprehensive Model Context Protocol (MCP) server that provides blockchain services across multiple EVM-compatible networks. This server enables AI agents to interact with Ethereum, Optimism, Arbitrum, Base, Polygon, and many other EVM chains with a unified interface. +A comprehensive Model Context Protocol (MCP) server that provides blockchain services across 60+ EVM-compatible networks. This server enables AI agents to interact with Ethereum, Optimism, Arbitrum, Base, Polygon, and many other EVM chains with a unified interface through 22 tools and 10 AI-guided prompts. ## 📋 Contents @@ -14,10 +15,13 @@ A comprehensive Model Context Protocol (MCP) server that provides blockchain ser - [Supported Networks](#supported-networks) - [Prerequisites](#prerequisites) - [Installation](#installation) -- [Server Configuration](#server-configuration) +- [Configuration](#configuration) + - [Environment Variables](#environment-variables) + - [Server Configuration](#server-configuration) - [Usage](#usage) - [API Reference](#api-reference) - [Tools](#tools) + - [Prompts](#prompts) - [Resources](#resources) - [Security Considerations](#security-considerations) - [Project Structure](#project-structure) @@ -29,19 +33,20 @@ A comprehensive Model Context Protocol (MCP) server that provides blockchain ser The MCP EVM Server leverages the Model Context Protocol to provide blockchain services to AI agents. It supports a wide range of services including: - Reading blockchain state (balances, transactions, blocks, etc.) -- Interacting with smart contracts +- Interacting with smart contracts with **automatic ABI fetching** from block explorers - Transferring tokens (native, ERC20, ERC721, ERC1155) - Querying token metadata and balances -- Chain-specific services across 30+ EVM networks +- Chain-specific services across 60+ EVM networks (34 mainnets + 26 testnets) - **ENS name resolution** for all address parameters (use human-readable names like 'vitalik.eth' instead of addresses) +- **AI-friendly prompts** that guide agents through complex workflows -All services are exposed through a consistent interface of MCP tools and resources, making it easy for AI agents to discover and use blockchain functionality. **Every tool that accepts Ethereum addresses also supports ENS names**, automatically resolving them to addresses behind the scenes. +All services are exposed through a consistent interface of MCP tools, resources, and prompts, making it easy for AI agents to discover and use blockchain functionality. **Every tool that accepts Ethereum addresses also supports ENS names**, automatically resolving them to addresses behind the scenes. The server includes intelligent ABI fetching, eliminating the need to know contract ABIs in advance. ## ✨ Features ### Blockchain Data Access -- **Multi-chain support** for 30+ EVM-compatible networks +- **Multi-chain support** for 60+ EVM-compatible networks (34 mainnets + 26 testnets) - **Chain information** including blockNumber, chainId, and RPCs - **Block data** access by number, hash, or latest - **Transaction details** and receipts with decoded logs @@ -70,17 +75,30 @@ All services are exposed through a consistent interface of MCP tools and resourc ### Smart Contract Interactions - **Read contract state** through view/pure functions -- **Write services** with private key signing +- **Write to contracts** - Execute any state-changing function with automatic ABI fetching - **Contract verification** to distinguish from EOAs - **Event logs** retrieval and filtering +- **Automatic ABI fetching** from Etherscan v2 API across all 60+ networks (no need to know ABIs in advance) +- **ABI parsing and validation** with function discovery ### Comprehensive Transaction Support +- **Flexible Wallet Support** - Configure with Private Key or Mnemonic (BIP-39) with HD path support - **Native token transfers** across all supported networks - **Gas estimation** for transaction planning - **Transaction status** and receipt information - **Error handling** with descriptive messages +### AI-Guided Workflows (Prompts) + +- **Transaction preparation** - Guidance for planning and executing transfers +- **Wallet analysis** - Tools for analyzing wallet activity and holdings +- **Smart contract exploration** - Interactive ABI fetching and contract analysis +- **Contract interaction** - Safe execution of write operations on smart contracts +- **Network information** - Learning about EVM networks and comparisons +- **Approval auditing** - Reviewing and managing token approvals +- **Error diagnosis** - Troubleshooting transaction failures + ## 🌐 Supported Networks ### Mainnets @@ -144,8 +162,9 @@ All services are exposed through a consistent interface of MCP tools and resourc ## 🛠️ Prerequisites -- [Bun](https://bun.sh/) 1.0.0 or higher -- Node.js 18.0.0 or higher (if not using Bun) +- [Bun](https://bun.sh/) 1.0.0 or higher (recommended) +- Node.js 20.0.0 or higher (if not using Bun) +- Optional: [Etherscan API key](https://etherscan.io/apis) for ABI fetching ## 📦 Installation @@ -161,7 +180,63 @@ bun install npm install ``` -## ⚙️ Server Configuration +## ⚙️ Configuration + +### Environment Variables + +The server uses the following environment variables. For write operations and ABI fetching, you must configure these variables: + +#### Wallet Configuration (For Write Operations) + +You can configure your wallet using **either** a private key or a mnemonic phrase: + +**Option 1: Private Key** + +```bash +export EVM_PRIVATE_KEY="0x..." # Your private key in hex format (with or without 0x prefix) +``` + +**Option 2: Mnemonic Phrase (Recommended for HD Wallets)** + +```bash +export EVM_MNEMONIC="word1 word2 word3 ... word12" # Your 12 or 24 word BIP-39 mnemonic +export EVM_ACCOUNT_INDEX="0" # Optional: Account index for HD wallet derivation (default: 0) +``` + +The mnemonic option supports hierarchical deterministic (HD) wallet derivation: +- Uses BIP-39 standard mnemonic phrases (12 or 24 words) +- Supports BIP-44 derivation path: `m/44'/60'/0'/0/{accountIndex}` +- `EVM_ACCOUNT_INDEX` allows you to derive different accounts from the same mnemonic +- Default account index is 0 (first account) + +**Wallet is used for:** +- Transferring native tokens (`transfer_native` tool) +- Transferring ERC20 tokens (`transfer_erc20` tool) +- Approving token spending (`approve_token_spending` tool) +- Writing to smart contracts (`write_contract` tool) + +⚠️ **Security**: +- Never commit your private key or mnemonic to version control +- Use environment variables or a secure key management system +- Store mnemonics securely - they provide access to all derived accounts +- Consider using different account indices for different purposes + +#### API Keys (For ABI Fetching) + +```bash +export ETHERSCAN_API_KEY="your-api-key-here" +``` + +This API key is optional but required for: +- Automatic ABI fetching from block explorers (`get_contract_abi` tool) +- Auto-fetching ABIs when reading contracts (`read_contract` tool with `abiJson` parameter) +- The `fetch_and_analyze_abi` prompt + +Get your free API key from: +- [Etherscan](https://etherscan.io/apis) - For Ethereum and compatible chains +- The same key works across all 60+ EVM networks via the Etherscan v2 API + +### Server Configuration The server uses the following default configuration: @@ -389,36 +464,69 @@ console.log(result); ### Tools -The server provides the following MCP tools for agents. **All tools that accept address parameters support both Ethereum addresses and ENS names.** +The server provides 22 focused MCP tools for agents. **All tools that accept address parameters support both Ethereum addresses and ENS names.** -#### Token services +#### Wallet Information | Tool Name | Description | Key Parameters | |-----------|-------------|----------------| -| `get-token-info` | Get ERC20 token metadata | `tokenAddress` (address/ENS), `network` | -| `get-token-balance` | Check ERC20 token balance | `tokenAddress` (address/ENS), `ownerAddress` (address/ENS), `network` | -| `transfer-token` | Transfer ERC20 tokens | `privateKey`, `tokenAddress` (address/ENS), `toAddress` (address/ENS), `amount`, `network` | -| `approve-token-spending` | Approve token allowances | `privateKey`, `tokenAddress` (address/ENS), `spenderAddress` (address/ENS), `amount`, `network` | -| `get-nft-info` | Get NFT metadata | `tokenAddress` (address/ENS), `tokenId`, `network` | -| `check-nft-ownership` | Verify NFT ownership | `tokenAddress` (address/ENS), `tokenId`, `ownerAddress` (address/ENS), `network` | -| `transfer-nft` | Transfer an NFT | `privateKey`, `tokenAddress` (address/ENS), `tokenId`, `toAddress` (address/ENS), `network` | -| `get-nft-balance` | Count NFTs owned | `tokenAddress` (address/ENS), `ownerAddress` (address/ENS), `network` | -| `get-erc1155-token-uri` | Get ERC1155 metadata | `tokenAddress` (address/ENS), `tokenId`, `network` | -| `get-erc1155-balance` | Check ERC1155 balance | `tokenAddress` (address/ENS), `tokenId`, `ownerAddress` (address/ENS), `network` | -| `transfer-erc1155` | Transfer ERC1155 tokens | `privateKey`, `tokenAddress` (address/ENS), `tokenId`, `amount`, `toAddress` (address/ENS), `network` | - -#### Blockchain services +| `get_wallet_address` | Get the address of the configured wallet (from EVM_PRIVATE_KEY) | none | + +#### Network Information + +| Tool Name | Description | Key Parameters | +|-----------|-------------|----------------| +| `get_chain_info` | Get network information | `network` | +| `get_supported_networks` | List all supported EVM networks | none | +| `get_gas_price` | Get current gas prices on a network | `network` | + +#### ENS Services + +| Tool Name | Description | Key Parameters | +|-----------|-------------|----------------| +| `resolve_ens_name` | Resolve ENS name to address | `ensName`, `network` | +| `lookup_ens_address` | Reverse lookup address to ENS name | `address`, `network` | + +#### Block & Transaction Information + +| Tool Name | Description | Key Parameters | +|-----------|-------------|----------------| +| `get_block` | Get block data | `blockNumber` or `blockHash`, `network` | +| `get_latest_block` | Get latest block data | `network` | +| `get_transaction` | Get transaction details | `txHash`, `network` | +| `get_transaction_receipt` | Get transaction receipt with logs | `txHash`, `network` | +| `wait_for_transaction` | Wait for transaction confirmation | `txHash`, `confirmations`, `network` | + +#### Balance & Token Information + +| Tool Name | Description | Key Parameters | +|-----------|-------------|----------------| +| `get_balance` | Get native token balance | `address` (address/ENS), `network` | +| `get_token_balance` | Check ERC20 token balance | `tokenAddress` (address/ENS), `ownerAddress` (address/ENS), `network` | +| `get_allowance` | Check token spending allowance | `tokenAddress` (address/ENS), `ownerAddress` (address/ENS), `spenderAddress` (address/ENS), `network` | + +#### Smart Contract Interactions + +| Tool Name | Description | Key Parameters | +|-----------|-------------|----------------| +| `get_contract_abi` | Fetch contract ABI from block explorer (60+ networks) | `contractAddress` (address/ENS), `network` | +| `read_contract` | Read smart contract state (auto-fetches ABI if needed) | `contractAddress`, `functionName`, `args[]`, `abiJson` (optional), `network` | +| `write_contract` | Execute state-changing functions (auto-fetches ABI if needed) | `contractAddress`, `functionName`, `args[]`, `value` (optional), `abiJson` (optional), `network` | + +#### Token Transfers + +| Tool Name | Description | Key Parameters | +|-----------|-------------|----------------| +| `transfer_native` | Send native tokens (ETH, etc.) | `to` (address/ENS), `amount`, `network` | +| `transfer_erc20` | Transfer ERC20 tokens | `tokenAddress` (address/ENS), `to` (address/ENS), `amount`, `network` | +| `approve_token_spending` | Approve token allowances | `tokenAddress` (address/ENS), `spenderAddress` (address/ENS), `amount`, `network` | + +#### NFT Services | Tool Name | Description | Key Parameters | |-----------|-------------|----------------| -| `get-chain-info` | Get network information | `network` | -| `get-balance` | Get native token balance | `address` (address/ENS), `network` | -| `transfer-eth` | Send native tokens | `privateKey`, `to` (address/ENS), `amount`, `network` | -| `get-transaction` | Get transaction details | `txHash`, `network` | -| `read-contract` | Read smart contract state | `contractAddress` (address/ENS), `abi`, `functionName`, `args`, `network` | -| `write-contract` | Write to smart contract | `contractAddress` (address/ENS), `abi`, `functionName`, `args`, `privateKey`, `network` | -| `is-contract` | Check if address is a contract | `address` (address/ENS), `network` | -| `resolve-ens` | Resolve ENS name to address | `ensName`, `network` | +| `get_nft_info` | Get NFT (ERC721) metadata | `tokenAddress` (address/ENS), `tokenId`, `network` | +| `get_erc1155_balance` | Check ERC1155 balance | `tokenAddress` (address/ENS), `tokenId`, `ownerAddress` (address/ENS), `network` | ### Resources diff --git a/bun.lock b/bun.lock index 40cb91b..1179d0d 100644 --- a/bun.lock +++ b/bun.lock @@ -1,19 +1,20 @@ { "lockfileVersion": 1, + "configVersion": 0, "workspaces": { "": { "name": "mcp-evm-server", "dependencies": { - "@modelcontextprotocol/sdk": "^1.6.1", - "cors": "^2.8.5", + "@modelcontextprotocol/sdk": "^1.22.0", "express": "^4.21.2", - "viem": "^2.23.8", - "zod": "^3.24.2", + "viem": "^2.39.3", + "zod": "^3.24.3", }, "devDependencies": { "@types/bun": "latest", - "@types/cors": "^2.8.17", "@types/express": "^5.0.0", + "@types/node": "^22.0.0", + "conventional-changelog-cli": "^5.0.0", }, "peerDependencies": { "typescript": "^5.8.2", @@ -23,52 +24,72 @@ "packages": { "@adraffy/ens-normalize": ["@adraffy/ens-normalize@1.11.0", "", {}, "sha512-/3DDPKHqqIqxUULp8yP4zODUY1i+2xvVWsv8A79xGWdCAG+8sb0hRh0Rk2QyOJUnnbyPUAZYcpBuRe3nS2OIUg=="], - "@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.7.0", "", { "dependencies": { "content-type": "^1.0.5", "cors": "^2.8.5", "eventsource": "^3.0.2", "express": "^5.0.1", "express-rate-limit": "^7.5.0", "pkce-challenge": "^4.1.0", "raw-body": "^3.0.0", "zod": "^3.23.8", "zod-to-json-schema": "^3.24.1" } }, "sha512-IYPe/FLpvF3IZrd/f5p5ffmWhMc3aEMuM2wGJASDqC2Ge7qatVCdbfPx3n/5xFeb19xN0j/911M2AaFuircsWA=="], + "@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="], - "@noble/curves": ["@noble/curves@1.8.1", "", { "dependencies": { "@noble/hashes": "1.7.1" } }, "sha512-warwspo+UYUPep0Q+vtdVB4Ugn8GGQj8iyB3gnRWsztmUHTI3S1nhdiWNsPUGL0vud7JlRRk1XEu7Lq1KGTnMQ=="], + "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="], - "@noble/hashes": ["@noble/hashes@1.7.1", "", {}, "sha512-B8XBPsn4vT/KJAGqDzbwztd+6Yte3P4V7iafm24bxgDe/mlRuK6xmWPuCNrKt2vDafZ8MfJLlchDG/vYafQEjQ=="], + "@conventional-changelog/git-client": ["@conventional-changelog/git-client@1.0.1", "", { "dependencies": { "@types/semver": "^7.5.5", "semver": "^7.5.2" }, "peerDependencies": { "conventional-commits-filter": "^5.0.0", "conventional-commits-parser": "^6.0.0" }, "optionalPeers": ["conventional-commits-filter", "conventional-commits-parser"] }, "sha512-PJEqBwAleffCMETaVm/fUgHldzBE35JFk3/9LL6NUA5EXa3qednu+UT6M7E5iBu3zIQZCULYIiZ90fBYHt6xUw=="], - "@scure/base": ["@scure/base@1.2.4", "", {}, "sha512-5Yy9czTO47mqz+/J8GM6GIId4umdCk1wc1q8rKERQulIoc8VP9pzDcghv10Tl2E7R96ZUx/PhND3ESYUQX8NuQ=="], + "@hutson/parse-repository-url": ["@hutson/parse-repository-url@5.0.0", "", {}, "sha512-e5+YUKENATs1JgYHMzTr2MW/NDcXGfYFAuOQU8gJgF/kEh4EqKgfGrfLI67bMD4tbhZVlkigz/9YYwWcbOFthg=="], - "@scure/bip32": ["@scure/bip32@1.6.2", "", { "dependencies": { "@noble/curves": "~1.8.1", "@noble/hashes": "~1.7.1", "@scure/base": "~1.2.2" } }, "sha512-t96EPDMbtGgtb7onKKqxRLfE5g05k7uHnHRM2xdE6BP/ZmxaLtPek4J4KfVn/90IQNrU1IOAqMgiDtUdtbe3nw=="], + "@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.22.0", "", { "dependencies": { "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.0.1", "express-rate-limit": "^7.5.0", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.23.8", "zod-to-json-schema": "^3.24.1" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-VUpl106XVTCpDmTBil2ehgJZjhyLY2QZikzF8NvTXtLRF1CvO5iEE2UNZdVIUer35vFOwMKYeUGbjJtvPWan3g=="], - "@scure/bip39": ["@scure/bip39@1.5.4", "", { "dependencies": { "@noble/hashes": "~1.7.1", "@scure/base": "~1.2.4" } }, "sha512-TFM4ni0vKvCfBpohoh+/lY05i9gRbSwXWngAsF4CABQxoaOHijxuaZ2R6cStDQ5CHtHO9aGJTr4ksVJASRRyMA=="], + "@noble/ciphers": ["@noble/ciphers@1.3.0", "", {}, "sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw=="], - "@types/body-parser": ["@types/body-parser@1.19.5", "", { "dependencies": { "@types/connect": "*", "@types/node": "*" } }, "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg=="], + "@noble/curves": ["@noble/curves@1.9.1", "", { "dependencies": { "@noble/hashes": "1.8.0" } }, "sha512-k11yZxZg+t+gWvBbIswW0yoJlu8cHOC7dhunwOzoWH/mXGBiYyR4YY6hAEK/3EUs4UpB8la1RfdRpeGsFHkWsA=="], + + "@noble/hashes": ["@noble/hashes@1.8.0", "", {}, "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A=="], + + "@scure/base": ["@scure/base@1.2.6", "", {}, "sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg=="], + + "@scure/bip32": ["@scure/bip32@1.7.0", "", { "dependencies": { "@noble/curves": "~1.9.0", "@noble/hashes": "~1.8.0", "@scure/base": "~1.2.5" } }, "sha512-E4FFX/N3f4B80AKWp5dP6ow+flD1LQZo/w8UnLGYZO674jS6YnYeepycOOksv+vLPSpgN35wgKgy+ybfTb2SMw=="], + + "@scure/bip39": ["@scure/bip39@1.6.0", "", { "dependencies": { "@noble/hashes": "~1.8.0", "@scure/base": "~1.2.5" } }, "sha512-+lF0BbLiJNwVlev4eKelw1WWLaiKXw7sSl8T6FvBlWkdX+94aGJ4o8XjUdlyhTCjd8c+B3KT3JfS8P0bLRNU6A=="], + + "@types/body-parser": ["@types/body-parser@1.19.6", "", { "dependencies": { "@types/connect": "*", "@types/node": "*" } }, "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g=="], "@types/bun": ["@types/bun@1.2.5", "", { "dependencies": { "bun-types": "1.2.5" } }, "sha512-w2OZTzrZTVtbnJew1pdFmgV99H0/L+Pvw+z1P67HaR18MHOzYnTYOi6qzErhK8HyT+DB782ADVPPE92Xu2/Opg=="], "@types/connect": ["@types/connect@3.4.38", "", { "dependencies": { "@types/node": "*" } }, "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug=="], - "@types/cors": ["@types/cors@2.8.17", "", { "dependencies": { "@types/node": "*" } }, "sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA=="], + "@types/express": ["@types/express@5.0.5", "", { "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^5.0.0", "@types/serve-static": "^1" } }, "sha512-LuIQOcb6UmnF7C1PCFmEU1u2hmiHL43fgFQX67sN3H4Z+0Yk0Neo++mFsBjhOAuLzvlQeqAAkeDOZrJs9rzumQ=="], - "@types/express": ["@types/express@5.0.0", "", { "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^5.0.0", "@types/qs": "*", "@types/serve-static": "*" } }, "sha512-DvZriSMehGHL1ZNLzi6MidnsDhUZM/x2pRdDIKdwbUNqqwHxMlRdkxtn6/EPKyqKpHqTl/4nRZsRNLpZxZRpPQ=="], + "@types/express-serve-static-core": ["@types/express-serve-static-core@5.1.0", "", { "dependencies": { "@types/node": "*", "@types/qs": "*", "@types/range-parser": "*", "@types/send": "*" } }, "sha512-jnHMsrd0Mwa9Cf4IdOzbz543y4XJepXrbia2T4b6+spXC2We3t1y6K44D3mR8XMFSXMCf3/l7rCgddfx7UNVBA=="], - "@types/express-serve-static-core": ["@types/express-serve-static-core@5.0.6", "", { "dependencies": { "@types/node": "*", "@types/qs": "*", "@types/range-parser": "*", "@types/send": "*" } }, "sha512-3xhRnjJPkULekpSzgtoNYYcTWgEZkp4myc+Saevii5JPnHNvHMRlBSHDbs7Bh1iPPoVTERHEZXyhyLbMEsExsA=="], - - "@types/http-errors": ["@types/http-errors@2.0.4", "", {}, "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA=="], + "@types/http-errors": ["@types/http-errors@2.0.5", "", {}, "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg=="], "@types/mime": ["@types/mime@1.3.5", "", {}, "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w=="], "@types/node": ["@types/node@22.13.10", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-I6LPUvlRH+O6VRUqYOcMudhaIdUVWfsjnZavnsraHvpBwaEyMN29ry+0UVJhImYL16xsscu0aske3yA+uPOWfw=="], - "@types/qs": ["@types/qs@6.9.18", "", {}, "sha512-kK7dgTYDyGqS+e2Q4aK9X3D7q234CIZ1Bv0q/7Z5IwRDoADNU81xXJK/YVyLbLTZCoIwUoDoffFeF+p/eIklAA=="], + "@types/normalize-package-data": ["@types/normalize-package-data@2.4.4", "", {}, "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA=="], + + "@types/qs": ["@types/qs@6.14.0", "", {}, "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ=="], "@types/range-parser": ["@types/range-parser@1.2.7", "", {}, "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ=="], - "@types/send": ["@types/send@0.17.4", "", { "dependencies": { "@types/mime": "^1", "@types/node": "*" } }, "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA=="], + "@types/semver": ["@types/semver@7.7.1", "", {}, "sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA=="], - "@types/serve-static": ["@types/serve-static@1.15.7", "", { "dependencies": { "@types/http-errors": "*", "@types/node": "*", "@types/send": "*" } }, "sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw=="], + "@types/send": ["@types/send@0.17.6", "", { "dependencies": { "@types/mime": "^1", "@types/node": "*" } }, "sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og=="], + + "@types/serve-static": ["@types/serve-static@1.15.10", "", { "dependencies": { "@types/http-errors": "*", "@types/node": "*", "@types/send": "<1" } }, "sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw=="], "@types/ws": ["@types/ws@8.5.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-bd/YFLW+URhBzMXurx7lWByOu+xzU9+kb3RboOteXYDfW+tr+JZa99OyNmPINEGB/ahzKrEuc8rcv4gnpJmxTw=="], - "abitype": ["abitype@1.0.8", "", { "peerDependencies": { "typescript": ">=5.0.4", "zod": "^3 >=3.22.0" }, "optionalPeers": ["typescript", "zod"] }, "sha512-ZeiI6h3GnW06uYDLx0etQtX/p8E24UaHHBj57RSjK7YBFe7iuVn07EDpOeP451D06sF27VOz9JJPlIKJmXgkEg=="], + "abitype": ["abitype@1.1.0", "", { "peerDependencies": { "typescript": ">=5.0.4", "zod": "^3.22.0 || ^4.0.0" }, "optionalPeers": ["typescript", "zod"] }, "sha512-6Vh4HcRxNMLA0puzPjM5GBgT4aAcFGKZzSgAXvuZ27shJP6NEpielTuqbBmZILR5/xd0PizkBGy5hReKz9jl5A=="], "accepts": ["accepts@1.3.8", "", { "dependencies": { "mime-types": "~2.1.34", "negotiator": "0.6.3" } }, "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw=="], + "add-stream": ["add-stream@1.0.0", "", {}, "sha512-qQLMr+8o0WC4FZGQTcJiKBVC59JylcPSrTtk6usvmIDFUOCKegapy1VHQwRbFMOFyb/inzUVqHs+eMYKDM1YeQ=="], + + "ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="], + + "ajv-formats": ["ajv-formats@3.0.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="], + "array-flatten": ["array-flatten@1.1.1", "", {}, "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg=="], + "array-ify": ["array-ify@1.0.0", "", {}, "sha512-c5AMf34bKdvPhQ7tBGhqkgKNUzMr4WUs+WDtC2ZUGOUncbxKMTvqxYctiseW3+L4bA8ec+GcZ6/A/FW4m8ukng=="], + "body-parser": ["body-parser@1.20.3", "", { "dependencies": { "bytes": "3.1.2", "content-type": "~1.0.5", "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", "http-errors": "2.0.0", "iconv-lite": "0.4.24", "on-finished": "2.4.1", "qs": "6.13.0", "raw-body": "2.5.2", "type-is": "~1.6.18", "unpipe": "1.0.0" } }, "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g=="], "bun-types": ["bun-types@1.2.5", "", { "dependencies": { "@types/node": "*", "@types/ws": "~8.5.10" } }, "sha512-3oO6LVGGRRKI4kHINx5PIdIgnLRb7l/SprhzqXapmoYkFl5m4j6EvALvbDVuuBFaamB46Ap6HCUxIXNLCGy+tg=="], @@ -79,22 +100,60 @@ "call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="], + "compare-func": ["compare-func@2.0.0", "", { "dependencies": { "array-ify": "^1.0.0", "dot-prop": "^5.1.0" } }, "sha512-zHig5N+tPWARooBnb0Zx1MFcdfpyJrfTJ3Y5L+IFvUm8rM74hHz66z0gw0x4tijh5CorKkKUCnW82R2vmpeCRA=="], + "content-disposition": ["content-disposition@0.5.4", "", { "dependencies": { "safe-buffer": "5.2.1" } }, "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ=="], "content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="], + "conventional-changelog": ["conventional-changelog@6.0.0", "", { "dependencies": { "conventional-changelog-angular": "^8.0.0", "conventional-changelog-atom": "^5.0.0", "conventional-changelog-codemirror": "^5.0.0", "conventional-changelog-conventionalcommits": "^8.0.0", "conventional-changelog-core": "^8.0.0", "conventional-changelog-ember": "^5.0.0", "conventional-changelog-eslint": "^6.0.0", "conventional-changelog-express": "^5.0.0", "conventional-changelog-jquery": "^6.0.0", "conventional-changelog-jshint": "^5.0.0", "conventional-changelog-preset-loader": "^5.0.0" } }, "sha512-tuUH8H/19VjtD9Ig7l6TQRh+Z0Yt0NZ6w/cCkkyzUbGQTnUEmKfGtkC9gGfVgCfOL1Rzno5NgNF4KY8vR+Jo3w=="], + + "conventional-changelog-angular": ["conventional-changelog-angular@8.1.0", "", { "dependencies": { "compare-func": "^2.0.0" } }, "sha512-GGf2Nipn1RUCAktxuVauVr1e3r8QrLP/B0lEUsFktmGqc3ddbQkhoJZHJctVU829U1c6mTSWftrVOCHaL85Q3w=="], + + "conventional-changelog-atom": ["conventional-changelog-atom@5.0.0", "", {}, "sha512-WfzCaAvSCFPkznnLgLnfacRAzjgqjLUjvf3MftfsJzQdDICqkOOpcMtdJF3wTerxSpv2IAAjX8doM3Vozqle3g=="], + + "conventional-changelog-cli": ["conventional-changelog-cli@5.0.0", "", { "dependencies": { "add-stream": "^1.0.0", "conventional-changelog": "^6.0.0", "meow": "^13.0.0", "tempfile": "^5.0.0" }, "bin": { "conventional-changelog": "cli.js" } }, "sha512-9Y8fucJe18/6ef6ZlyIlT2YQUbczvoQZZuYmDLaGvcSBP+M6h+LAvf7ON7waRxKJemcCII8Yqu5/8HEfskTxJQ=="], + + "conventional-changelog-codemirror": ["conventional-changelog-codemirror@5.0.0", "", {}, "sha512-8gsBDI5Y3vrKUCxN6Ue8xr6occZ5nsDEc4C7jO/EovFGozx8uttCAyfhRrvoUAWi2WMm3OmYs+0mPJU7kQdYWQ=="], + + "conventional-changelog-conventionalcommits": ["conventional-changelog-conventionalcommits@8.0.0", "", { "dependencies": { "compare-func": "^2.0.0" } }, "sha512-eOvlTO6OcySPyyyk8pKz2dP4jjElYunj9hn9/s0OB+gapTO8zwS9UQWrZ1pmF2hFs3vw1xhonOLGcGjy/zgsuA=="], + + "conventional-changelog-core": ["conventional-changelog-core@8.0.0", "", { "dependencies": { "@hutson/parse-repository-url": "^5.0.0", "add-stream": "^1.0.0", "conventional-changelog-writer": "^8.0.0", "conventional-commits-parser": "^6.0.0", "git-raw-commits": "^5.0.0", "git-semver-tags": "^8.0.0", "hosted-git-info": "^7.0.0", "normalize-package-data": "^6.0.0", "read-package-up": "^11.0.0", "read-pkg": "^9.0.0" } }, "sha512-EATUx5y9xewpEe10UEGNpbSHRC6cVZgO+hXQjofMqpy+gFIrcGvH3Fl6yk2VFKh7m+ffenup2N7SZJYpyD9evw=="], + + "conventional-changelog-ember": ["conventional-changelog-ember@5.0.0", "", {}, "sha512-RPflVfm5s4cSO33GH/Ey26oxhiC67akcxSKL8CLRT3kQX2W3dbE19sSOM56iFqUJYEwv9mD9r6k79weWe1urfg=="], + + "conventional-changelog-eslint": ["conventional-changelog-eslint@6.0.0", "", {}, "sha512-eiUyULWjzq+ybPjXwU6NNRflApDWlPEQEHvI8UAItYW/h22RKkMnOAtfCZxMmrcMO1OKUWtcf2MxKYMWe9zJuw=="], + + "conventional-changelog-express": ["conventional-changelog-express@5.0.0", "", {}, "sha512-D8Q6WctPkQpvr2HNCCmwU5GkX22BVHM0r4EW8vN0230TSyS/d6VQJDAxGb84lbg0dFjpO22MwmsikKL++Oo/oQ=="], + + "conventional-changelog-jquery": ["conventional-changelog-jquery@6.0.0", "", {}, "sha512-2kxmVakyehgyrho2ZHBi90v4AHswkGzHuTaoH40bmeNqUt20yEkDOSpw8HlPBfvEQBwGtbE+5HpRwzj6ac2UfA=="], + + "conventional-changelog-jshint": ["conventional-changelog-jshint@5.0.0", "", { "dependencies": { "compare-func": "^2.0.0" } }, "sha512-gGNphSb/opc76n2eWaO6ma4/Wqu3tpa2w7i9WYqI6Cs2fncDSI2/ihOfMvXveeTTeld0oFvwMVNV+IYQIk3F3g=="], + + "conventional-changelog-preset-loader": ["conventional-changelog-preset-loader@5.0.0", "", {}, "sha512-SetDSntXLk8Jh1NOAl1Gu5uLiCNSYenB5tm0YVeZKePRIgDW9lQImromTwLa3c/Gae298tsgOM+/CYT9XAl0NA=="], + + "conventional-changelog-writer": ["conventional-changelog-writer@8.2.0", "", { "dependencies": { "conventional-commits-filter": "^5.0.0", "handlebars": "^4.7.7", "meow": "^13.0.0", "semver": "^7.5.2" }, "bin": { "conventional-changelog-writer": "dist/cli/index.js" } }, "sha512-Y2aW4596l9AEvFJRwFGJGiQjt2sBYTjPD18DdvxX9Vpz0Z7HQ+g1Z+6iYDAm1vR3QOJrDBkRHixHK/+FhkR6Pw=="], + + "conventional-commits-filter": ["conventional-commits-filter@5.0.0", "", {}, "sha512-tQMagCOC59EVgNZcC5zl7XqO30Wki9i9J3acbUvkaosCT6JX3EeFwJD7Qqp4MCikRnzS18WXV3BLIQ66ytu6+Q=="], + + "conventional-commits-parser": ["conventional-commits-parser@6.2.1", "", { "dependencies": { "meow": "^13.0.0" }, "bin": { "conventional-commits-parser": "dist/cli/index.js" } }, "sha512-20pyHgnO40rvfI0NGF/xiEoFMkXDtkF8FwHvk5BokoFoCuTQRI8vrNCNFWUOfuolKJMm1tPCHc8GgYEtr1XRNA=="], + "cookie": ["cookie@0.7.1", "", {}, "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w=="], "cookie-signature": ["cookie-signature@1.0.6", "", {}, "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ=="], "cors": ["cors@2.8.5", "", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g=="], + "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], + "debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], "depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="], "destroy": ["destroy@1.2.0", "", {}, "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg=="], + "dot-prop": ["dot-prop@5.3.0", "", { "dependencies": { "is-obj": "^2.0.0" } }, "sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q=="], + "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], "ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="], @@ -121,8 +180,14 @@ "express-rate-limit": ["express-rate-limit@7.5.0", "", { "peerDependencies": { "express": "^4.11 || 5 || ^5.0.0-beta.1" } }, "sha512-eB5zbQh5h+VenMPM3fh+nw1YExi5nMr6HUCR62ELSP11huvxm/Uir1H1QEyTkk5QX6A58pX6NmaTMceKZ0Eodg=="], + "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], + + "fast-uri": ["fast-uri@3.1.0", "", {}, "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA=="], + "finalhandler": ["finalhandler@1.3.1", "", { "dependencies": { "debug": "2.6.9", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "on-finished": "2.4.1", "parseurl": "~1.3.3", "statuses": "2.0.1", "unpipe": "~1.0.0" } }, "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ=="], + "find-up-simple": ["find-up-simple@1.0.1", "", {}, "sha512-afd4O7zpqHeRyg4PfDQsXmlDe2PfdHtJt6Akt8jOWaApLOZk5JXs6VMR29lz03pRe9mpykrRCYIYxaJYcfpncQ=="], + "forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="], "fresh": ["fresh@0.5.2", "", {}, "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q=="], @@ -133,28 +198,50 @@ "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="], + "git-raw-commits": ["git-raw-commits@5.0.0", "", { "dependencies": { "@conventional-changelog/git-client": "^1.0.0", "meow": "^13.0.0" }, "bin": { "git-raw-commits": "src/cli.js" } }, "sha512-I2ZXrXeOc0KrCvC7swqtIFXFN+rbjnC7b2T943tvemIOVNl+XP8YnA9UVwqFhzzLClnSA60KR/qEjLpXzs73Qg=="], + + "git-semver-tags": ["git-semver-tags@8.0.0", "", { "dependencies": { "@conventional-changelog/git-client": "^1.0.0", "meow": "^13.0.0" }, "bin": { "git-semver-tags": "src/cli.js" } }, "sha512-N7YRIklvPH3wYWAR2vysaqGLPRcpwQ0GKdlqTiVN5w1UmCdaeY3K8s6DMKRCh54DDdzyt/OAB6C8jgVtb7Y2Fg=="], + "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="], + "handlebars": ["handlebars@4.7.8", "", { "dependencies": { "minimist": "^1.2.5", "neo-async": "^2.6.2", "source-map": "^0.6.1", "wordwrap": "^1.0.0" }, "optionalDependencies": { "uglify-js": "^3.1.4" }, "bin": { "handlebars": "bin/handlebars" } }, "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ=="], + "has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="], "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="], + "hosted-git-info": ["hosted-git-info@7.0.2", "", { "dependencies": { "lru-cache": "^10.0.1" } }, "sha512-puUZAUKT5m8Zzvs72XWy3HtvVbTWljRE66cP60bxJzAqf2DgICo7lYTY2IHUmLnNpjYvw5bvmoHvPc0QO2a62w=="], + "http-errors": ["http-errors@2.0.0", "", { "dependencies": { "depd": "2.0.0", "inherits": "2.0.4", "setprototypeof": "1.2.0", "statuses": "2.0.1", "toidentifier": "1.0.1" } }, "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ=="], "iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="], + "index-to-position": ["index-to-position@1.2.0", "", {}, "sha512-Yg7+ztRkqslMAS2iFaU+Oa4KTSidr63OsFGlOrJoW981kIYO3CGCS3wA95P1mUi/IVSJkn0D479KTJpVpvFNuw=="], + "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], "ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="], + "is-obj": ["is-obj@2.0.0", "", {}, "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w=="], + "is-promise": ["is-promise@4.0.0", "", {}, "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="], - "isows": ["isows@1.0.6", "", { "peerDependencies": { "ws": "*" } }, "sha512-lPHCayd40oW98/I0uvgaHKWCSvkzY27LjWLbtzOm64yQ+G3Q5npjjbdppU65iZXkK1Zt+kH9pfegli0AYfwYYw=="], + "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], + + "isows": ["isows@1.0.7", "", { "peerDependencies": { "ws": "*" } }, "sha512-I1fSfDCZL5P0v33sVqeTDSpcstAg/N+wF5HS033mogOVIp4B+oHC7oOCsA3axAbBSGTJ8QubbNmnIRN/h8U7hg=="], + + "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], + + "json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], + + "lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], "media-typer": ["media-typer@0.3.0", "", {}, "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ=="], + "meow": ["meow@13.2.0", "", {}, "sha512-pxQJQzB6djGPXh08dacEloMFopsOqGVRKFPYvPOt9XDZ1HasbgDZA74CJGreSU4G3Ak7EFJGoiH2auq+yXISgA=="], + "merge-descriptors": ["merge-descriptors@1.0.3", "", {}, "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ=="], "methods": ["methods@1.1.2", "", {}, "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w=="], @@ -165,10 +252,16 @@ "mime-types": ["mime-types@3.0.0", "", { "dependencies": { "mime-db": "^1.53.0" } }, "sha512-XqoSHeCGjVClAmoGFG3lVFqQFRIrTVw2OH3axRqAcfaw+gHWIfnASS92AV+Rl/mk0MupgZTRHQOjxY6YVnzK5w=="], + "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], + "ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], "negotiator": ["negotiator@0.6.3", "", {}, "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="], + "neo-async": ["neo-async@2.6.2", "", {}, "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw=="], + + "normalize-package-data": ["normalize-package-data@6.0.2", "", { "dependencies": { "hosted-git-info": "^7.0.0", "semver": "^7.3.5", "validate-npm-package-license": "^3.0.4" } }, "sha512-V6gygoYb/5EmNI+MEGrWkC+e6+Rr7mTmfHrxDbLzxQogBkgzo76rkok0Am6thgSF7Mv2nLOajAJj5vDJZEFn7g=="], + "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], "object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="], @@ -177,13 +270,19 @@ "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], - "ox": ["ox@0.6.9", "", { "dependencies": { "@adraffy/ens-normalize": "^1.10.1", "@noble/curves": "^1.6.0", "@noble/hashes": "^1.5.0", "@scure/bip32": "^1.5.0", "@scure/bip39": "^1.4.0", "abitype": "^1.0.6", "eventemitter3": "5.0.1" }, "peerDependencies": { "typescript": ">=5.4.0" }, "optionalPeers": ["typescript"] }, "sha512-wi5ShvzE4eOcTwQVsIPdFr+8ycyX+5le/96iAJutaZAvCes1J0+RvpEPg5QDPDiaR0XQQAvZVl7AwqQcINuUug=="], + "ox": ["ox@0.9.6", "", { "dependencies": { "@adraffy/ens-normalize": "^1.11.0", "@noble/ciphers": "^1.3.0", "@noble/curves": "1.9.1", "@noble/hashes": "^1.8.0", "@scure/bip32": "^1.7.0", "@scure/bip39": "^1.6.0", "abitype": "^1.0.9", "eventemitter3": "5.0.1" }, "peerDependencies": { "typescript": ">=5.4.0" }, "optionalPeers": ["typescript"] }, "sha512-8SuCbHPvv2eZLYXrNmC0EC12rdzXQLdhnOMlHDW2wiCPLxBrOOJwX5L5E61by+UjTPOryqQiRSnjIKCI+GykKg=="], + + "parse-json": ["parse-json@8.3.0", "", { "dependencies": { "@babel/code-frame": "^7.26.2", "index-to-position": "^1.1.0", "type-fest": "^4.39.1" } }, "sha512-ybiGyvspI+fAoRQbIPRddCcSTV9/LsJbf0e/S85VLowVGzRmokfneg2kwVW/KU5rOXrPSbF1qAKPMgNTqqROQQ=="], "parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="], + "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], + "path-to-regexp": ["path-to-regexp@0.1.12", "", {}, "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ=="], - "pkce-challenge": ["pkce-challenge@4.1.0", "", {}, "sha512-ZBmhE1C9LcPoH9XZSdwiPtbPHZROwAnMy+kIFQVrnMCxY4Cudlz3gBOpzilgc0jOgRaiT3sIWfpMomW2ar2orQ=="], + "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], + + "pkce-challenge": ["pkce-challenge@5.0.0", "", {}, "sha512-ueGLflrrnvwB3xuo/uGob5pd5FN7l0MsLf0Z87o/UQmRtwjvfylfc9MurIxRAWywCYTgrvpXBcqjV4OfCYGCIQ=="], "proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="], @@ -193,18 +292,30 @@ "raw-body": ["raw-body@3.0.0", "", { "dependencies": { "bytes": "3.1.2", "http-errors": "2.0.0", "iconv-lite": "0.6.3", "unpipe": "1.0.0" } }, "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g=="], + "read-package-up": ["read-package-up@11.0.0", "", { "dependencies": { "find-up-simple": "^1.0.0", "read-pkg": "^9.0.0", "type-fest": "^4.6.0" } }, "sha512-MbgfoNPANMdb4oRBNg5eqLbB2t2r+o5Ua1pNt8BqGp4I0FJZhuVSOj3PaBPni4azWuSzEdNn2evevzVmEk1ohQ=="], + + "read-pkg": ["read-pkg@9.0.1", "", { "dependencies": { "@types/normalize-package-data": "^2.4.3", "normalize-package-data": "^6.0.0", "parse-json": "^8.0.0", "type-fest": "^4.6.0", "unicorn-magic": "^0.1.0" } }, "sha512-9viLL4/n1BJUCT1NXVTdS1jtm80yDEgR5T4yCelII49Mbj0v1rZdKqj7zCiYdbB0CuCgdrvHcNogAKTFPBocFA=="], + + "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="], + "router": ["router@2.1.0", "", { "dependencies": { "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-/m/NSLxeYEgWNtyC+WtNHCF7jbGxOibVWKnn+1Psff4dJGOfoXP+MuC/f2CwSmyiHdOIzYnYFp4W6GxWfekaLA=="], "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], + "semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], + "send": ["send@0.19.0", "", { "dependencies": { "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", "encodeurl": "~1.0.2", "escape-html": "~1.0.3", "etag": "~1.8.1", "fresh": "0.5.2", "http-errors": "2.0.0", "mime": "1.6.0", "ms": "2.1.3", "on-finished": "2.4.1", "range-parser": "~1.2.1", "statuses": "2.0.1" } }, "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw=="], "serve-static": ["serve-static@1.16.2", "", { "dependencies": { "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "parseurl": "~1.3.3", "send": "0.19.0" } }, "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw=="], "setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="], + "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], + + "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], + "side-channel": ["side-channel@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", "side-channel-list": "^1.0.0", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="], "side-channel-list": ["side-channel-list@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3" } }, "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA=="], @@ -213,29 +324,55 @@ "side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="], + "source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], + + "spdx-correct": ["spdx-correct@3.2.0", "", { "dependencies": { "spdx-expression-parse": "^3.0.0", "spdx-license-ids": "^3.0.0" } }, "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA=="], + + "spdx-exceptions": ["spdx-exceptions@2.5.0", "", {}, "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w=="], + + "spdx-expression-parse": ["spdx-expression-parse@3.0.1", "", { "dependencies": { "spdx-exceptions": "^2.1.0", "spdx-license-ids": "^3.0.0" } }, "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q=="], + + "spdx-license-ids": ["spdx-license-ids@3.0.22", "", {}, "sha512-4PRT4nh1EImPbt2jASOKHX7PB7I+e4IWNLvkKFDxNhJlfjbYlleYQh285Z/3mPTHSAK/AvdMmw5BNNuYH8ShgQ=="], + "statuses": ["statuses@2.0.1", "", {}, "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ=="], + "temp-dir": ["temp-dir@3.0.0", "", {}, "sha512-nHc6S/bwIilKHNRgK/3jlhDoIHcp45YgyiwcAk46Tr0LfEqGBVpmiAyuiuxeVE44m3mXnEeVhaipLOEWmH+Njw=="], + + "tempfile": ["tempfile@5.0.0", "", { "dependencies": { "temp-dir": "^3.0.0" } }, "sha512-bX655WZI/F7EoTDw9JvQURqAXiPHi8o8+yFxPF2lWYyz1aHnmMRuXWqL6YB6GmeO0o4DIYWHLgGNi/X64T+X4Q=="], + "toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="], + "type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="], + "type-is": ["type-is@1.6.18", "", { "dependencies": { "media-typer": "0.3.0", "mime-types": "~2.1.24" } }, "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g=="], "typescript": ["typescript@5.8.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ=="], + "uglify-js": ["uglify-js@3.19.3", "", { "bin": { "uglifyjs": "bin/uglifyjs" } }, "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ=="], + "undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="], + "unicorn-magic": ["unicorn-magic@0.1.0", "", {}, "sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ=="], + "unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="], "utils-merge": ["utils-merge@1.0.1", "", {}, "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA=="], + "validate-npm-package-license": ["validate-npm-package-license@3.0.4", "", { "dependencies": { "spdx-correct": "^3.0.0", "spdx-expression-parse": "^3.0.0" } }, "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew=="], + "vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="], - "viem": ["viem@2.23.9", "", { "dependencies": { "@noble/curves": "1.8.1", "@noble/hashes": "1.7.1", "@scure/bip32": "1.6.2", "@scure/bip39": "1.5.4", "abitype": "1.0.8", "isows": "1.0.6", "ox": "0.6.9", "ws": "8.18.1" }, "peerDependencies": { "typescript": ">=5.0.4" }, "optionalPeers": ["typescript"] }, "sha512-y8VLPfKukrstZKTerS9bm45ajZ22wUyStF+VquK3I2OovWLOyXSbQmJWei8syMFhp1uwhxh1tb0fAdx0WSRZWg=="], + "viem": ["viem@2.39.3", "", { "dependencies": { "@noble/curves": "1.9.1", "@noble/hashes": "1.8.0", "@scure/bip32": "1.7.0", "@scure/bip39": "1.6.0", "abitype": "1.1.0", "isows": "1.0.7", "ox": "0.9.6", "ws": "8.18.3" }, "peerDependencies": { "typescript": ">=5.0.4" }, "optionalPeers": ["typescript"] }, "sha512-s11rPQRvUEdc5qHK3xT4fIk4qvgPAaLwaTFq+EbFlcJJD+Xn3R4mc9H6B6fquEiHl/mdsdbG/uKCnYpoNtHNHw=="], + + "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], + + "wordwrap": ["wordwrap@1.0.0", "", {}, "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q=="], "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], - "ws": ["ws@8.18.1", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-RKW2aJZMXeMxVpnZ6bck+RswznaxmzdULiBr6KY7XkTnW8uvt0iT9H5DkHUChXrc+uurzwa0rVI16n/Xzjdz1w=="], + "ws": ["ws@8.18.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg=="], - "zod": ["zod@3.24.2", "", {}, "sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ=="], + "zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], "zod-to-json-schema": ["zod-to-json-schema@3.24.3", "", { "peerDependencies": { "zod": "^3.24.1" } }, "sha512-HIAfWdYIt1sssHfYZFCXp4rU1w2r8hVVXYIlmoa0r0gABLs5di3RCqPU5DDROogVz1pAdYBaz7HK5n9pSUNs3A=="], diff --git a/package.json b/package.json index ab33609..c753a8d 100644 --- a/package.json +++ b/package.json @@ -2,8 +2,8 @@ "name": "@mcpdotdirect/evm-mcp-server", "module": "src/index.ts", "type": "module", - "version": "1.2.0", - "description": "Model Context Protocol (MCP) server for interacting with EVM-compatible networks", + "version": "2.0.0-pre", + "description": "MCP server for interacting with EVM-compatible blockchains - supports 22 tools and 10 prompts across 60+ networks", "bin": { "evm-mcp-server": "./bin/cli.js" }, @@ -27,23 +27,23 @@ "version:major": "npm version major", "release": "npm publish", "changelog": "conventional-changelog -p angular -i CHANGELOG.md -s -r 0", - "changelog:latest": "conventional-changelog -p angular -r 1 > RELEASE_NOTES.md" + "changelog:latest": "conventional-changelog -p angular -r 1 > RELEASE_NOTES.md", + "inspect": "npx @modelcontextprotocol/inspector node build/index.js" }, "devDependencies": { "@types/bun": "latest", - "@types/cors": "^2.8.17", "@types/express": "^5.0.0", + "@types/node": "^22.0.0", "conventional-changelog-cli": "^5.0.0" }, "peerDependencies": { "typescript": "^5.8.2" }, "dependencies": { - "@modelcontextprotocol/sdk": "^1.7.0", - "cors": "^2.8.5", + "@modelcontextprotocol/sdk": "^1.22.0", "express": "^4.21.2", - "viem": "^2.23.9", - "zod": "^3.24.2" + "viem": "^2.39.3", + "zod": "^3.24.3" }, "keywords": [ "mcp", @@ -59,7 +59,7 @@ "author": "Etheral ", "license": "MIT", "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" }, "repository": { "type": "git", @@ -72,4 +72,4 @@ "publishConfig": { "access": "public" } -} +} \ No newline at end of file diff --git a/src/core/chains.ts b/src/core/chains.ts index c5c5dda..75115d3 100644 --- a/src/core/chains.ts +++ b/src/core/chains.ts @@ -296,8 +296,9 @@ export function resolveChainId(chainIdentifier: number | string): number { const networkName = chainIdentifier.toLowerCase(); // Check if the network name is in our map - if (networkName in networkNameMap) { - return networkNameMap[networkName]; + const chainId = networkNameMap[networkName]; + if (chainId !== undefined) { + return chainId; } // Try parsing as a number diff --git a/src/core/prompts.ts b/src/core/prompts.ts index b7f7bce..ada7df8 100644 --- a/src/core/prompts.ts +++ b/src/core/prompts.ts @@ -2,161 +2,1055 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { z } from "zod"; /** - * Register all EVM-related prompts with the MCP server + * Register task-oriented prompts with the MCP server + * + * All prompts follow a consistent structure: + * - Clear objective statement + * - Step-by-step instructions + * - Expected outputs + * - Safety/security considerations + * + * Prompts guide the model through complex workflows that would otherwise + * require multiple tool calls in the correct sequence. + * * @param server The MCP server instance */ export function registerEVMPrompts(server: McpServer) { - // Basic block explorer prompt - server.prompt( - "explore_block", - "Explore information about a specific block", + // ============================================================================ + // TRANSACTION PROMPTS + // ============================================================================ + + server.registerPrompt( + "prepare_transfer", { - blockNumber: z.string().optional().describe("Block number to explore. If not provided, latest block will be used."), - network: z.string().optional().describe("Network name (e.g., 'ethereum', 'optimism', 'arbitrum', 'base', etc.) or chain ID. Supports all EVM-compatible networks. Defaults to Ethereum mainnet.") + description: "Safely prepare and execute a token transfer with validation checks", + argsSchema: { + tokenType: z.enum(["native", "erc20"]).describe("Token type: 'native' for ETH/MATIC or 'erc20' for contract tokens"), + recipient: z.string().describe("Recipient address or ENS name"), + amount: z.string().describe("Amount to transfer (in ether for native, token units for ERC20)"), + network: z.string().optional().describe("Network name (default: ethereum)"), + tokenAddress: z.string().optional().describe("Token contract address (required for ERC20)") + } }, - ({ blockNumber, network = "ethereum" }) => ({ + ({ tokenType, recipient, amount, network = "ethereum", tokenAddress }) => ({ messages: [{ role: "user", content: { type: "text", - text: blockNumber - ? `Please analyze block #${blockNumber} on the ${network} network and provide information about its key metrics, transactions, and significance.` - : `Please analyze the latest block on the ${network} network and provide information about its key metrics, transactions, and significance.` + text: `# Token Transfer Task + +**Objective**: Safely transfer ${amount} ${tokenType === "native" ? "native tokens" : "ERC20 tokens"} to ${recipient} on ${network} + +## Validation & Checks +Before executing any transfer: +1. **Wallet Verification**: Call \`get_wallet_address\` to confirm the sending wallet +2. **Balance Check**: + ${tokenType === "native" + ? "- Call `get_balance` to verify native token balance" + : "- Call `get_token_balance` with tokenAddress=${tokenAddress} to verify balance"} +3. **Gas Analysis**: Call \`get_gas_price\` to assess current network costs +${tokenType === "erc20" ? `4. **Approval Check**: Call \`get_allowance\` to verify approval (if needed for protocols)` : ""} + +## Execution Steps +${tokenType === "native" ? ` +1. Summarize: sender address, recipient, amount, and estimated gas cost +2. Request confirmation from user +3. Call \`transfer_native\` with to="${recipient}", amount="${amount}", network="${network}" +4. Return transaction hash to user +5. Call \`wait_for_transaction\` to confirm completion +` : ` +1. Check if approval is needed: + - If allowance < amount: Call \`approve_token_spending\` first + - Then proceed with transfer +2. Summarize: sender, recipient, token, amount, decimals, gas estimate +3. Request confirmation +4. Call \`transfer_erc20\` with tokenAddress, recipient, amount +5. Wait for confirmation with \`wait_for_transaction\` +`} + +## Output Format +- **Transaction Hash**: Clear hex value +- **Status**: Pending or Confirmed +- **Cost Estimate**: Gas price and total cost +- **User Confirmation**: Always ask before sending + +## Safety Considerations +- Never send more than available balance +- Double-check recipient address +- Warn about high gas prices +- Explain any approval requirements +` } }] }) ); - // Transaction analysis prompt - server.prompt( - "analyze_transaction", - "Analyze a specific transaction", + server.registerPrompt( + "diagnose_transaction", { - txHash: z.string().describe("Transaction hash to analyze"), - network: z.string().optional().describe("Network name (e.g., 'ethereum', 'optimism', 'arbitrum', 'base', etc.) or chain ID. Supports all EVM-compatible networks. Defaults to Ethereum mainnet.") + description: "Analyze transaction status, failures, and provide debugging insights", + argsSchema: { + txHash: z.string().describe("Transaction hash to diagnose (0x...)"), + network: z.string().optional().describe("Network name (default: ethereum)") + } }, ({ txHash, network = "ethereum" }) => ({ messages: [{ role: "user", content: { type: "text", - text: `Please analyze transaction ${txHash} on the ${network} network and provide a detailed explanation of what this transaction does, who the parties involved are, the amount transferred (if applicable), gas used, and any other relevant information.` + text: `# Transaction Diagnosis + +**Objective**: Analyze transaction ${txHash} on ${network} and identify any issues + +## Investigation Process + +### 1. Gather Transaction Data +- Call \`get_transaction\` to fetch transaction details +- Call \`get_transaction_receipt\` to get status and gas used +- Note: both calls are read-only and free + +### 2. Status Assessment +Determine transaction state: +- **Pending**: Not yet mined (check mempool conditions) +- **Confirmed**: Successfully executed (status='success') +- **Failed**: Execution failed (status='failed') +- **Replaced**: Transaction was dropped/replaced (check nonce) + +### 3. Failure Analysis +If transaction failed, investigate: + +**Out of Gas**: +- Compare gasUsed vs gasLimit in receipt +- If gasUsed >= gasLimit, suggest increasing gas limit + +**Contract Revert**: +- Check function called and parameters +- Verify sufficient balance/approvals +- Look for require/revert statements in contract + +**Invalid Nonce**: +- Compare transaction nonce with account's current nonce +- Suggest pending transactions may need replacement + +**Other Issues**: +- Check sender/recipient addresses are valid +- Verify function parameters are correct type +- Look for access control restrictions + +### 4. Gas Analysis +- Calculate gas cost: gasUsed * gasPrice +- Compare to current gas prices (call \`get_gas_price\`) +- Assess if overpaid or underpaid + +## Output Format + +Provide structured diagnosis: +- **Status**: Pending/Confirmed/Failed with reason +- **Transaction Hash**: The hash analyzed +- **From/To**: Addresses involved +- **Function**: What was called +- **Gas Analysis**: Used vs limit, cost +- **Issue (if failed)**: Root cause and explanation +- **Recommended Actions**: Next steps to resolve + +## Important Notes +- Be specific about error messages and codes +- Provide actionable recommendations +- Link issues to specific contract behavior +- Suggest solutions (retry, increase gas, fix parameters, etc.) +` } }] }) ); - // Address analysis prompt - server.prompt( - "analyze_address", - "Analyze an EVM address", + // ============================================================================ + // WALLET ANALYSIS PROMPTS + // ============================================================================ + + server.registerPrompt( + "analyze_wallet", + { + description: "Get comprehensive overview of wallet assets, balances, and activity", + argsSchema: { + address: z.string().describe("Wallet address or ENS name to analyze"), + network: z.string().optional().describe("Network name (default: ethereum)"), + tokens: z.string().optional().describe("Comma-separated token addresses to check") + } + }, + ({ address, network = "ethereum", tokens }) => { + const tokenList = tokens ? tokens.split(',').map(t => t.trim()) : []; + return { + messages: [{ + role: "user", + content: { + type: "text", + text: `# Wallet Analysis + +**Objective**: Provide complete asset overview for ${address} on ${network} + +## Information Gathering + +### 1. Address Resolution +- If input contains '.eth', call \`resolve_ens_name\` to get address +- Otherwise use as direct address +- Provide both resolved address and ENS name if applicable + +### 2. Native Token Balance +- Call \`get_balance\` to fetch native token (ETH/MATIC/etc) balance +- Report both wei and ether/human-readable formats +- Note: Free read-only call + +### 3. Token Balances +${tokenList.length > 0 + ? `- Call \`get_token_balance\` for each token:\n${tokenList.map(t => ` * ${t}`).join('\n')}` + : `- If specific tokens provided: call \`get_token_balance\` for each +- Include token symbol and decimals if available`} + +## Output Format + +Provide analysis with clear sections: + +**Wallet Overview** +- Address: [address] +- ENS Name: [name or none] +- Network: [network] + +**Native Token Balance** +- Ether: [formatted amount] +- Wei: [raw amount] +- In USD (if price available): [estimated value] + +**Token Holdings** (if requested) +- Token: [address] +- Symbol: [symbol] +- Balance: [formatted] +- Decimals: [decimals] + +**Summary** +- Total assets value (if prices available) +- Primary holdings +- Notable observations + +## Key Considerations +- Show both formatted and raw amounts +- Include token decimals for precision +- Note if wallet has low/no balance +- Highlight any unusual patterns +- Be clear about what data was available vs not +` + } + }] + }; + } + ); + + server.registerPrompt( + "audit_approvals", { - address: z.string().describe("Ethereum address to analyze"), - network: z.string().optional().describe("Network name (e.g., 'ethereum', 'optimism', 'arbitrum', 'base', etc.) or chain ID. Supports all EVM-compatible networks. Defaults to Ethereum mainnet.") + description: "Review token approvals and identify security risks from unlimited spend", + argsSchema: { + address: z.string().optional().describe("Wallet to audit (default: configured wallet)"), + tokenAddress: z.string().describe("Token contract address to check approvals for"), + network: z.string().optional().describe("Network name (default: ethereum)") + } }, - ({ address, network = "ethereum" }) => ({ + ({ address, tokenAddress, network = "ethereum" }) => ({ messages: [{ role: "user", content: { type: "text", - text: `Please analyze the address ${address} on the ${network} network. Provide information about its balance, transaction count, and any other relevant information you can find.` + text: `# Token Approval Audit + +**Objective**: Check and analyze token approvals to identify security risks + +## Approval Analysis + +### 1. Get Configured Wallet (if needed) +- If no address provided: call \`get_wallet_address\` to get the configured wallet +- Use that as the owner for approval checks + +### 2. Check Current Approvals +- Call \`get_allowance\` with: + * tokenAddress: ${tokenAddress} + * ownerAddress: [wallet address from step 1] + * spenderAddress: [contract being analyzed] +- Note the allowance amount returned + +### 3. Interpret Results + +**Allowance = 0** +- No approval set +- User must approve before spender can use tokens +- Safe state + +**Allowance < Max Value** +- Limited approval (safest approach) +- Spender can only use up to this amount +- Tokens are protected + +**Allowance = Max uint256 (unlimited)** +- Dangerous! Spender has unlimited access +- Common but risky pattern +- Should be revoked if not actively used + +## Security Assessment + +For each approval found: +1. **Risk Level**: Low/Medium/High based on: + - Is it unlimited (high risk)? + - How trusted is the spender? + - Is it actively used? + +2. **Recommendations**: + - Revoke unknown/untrusted spenders + - Lower limits on high-risk approvals + - Keep active approvals but monitor + - Remove expired/legacy approvals + +## Output Format + +**Token Approval Audit Report** + +For each spender: +- **Spender Address**: [contract address] +- **Current Allowance**: [amount or "Unlimited"] +- **Risk Level**: Low/Medium/High +- **Status**: Active/Unused +- **Recommendation**: Keep/Reduce/Revoke + +**Summary** +- Total dangerous approvals: [count] +- Recommendations: [action items] +- Overall risk: Safe/Moderate/High + +## Important Notes +- Unlimited approvals are a major attack vector +- Only approve what's necessary +- Regularly audit and revoke unused approvals +- Be especially careful with new/unknown contracts +` } }] }) ); - // Smart contract interaction guidance - server.prompt( - "interact_with_contract", - "Get guidance on interacting with a smart contract", + // ============================================================================ + // SMART CONTRACT ANALYSIS PROMPTS + // ============================================================================ + + server.registerPrompt( + "fetch_and_analyze_abi", { - contractAddress: z.string().describe("The contract address"), - abiJson: z.string().optional().describe("The contract ABI as a JSON string"), - network: z.string().optional().describe("Network name or chain ID. Defaults to Ethereum mainnet.") + description: "Fetch contract ABI from block explorer and provide comprehensive analysis", + argsSchema: { + contractAddress: z.string().describe("Contract address to analyze"), + network: z.string().optional().describe("Network name (default: ethereum)"), + findFunction: z.string().optional().describe("Specific function to analyze (e.g., 'swap', 'mint')") + } }, - ({ contractAddress, abiJson, network = "ethereum" }) => ({ + ({ contractAddress, network = "ethereum", findFunction }) => ({ messages: [{ role: "user", content: { type: "text", - text: abiJson - ? `I need to interact with the smart contract at address ${contractAddress} on the ${network} network. Here's the ABI:\n\n${abiJson}\n\nPlease analyze this contract's functions and provide guidance on how to interact with it safely. Explain what each function does and what parameters it requires.` - : `I need to interact with the smart contract at address ${contractAddress} on the ${network} network. Please help me understand what this contract does and how I can interact with it safely.` + text: `# ABI Fetch and Analysis + +**Objective**: Retrieve and analyze contract ABI from block explorer + +## Prerequisites +- Contract must be verified on block explorer (Etherscan/Polygonscan/etc) +- ETHERSCAN_API_KEY environment variable required +- Supports 30+ EVM networks via unified Etherscan v2 API +- Read-only, no gas cost + +## Fetching Process + +### 1. Fetch the ABI +- Call \`get_contract_abi\` with contractAddress="${contractAddress}", network="${network}" +- Returns full ABI array with all functions, events, state variables +- Includes metadata about each function (inputs, outputs, mutability) + +### 2. Parse and Categorize +Organize functions by type: + +**View/Pure Functions** (Read-only, free): +- Check current state +- Query data without state change +- Safe to call + +**State-Changing Functions**: +- Payable: require ETH value +- Nonpayable: modify contract state +- Cost gas, need signer + +**Admin Functions**: +- Often restricted (onlyOwner, etc) +- Control contract behavior +- High risk if compromised + +### 3. Analyze Structure +- Count functions by type +- Identify events and their usage +- Look for special functions (constructor, fallback, receive) +- Check for custom errors + +${findFunction ? `### 4. Find Specific Function +- Search for "${findFunction}" in ABI +- Document: inputs, outputs, mutability +- Explain what it does +- Note any access controls` : `### 4. Key Functions +- Identify most important/used functions +- Explain inputs and outputs +- Note special requirements`} + +## Function Analysis Format + +For important functions provide: +- **Name**: Function name +- **Type**: View/Pure/Payable/Nonpayable +- **Inputs**: Parameter names and types with descriptions +- **Outputs**: Return values and types +- **Access**: Public/External/Restricted +- **Purpose**: What it does +- **Usage**: How to call it + +## Security Analysis + +Look for: +- **Proxy Patterns**: Is this a proxy contract? +- **Access Controls**: Who can call what? +- **Special Functions**: Initialization, upgrade paths +- **Obvious Issues**: Reentrancy risks, overflow/underflow patterns +- **Standard Compliance**: Is it ERC20/721/1155 compatible? + +## Output Format + +**Contract Analysis Report** + +- **Contract Type**: Identified purpose (Token/DEX/Lending/etc) +- **Network**: Where deployed +- **Verified**: Yes (since we fetched ABI) +- **Function Count**: Total functions by type + +**Function Categories**: +- View/Pure: [list of read functions] +- Write: [list of state-changing functions] +- Admin: [restricted functions] + +**Key Functions**: +[Detailed analysis of important functions] + +**Security Notes**: +[Vulnerabilities, patterns, recommendations] + +**How to Interact**: +[Step-by-step guide for common operations] +` } }] }) ); - // EVM concept explanation - server.prompt( - "explain_evm_concept", - "Get an explanation of an EVM concept", + server.registerPrompt( + "explore_contract", { - concept: z.string().describe("The EVM concept to explain (e.g., gas, nonce, etc.)") + description: "Analyze contract functions and state without requiring full ABI", + argsSchema: { + contractAddress: z.string().describe("Contract address to explore"), + network: z.string().optional().describe("Network name (default: ethereum)"), + fetchAbi: z.string().optional().describe("Set to 'true' to auto-fetch ABI (requires ETHERSCAN_API_KEY)") + } }, - ({ concept }) => ({ + ({ contractAddress, network = "ethereum", fetchAbi }) => ({ messages: [{ role: "user", content: { type: "text", - text: `Please explain the EVM Blockchain concept of "${concept}" in detail. Include how it works, why it's important, and provide examples if applicable.` + text: `# Contract Exploration + +**Objective**: Understand what contract ${contractAddress} does and how to use it + +## Exploration Strategy + +${fetchAbi === 'true' + ? `### With Full ABI (Fetched) +1. Call \`get_contract_abi\` to fetch verified ABI +2. Parse all available functions +3. Call \`read_contract\` for important state functions +4. Build comprehensive understanding +` + : `### Without Full ABI (Probing) +1. Test common function signatures +2. Call \`read_contract\` with standard functions: + - name(), symbol(), decimals(), totalSupply() + - owner(), paused(), version() + - balanceOf(), allowance(), totalSupply() +3. Infer contract type from successful calls +`} + +## Detection Process + +### 1. Identify Contract Type +Based on available functions, determine: +- **Token**: Has name, symbol, decimals, totalSupply, balanceOf +- **NFT/ERC721**: Has tokenURI, ownerOf, name, symbol +- **NFT/ERC1155**: Has uri, balanceOf, balanceOfBatch +- **Staking**: Has stake, unstake, reward, claim functions +- **DEX**: Has swap, liquidity, pair functions +- **Other**: Analyze unique functions + +### 2. Gather Key Information + +For each contract type: + +**Token (ERC20)**: +- name, symbol, decimals, totalSupply +- If owner, supply cap, minting rules +- If tax/fee mechanism + +**NFT (ERC721)**: +- name, symbol, totalSupply +- baseURI, tokenURI patterns +- royalty info if available + +**Staking/Farming**: +- Pool info, APY, reward token +- Lockup periods, early withdrawal penalties +- Reward distribution mechanism + +### 3. Security Assessment +- Check for pause functions (risk of rug) +- Look for upgrade mechanisms (upgradeable proxy) +- Identify admin-only functions +- Note unusual patterns + +## Output Format + +**Contract Overview** +- Address: [address] +- Type: [identified type] +- Network: [network] +- Verified: [yes/if ABI was fetched] + +**Key Properties** +[Type-specific details discovered] + +**Available Functions** +- Read-only: [list] +- State-changing: [list] +- Admin: [list if any] + +**How to Use** +[Step-by-step guide for primary use case] + +**Security Notes** +[Observations and recommendations] + +**Limitations** +[What couldn't be determined without full ABI] + +## When to Use ABI Fetch +- Need complete function list +- Want detailed parameter information +- Exploring unfamiliar/complex contracts +- Security due diligence +- Learn contract architecture +` } }] }) ); - // Network comparison - server.prompt( - "compare_networks", - "Compare different EVM-compatible networks", + // ============================================================================ + // NETWORK & EDUCATION PROMPTS + // ============================================================================ + + server.registerPrompt( + "interact_with_contract", { - networkList: z.string().describe("Comma-separated list of networks to compare (e.g., 'ethereum,optimism,arbitrum')") + description: "Safely execute write operations on a smart contract with validation and confirmation", + argsSchema: { + contractAddress: z.string().describe("Contract address to interact with"), + functionName: z.string().describe("Function to call (e.g., 'mint', 'swap', 'stake')"), + args: z.string().optional().describe("Comma-separated function arguments"), + value: z.string().optional().describe("ETH value to send (for payable functions)"), + network: z.string().optional().describe("Network name (default: ethereum)") + } }, - ({ networkList }) => { - const networks = networkList.split(',').map(n => n.trim()); + ({ contractAddress, functionName, args, value, network = "ethereum" }) => { + const argsList = args ? args.split(',').map(a => a.trim()) : []; return { messages: [{ role: "user", content: { type: "text", - text: `Please compare the following EVM-compatible networks: ${networks.join(', ')}. Include information about their architecture, gas fees, transaction speed, security, and any other relevant differences.` + text: `# Smart Contract Interaction + +**Objective**: Safely execute ${functionName} on contract ${contractAddress} on ${network} + +## Prerequisites Check + +### 1. Wallet Verification +- Call \`get_wallet_address\` to confirm the wallet that will execute this transaction +- Verify this is the correct wallet for this operation + +### 2. Contract Analysis +- Call \`get_contract_abi\` to fetch and analyze the contract ABI +- Verify the function exists and understand its parameters +- Check function type: + * **View/Pure**: Read-only (use \`read_contract\` instead) + * **Nonpayable**: State-changing, no ETH required + * **Payable**: State-changing, can accept ETH + +### 3. Function Parameter Validation +For function: **${functionName}** +${argsList.length > 0 ? `Arguments provided: ${argsList.join(', ')}` : 'No arguments provided'} + +- Verify parameter types match the ABI +- Validate addresses are checksummed +- Check numeric values are in correct units +- Resolve any ENS names to addresses if needed + +### 4. Pre-execution Checks + +**Balance Check**: +- Call \`get_balance\` to verify sufficient native token balance +- Account for gas costs + value (if payable) + +**Gas Estimation**: +- Call \`get_gas_price\` to estimate transaction cost +- Calculate total cost: (gas_price * estimated_gas) + value + +**State Verification** (if applicable): +- Use \`read_contract\` to check current contract state +- Verify conditions are met (e.g., allowances, balances, ownership) + +## Execution Process + +### 1. Present Summary to User +Before executing, show: +- **Contract**: ${contractAddress} +- **Network**: ${network} +- **Function**: ${functionName} +- **Arguments**: ${argsList.length > 0 ? argsList.join(', ') : 'None'} +${value ? `- **Value**: ${value} ETH` : ''} +- **From**: [wallet address from step 1] +- **Estimated Gas Cost**: [from gas estimation] +- **Total Cost**: [gas + value] + +### 2. Request User Confirmation +⚠️ **IMPORTANT**: Always ask user to confirm before executing write operations +- Clearly state what will happen +- Show all costs involved +- Explain any risks or irreversible actions + +### 3. Execute Transaction +Only after user confirms: +\`\`\` +Call write_contract with: +- contractAddress: "${contractAddress}" +- functionName: "${functionName}" +${argsList.length > 0 ? `- args: ${JSON.stringify(argsList)}` : ''} +${value ? `- value: "${value}"` : ''} +- network: "${network}" +\`\`\` + +### 4. Monitor Transaction +After execution: +1. Return transaction hash to user +2. Call \`wait_for_transaction\` to monitor confirmation +3. Call \`get_transaction_receipt\` to verify success +4. If failed, call \`diagnose_transaction\` to understand why + +## Output Format + +**Pre-Execution Summary**: +- Contract details +- Function and parameters +- Cost breakdown +- Risk assessment + +**Confirmation Request**: +"Ready to execute ${functionName} on ${contractAddress}. This will cost approximately [X] ETH. Proceed? (yes/no)" + +**Execution Result**: +- Transaction Hash: [hash] +- Status: Pending/Confirmed/Failed +- Block Number: [if confirmed] +- Gas Used: [actual gas used] +- Total Cost: [final cost] + +## Safety Considerations + +### Critical Checks +- ✅ Verify contract is verified on block explorer +- ✅ Check function parameters are correct type and format +- ✅ Ensure sufficient balance for gas + value +- ✅ Validate addresses (no typos, correct network) +- ✅ Understand what the function does before calling + +### Common Risks +- **Irreversible**: Most blockchain transactions cannot be undone +- **Gas Loss**: Failed transactions still consume gas +- **Approval Risks**: Be careful with unlimited approvals +- **Reentrancy**: Some functions may be vulnerable +- **Access Control**: Verify you have permission to call this function + +### Red Flags +🚨 Stop and warn user if: +- Contract is not verified +- Function requires admin/owner privileges you don't have +- Unusually high gas estimate +- Suspicious parameter values +- Contract has known vulnerabilities + +## Error Handling + +If transaction fails: +1. Get the revert reason from receipt +2. Check common issues: + - Insufficient balance/allowance + - Access control (onlyOwner, etc.) + - Invalid parameters + - Contract paused + - Slippage (for DEX operations) +3. Provide actionable fix suggestions +4. Offer to retry with corrected parameters + +## Example Workflow + +For a token mint operation: +1. ✅ Verify wallet +2. ✅ Fetch contract ABI +3. ✅ Check mint function exists and is callable +4. ✅ Verify sufficient ETH for gas +5. ✅ Show summary: "Minting 1 NFT will cost ~0.002 ETH" +6. ⏸️ Wait for user confirmation +7. ✅ Execute write_contract +8. ✅ Monitor transaction +9. ✅ Confirm success and return token ID + +**Remember**: Always prioritize user safety and transparency! +` } }] }; } ); - // Token analysis prompt - server.prompt( - "analyze_token", - "Analyze an ERC20 or NFT token", + server.registerPrompt( + "explain_evm_concept", { - tokenAddress: z.string().describe("Token contract address to analyze"), - tokenType: z.string().optional().describe("Type of token to analyze (erc20, erc721/nft, or auto-detect). Defaults to auto."), - tokenId: z.string().optional().describe("Token ID (required for NFT analysis)"), - network: z.string().optional().describe("Network name (e.g., 'ethereum', 'optimism', 'arbitrum', 'base', etc.) or chain ID. Supports all EVM-compatible networks. Defaults to Ethereum mainnet.") - }, - ({ tokenAddress, tokenType = "auto", tokenId, network = "ethereum" }) => { - let promptText = ""; - - if (tokenType === "erc20" || tokenType === "auto") { - promptText = `Please analyze the ERC20 token at address ${tokenAddress} on the ${network} network. Provide information about its name, symbol, total supply, and any other relevant details. If possible, explain the token's purpose, utility, and market context.`; - } else if ((tokenType === "erc721" || tokenType === "nft") && tokenId) { - promptText = `Please analyze the NFT with token ID ${tokenId} from the collection at address ${tokenAddress} on the ${network} network. Provide information about the collection name, token details, ownership history if available, and any other relevant information about this specific NFT.`; - } else if (tokenType === "nft" || tokenType === "erc721") { - promptText = `Please analyze the NFT collection at address ${tokenAddress} on the ${network} network. Provide information about the collection name, symbol, total supply if available, floor price if available, and any other relevant details about this NFT collection.`; + description: "Explain EVM and blockchain concepts with examples", + argsSchema: { + concept: z.string().describe("Concept to explain (gas, nonce, smart contracts, MEV, etc)") } + }, + ({ concept }) => ({ + messages: [{ + role: "user", + content: { + type: "text", + text: `# Concept Explanation: ${concept} + +**Objective**: Provide clear, practical explanation of "${concept}" + +## Explanation Structure + +### 1. Definition +- What is it? +- Simple one-sentence summary +- Technical name/terminology + +### 2. How It Works +- Step-by-step explanation +- Why it exists/why it's important +- How it relates to blockchain + +### 3. Real-World Analogy +- Compare to familiar concept +- Make it relatable for beginners +- Highlight key differences + +### 4. Practical Examples +- Real transaction examples +- Numbers and metrics where applicable +- Common scenarios +- Edge cases or gotchas + +### 5. Relevance to Users +- Why should developers care? +- How does it affect transactions? +- How to optimize/reduce costs? +- Common mistakes to avoid + +## Output Format +Provide explanation in sections: + +**What is ${concept}?** +[Definition and overview] + +**How Does It Work?** +[Mechanics and process] + +**Example** +[Real or hypothetical scenario] + +**Key Takeaways** +[Bullet points of important facts] + +**Common Questions** +- Question 1? Answer +- Question 2? Answer + +## Important +- Use clear, non-technical language first +- Progress to technical details +- Include concrete numbers where helpful +- Be honest about complexity +- Suggest further learning if needed +` + } + }] + }) + ); + + server.registerPrompt( + "compare_networks", + { + description: "Compare multiple EVM networks on key metrics and characteristics", + argsSchema: { + networks: z.string().describe("Comma-separated network names (ethereum,polygon,arbitrum)") + } + }, + ({ networks }) => { + const networkList = networks.split(',').map(n => n.trim()); return { messages: [{ role: "user", content: { type: "text", - text: promptText + text: `# Network Comparison + +**Objective**: Compare ${networkList.join(', ')} on key metrics + +## Comparison Metrics + +### 1. Network Health (Current) +For each network, call: +- \`get_chain_info\` for chain ID and current block +- \`get_gas_price\` for current gas costs +- \`get_latest_block\` for block time and recent activity + +### 2. Key Characteristics +Compare across these dimensions: + +**Architecture**: +- Execution layer (Rollup/Sidechain/L1) +- Consensus mechanism +- Finality +- Decentralization level + +**Performance**: +- Block time (seconds per block) +- Transactions per second (TPS) +- Confirmation time +- Throughput + +**Costs**: +- Current gas prices (in gwei) +- Average transaction cost +- Cost to deploy contract +- Price trends + +**Security**: +- Validator count / decentralization +- Mainnet maturity +- Track record +- Security audits + +**Ecosystem**: +- Major protocols deployed +- Liquidity depth +- Developer activity +- Community size + +## Comparison Table + +Create table with: +- Network name +- Block time +- TPS capacity +- Current gas (gwei) +- Est. tx cost (USD) +- Security level +- Best for + +## Analysis + +For each network: +- **Strengths**: What it does well +- **Weaknesses**: Limitations +- **Best Use Cases**: When to use +- **Trade-offs**: Speed vs cost vs security + +## Recommendations + +Provide guidance: +- For small frequent transactions: [network] +- For large one-time transfers: [network] +- For DeFi/trading: [network] +- For NFTs: [network] +- For cost optimization: [network] + +## Output Format + +**Network Comparison Analysis** + +[Comparison table] + +**Network Profiles** + +For each network: +- Overview +- Current metrics +- Strengths +- Weaknesses +- Best use cases + +**Recommendations** + +Based on user needs: +- Speed priority: [suggestion] +- Cost priority: [suggestion] +- Security priority: [suggestion] +- Overall best: [suggestion] + +**Decision Matrix** + +Help user choose based on: +- Transaction frequency +- Transaction size +- Budget constraints +- Required finality +- Ecosystem needs +` } }] }; } ); -} \ No newline at end of file + server.registerPrompt( + "check_network_status", + { + description: "Check current network health and conditions", + argsSchema: { + network: z.string().optional().describe("Network name (default: ethereum)") + } + }, + ({ network = "ethereum" }) => ({ + messages: [{ + role: "user", + content: { + type: "text", + text: `# Network Status Check + +**Objective**: Assess health and current conditions of ${network} + +## Status Assessment + +### 1. Gather Current Data +Call these read-only tools: +- \`get_chain_info\` for chain ID and current block number +- \`get_latest_block\` for block details and timing +- \`get_gas_price\` for current gas prices + +### 2. Network Health Analysis + +**Block Production**: +- Current block number +- Block timing (normal ~12-15 sec for Ethereum) +- Consistent vs irregular blocks +- Any gaps or delays + +**Gas Market**: +- Base fee level (in gwei) +- Priority fee level +- Gas price trend (up/down/stable) +- Congestion level + +**Overall Status**: +- Operational: Yes/No +- Issues detected: Yes/No +- Performance: Normal/Degraded/Critical + +### 3. Congestion Assessment + +Evaluate: +- Current gas prices vs average +- Pending transaction count +- Memory pool size +- Are transactions backing up? + +## Output Format + +**Network Status Report: ${network}** + +**Overall Status** +- Operational Status: [Online/Degraded/Offline] +- Current Block: [number] +- Network Time: [timestamp] +- Last Updated: [when] + +**Performance Metrics** +- Block Time: [seconds] (normal: 12-15s) +- Gas Base Fee: [gwei] +- Priority Fee: [gwei] +- Total Cost for Standard Tx: [estimate USD] + +**Congestion Level** +- Level: [Low/Moderate/High/Critical] +- Current vs Historical: [comparison] +- Trend: [increasing/stable/decreasing] + +**Network Activity** +- Blocks per minute: [rate] +- Recent block details: [hash, time, tx count] +- Network security: [indicators] + +**Recommendations** + +For **sending transactions now**: +- Best for: [low-value / high-value / time-critical] +- Gas setting: [standard / fast / extreme] +- Estimated cost: [range] +- Estimated wait time: [minutes] + +**If Congested**: +- Consider using: [alternative networks] +- Wait time: [estimated minutes] +- Cost to expedite: [gas increase needed] + +**If Issues Detected**: +- Known issues: [list if any] +- Expected duration: [if known] +- Recommended action: [wait / use alternate / etc] + +## Key Metrics + +Reference points for interpretation: +- Ethereum normal block: 12-15 seconds +- Polygon normal: 2 seconds +- Arbitrum normal: <1 second +- Normal gas: 20-50 gwei +- High congestion: 100+ gwei +` + } + }] + }) + ); +} diff --git a/src/core/resources.ts b/src/core/resources.ts index f8afca9..0e97523 100644 --- a/src/core/resources.ts +++ b/src/core/resources.ts @@ -1,636 +1,40 @@ -import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js"; -import { getSupportedNetworks, getRpcUrl } from "./chains.js"; -import * as services from "./services/index.js"; -import type { Address, Hash } from "viem"; +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { getSupportedNetworks } from "./chains.js"; /** - * Register all EVM-related resources + * Register EVM-related resources with the MCP server + * + * Resources are application-driven, read-only data that clients can explicitly load. + * For an AI agent use case, most data should be exposed through tools instead, + * which allow the model to discover and autonomously fetch information. + * + * The supported_networks resource provides a static reference list that clients + * may want to browse when configuring which networks to use. + * * @param server The MCP server instance */ export function registerEVMResources(server: McpServer) { - // Get EVM info for a specific network - server.resource( - "chain_info_by_network", - new ResourceTemplate("evm://{network}/chain", { list: undefined }), - async (uri, params) => { - try { - const network = params.network as string; - const chainId = await services.getChainId(network); - const blockNumber = await services.getBlockNumber(network); - const rpcUrl = getRpcUrl(network); - - return { - contents: [{ - uri: uri.href, - text: JSON.stringify({ - network, - chainId, - blockNumber: blockNumber.toString(), - rpcUrl - }, null, 2) - }] - }; - } catch (error) { - return { - contents: [{ - uri: uri.href, - text: `Error fetching chain info: ${error instanceof Error ? error.message : String(error)}` - }] - }; - } - } - ); - - // Default chain info (Ethereum mainnet) - server.resource( - "ethereum_chain_info", - "evm://chain", - async (uri) => { - try { - const network = "ethereum"; - const chainId = await services.getChainId(network); - const blockNumber = await services.getBlockNumber(network); - const rpcUrl = getRpcUrl(network); - - return { - contents: [{ - uri: uri.href, - text: JSON.stringify({ - network, - chainId, - blockNumber: blockNumber.toString(), - rpcUrl - }, null, 2) - }] - }; - } catch (error) { - return { - contents: [{ - uri: uri.href, - text: `Error fetching chain info: ${error instanceof Error ? error.message : String(error)}` - }] - }; - } - } - ); - - // Get block by number for a specific network - server.resource( - "evm_block_by_number", - new ResourceTemplate("evm://{network}/block/{blockNumber}", { list: undefined }), - async (uri, params) => { - try { - const network = params.network as string; - const blockNumber = params.blockNumber as string; - const block = await services.getBlockByNumber(parseInt(blockNumber), network); - - return { - contents: [{ - uri: uri.href, - text: services.helpers.formatJson(block) - }] - }; - } catch (error) { - return { - contents: [{ - uri: uri.href, - text: `Error fetching block: ${error instanceof Error ? error.message : String(error)}` - }] - }; - } - } - ); - - // Get block by hash for a specific network - server.resource( - "block_by_hash", - new ResourceTemplate("evm://{network}/block/hash/{blockHash}", { list: undefined }), - async (uri, params) => { - try { - const network = params.network as string; - const blockHash = params.blockHash as string; - const block = await services.getBlockByHash(blockHash as Hash, network); - - return { - contents: [{ - uri: uri.href, - text: services.helpers.formatJson(block) - }] - }; - } catch (error) { - return { - contents: [{ - uri: uri.href, - text: `Error fetching block with hash: ${error instanceof Error ? error.message : String(error)}` - }] - }; - } - } - ); - - // Get latest block for a specific network - server.resource( - "evm_latest_block", - new ResourceTemplate("evm://{network}/block/latest", { list: undefined }), - async (uri, params) => { - try { - const network = params.network as string; - const block = await services.getLatestBlock(network); - - return { - contents: [{ - uri: uri.href, - text: services.helpers.formatJson(block) - }] - }; - } catch (error) { - return { - contents: [{ - uri: uri.href, - text: `Error fetching latest block: ${error instanceof Error ? error.message : String(error)}` - }] - }; - } - } - ); - - // Default latest block (Ethereum mainnet) - server.resource( - "default_latest_block", - "evm://block/latest", - async (uri) => { - try { - const network = "ethereum"; - const block = await services.getLatestBlock(network); - - return { - contents: [{ - uri: uri.href, - text: services.helpers.formatJson(block) - }] - }; - } catch (error) { - return { - contents: [{ - uri: uri.href, - text: `Error fetching latest block: ${error instanceof Error ? error.message : String(error)}` - }] - }; - } - } - ); - - // Get ETH balance for a specific network - server.resource( - "evm_address_native_balance", - new ResourceTemplate("evm://{network}/address/{address}/balance", { list: undefined }), - async (uri, params) => { - try { - const network = params.network as string; - const address = params.address as string; - const balance = await services.getETHBalance(address as Address, network); - - return { - contents: [{ - uri: uri.href, - text: JSON.stringify({ - network, - address, - balance: { - wei: balance.wei.toString(), - ether: balance.ether - } - }, null, 2) - }] - }; - } catch (error) { - return { - contents: [{ - uri: uri.href, - text: `Error fetching ETH balance: ${error instanceof Error ? error.message : String(error)}` - }] - }; - } - } - ); - - // Default ETH balance (Ethereum mainnet) - server.resource( - "default_eth_balance", - new ResourceTemplate("evm://address/{address}/eth-balance", { list: undefined }), - async (uri, params) => { - try { - const network = "ethereum"; - const address = params.address as string; - const balance = await services.getETHBalance(address as Address, network); - - return { - contents: [{ - uri: uri.href, - text: JSON.stringify({ - network, - address, - balance: { - wei: balance.wei.toString(), - ether: balance.ether - } - }, null, 2) - }] - }; - } catch (error) { - return { - contents: [{ - uri: uri.href, - text: `Error fetching ETH balance: ${error instanceof Error ? error.message : String(error)}` - }] - }; - } - } - ); - - // Get ERC20 balance for a specific network - server.resource( - "erc20_balance", - new ResourceTemplate("evm://{network}/address/{address}/token/{tokenAddress}/balance", { list: undefined }), - async (uri, params) => { - try { - const network = params.network as string; - const address = params.address as string; - const tokenAddress = params.tokenAddress as string; - - const balance = await services.getERC20Balance( - tokenAddress as Address, - address as Address, - network - ); - - return { - contents: [{ - uri: uri.href, - text: JSON.stringify({ - network, - address, - tokenAddress, - balance: { - raw: balance.raw.toString(), - formatted: balance.formatted, - decimals: balance.token.decimals - } - }, null, 2) - }] - }; - } catch (error) { - return { - contents: [{ - uri: uri.href, - text: `Error fetching ERC20 balance: ${error instanceof Error ? error.message : String(error)}` - }] - }; - } - } - ); - - // Default ERC20 balance (Ethereum mainnet) - server.resource( - "default_erc20_balance", - new ResourceTemplate("evm://address/{address}/token/{tokenAddress}/balance", { list: undefined }), - async (uri, params) => { - try { - const network = "ethereum"; - const address = params.address as string; - const tokenAddress = params.tokenAddress as string; - - const balance = await services.getERC20Balance( - tokenAddress as Address, - address as Address, - network - ); - - return { - contents: [{ - uri: uri.href, - text: JSON.stringify({ - network, - address, - tokenAddress, - balance: { - raw: balance.raw.toString(), - formatted: balance.formatted, - decimals: balance.token.decimals - } - }, null, 2) - }] - }; - } catch (error) { - return { - contents: [{ - uri: uri.href, - text: `Error fetching ERC20 balance: ${error instanceof Error ? error.message : String(error)}` - }] - }; - } - } - ); - - // Get transaction by hash for a specific network - server.resource( - "evm_transaction_details", - new ResourceTemplate("evm://{network}/tx/{txHash}", { list: undefined }), - async (uri, params) => { - try { - const network = params.network as string; - const txHash = params.txHash as string; - const tx = await services.getTransaction(txHash as Hash, network); - - return { - contents: [{ - uri: uri.href, - text: services.helpers.formatJson(tx) - }] - }; - } catch (error) { - return { - contents: [{ - uri: uri.href, - text: `Error fetching transaction: ${error instanceof Error ? error.message : String(error)}` - }] - }; - } - } - ); - - // Default transaction by hash (Ethereum mainnet) - server.resource( - "default_transaction_by_hash", - new ResourceTemplate("evm://tx/{txHash}", { list: undefined }), - async (uri, params) => { - try { - const network = "ethereum"; - const txHash = params.txHash as string; - const tx = await services.getTransaction(txHash as Hash, network); - - return { - contents: [{ - uri: uri.href, - text: services.helpers.formatJson(tx) - }] - }; - } catch (error) { - return { - contents: [{ - uri: uri.href, - text: `Error fetching transaction: ${error instanceof Error ? error.message : String(error)}` - }] - }; - } - } - ); - - // Get supported networks - server.resource( + server.registerResource( "supported_networks", "evm://networks", + { description: "Get list of all supported EVM networks and their configuration", mimeType: "application/json" }, async (uri) => { try { const networks = getSupportedNetworks(); - - return { - contents: [{ - uri: uri.href, - text: JSON.stringify({ - supportedNetworks: networks - }, null, 2) - }] - }; - } catch (error) { - return { - contents: [{ - uri: uri.href, - text: `Error fetching supported networks: ${error instanceof Error ? error.message : String(error)}` - }] - }; - } - } - ); - - // Add ERC20 token info resource - server.resource( - "erc20_token_details", - new ResourceTemplate("evm://{network}/token/{tokenAddress}", { list: undefined }), - async (uri, params) => { - try { - const network = params.network as string; - const tokenAddress = params.tokenAddress as Address; - - const tokenInfo = await services.getERC20TokenInfo(tokenAddress, network); - - return { - contents: [{ - uri: uri.href, - text: JSON.stringify({ - address: tokenAddress, - network, - ...tokenInfo - }, null, 2) - }] - }; - } catch (error) { - return { - contents: [{ - uri: uri.href, - text: `Error fetching ERC20 token info: ${error instanceof Error ? error.message : String(error)}` - }] - }; - } - } - ); - - // Add ERC20 token balance resource - server.resource( - "erc20_token_address_balance", - new ResourceTemplate("evm://{network}/token/{tokenAddress}/balanceOf/{address}", { list: undefined }), - async (uri, params) => { - try { - const network = params.network as string; - const tokenAddress = params.tokenAddress as Address; - const address = params.address as Address; - - const balance = await services.getERC20Balance(tokenAddress, address, network); - - return { - contents: [{ - uri: uri.href, - text: JSON.stringify({ - tokenAddress, - owner: address, - network, - raw: balance.raw.toString(), - formatted: balance.formatted, - symbol: balance.token.symbol, - decimals: balance.token.decimals - }, null, 2) - }] - }; - } catch (error) { - return { - contents: [{ - uri: uri.href, - text: `Error fetching ERC20 token balance: ${error instanceof Error ? error.message : String(error)}` - }] - }; - } - } - ); - - // Add NFT (ERC721) token info resource - server.resource( - "erc721_nft_token_details", - new ResourceTemplate("evm://{network}/nft/{tokenAddress}/{tokenId}", { list: undefined }), - async (uri, params) => { - try { - const network = params.network as string; - const tokenAddress = params.tokenAddress as Address; - const tokenId = BigInt(params.tokenId as string); - - const nftInfo = await services.getERC721TokenMetadata(tokenAddress, tokenId, network); - - // Get owner separately - let owner = "Unknown"; - try { - const isOwner = await services.isNFTOwner(tokenAddress, params.address as Address, tokenId, network); - if (isOwner) { - owner = params.address as string; - } - } catch (e) { - // Owner info not available - } - - return { - contents: [{ - uri: uri.href, - text: JSON.stringify({ - contract: tokenAddress, - tokenId: tokenId.toString(), - network, - ...nftInfo, - owner - }, null, 2) - }] - }; - } catch (error) { - return { - contents: [{ - uri: uri.href, - text: `Error fetching NFT info: ${error instanceof Error ? error.message : String(error)}` - }] - }; - } - } - ); - - // Add NFT ownership check resource - server.resource( - "erc721_nft_ownership_check", - new ResourceTemplate("evm://{network}/nft/{tokenAddress}/{tokenId}/isOwnedBy/{address}", { list: undefined }), - async (uri, params) => { - try { - const network = params.network as string; - const tokenAddress = params.tokenAddress as Address; - const tokenId = BigInt(params.tokenId as string); - const address = params.address as Address; - - const isOwner = await services.isNFTOwner(tokenAddress, address, tokenId, network); - - return { - contents: [{ - uri: uri.href, - text: JSON.stringify({ - contract: tokenAddress, - tokenId: tokenId.toString(), - owner: address, - network, - isOwner - }, null, 2) - }] - }; - } catch (error) { - return { - contents: [{ - uri: uri.href, - text: `Error checking NFT ownership: ${error instanceof Error ? error.message : String(error)}` - }] - }; - } - } - ); - - // Add ERC1155 token URI resource - server.resource( - "erc1155_token_metadata_uri", - new ResourceTemplate("evm://{network}/erc1155/{tokenAddress}/{tokenId}/uri", { list: undefined }), - async (uri, params) => { - try { - const network = params.network as string; - const tokenAddress = params.tokenAddress as Address; - const tokenId = BigInt(params.tokenId as string); - - const tokenURI = await services.getERC1155TokenURI(tokenAddress, tokenId, network); - - return { - contents: [{ - uri: uri.href, - text: JSON.stringify({ - contract: tokenAddress, - tokenId: tokenId.toString(), - network, - uri: tokenURI - }, null, 2) - }] - }; - } catch (error) { - return { - contents: [{ - uri: uri.href, - text: `Error fetching ERC1155 token URI: ${error instanceof Error ? error.message : String(error)}` - }] - }; - } - } - ); - - // Add ERC1155 token balance resource - server.resource( - "erc1155_token_address_balance", - new ResourceTemplate("evm://{network}/erc1155/{tokenAddress}/{tokenId}/balanceOf/{address}", { list: undefined }), - async (uri, params) => { - try { - const network = params.network as string; - const tokenAddress = params.tokenAddress as Address; - const tokenId = BigInt(params.tokenId as string); - const address = params.address as Address; - - const balance = await services.getERC1155Balance(tokenAddress, address, tokenId, network); - return { contents: [{ uri: uri.href, - text: JSON.stringify({ - contract: tokenAddress, - tokenId: tokenId.toString(), - owner: address, - network, - balance: balance.toString() - }, null, 2) + text: JSON.stringify({ supportedNetworks: networks }, null, 2) }] }; } catch (error) { return { contents: [{ uri: uri.href, - text: `Error fetching ERC1155 token balance: ${error instanceof Error ? error.message : String(error)}` + text: `Error: ${error instanceof Error ? error.message : String(error)}` }] }; } } ); -} \ No newline at end of file +} diff --git a/src/core/services/abi.ts b/src/core/services/abi.ts new file mode 100644 index 0000000..d4e7757 --- /dev/null +++ b/src/core/services/abi.ts @@ -0,0 +1,107 @@ +import { type Address } from 'viem'; +import { resolveChainId, getSupportedNetworks } from '../chains.js'; + +/** + * Fetch contract ABI from Etherscan v2 API (unified endpoint for all EVM chains) + * Requires ETHERSCAN_API_KEY environment variable to be set + * + * @param contractAddress The contract address to fetch ABI for + * @param network The network name or chain ID + * @returns The contract ABI as a JSON string + */ +export async function fetchContractABI( + contractAddress: Address, + network: string = 'ethereum' +): Promise { + const apiKey = process.env.ETHERSCAN_API_KEY; + if (!apiKey) { + throw new Error('ETHERSCAN_API_KEY environment variable is not set. Set it to fetch contract ABIs from block explorers.'); + } + + // Resolve chain ID using the chains.ts utilities + let chainId: number; + try { + chainId = resolveChainId(network); + } catch (error) { + const supported = getSupportedNetworks(); + throw new Error(`Network "${network}" is not supported. Supported: ${supported.join(', ')}`); + } + + try { + // Use unified Etherscan v2 API endpoint + const url = new URL('https://api.etherscan.io/v2/api'); + url.searchParams.set('module', 'contract'); + url.searchParams.set('action', 'getabi'); + url.searchParams.set('address', contractAddress); + url.searchParams.set('chainid', chainId.toString()); + url.searchParams.set('apikey', apiKey); + + const response = await fetch(url.toString()); + const data = await response.json() as any; + + if (data.status === '0') { + throw new Error(data.result || 'Failed to fetch ABI from block explorer'); + } + + if (!data.result) { + throw new Error('No ABI found for this contract. Contract might not be verified.'); + } + + return data.result; + } catch (error) { + if (error instanceof Error) { + throw new Error(`Failed to fetch ABI: ${error.message}`); + } + throw error; + } +} + +/** + * Parse and validate an ABI JSON string + * @param abiJson The ABI as a JSON string + * @returns Parsed ABI array + */ +export function parseABI(abiJson: string): any[] { + try { + const abi = JSON.parse(abiJson); + if (!Array.isArray(abi)) { + throw new Error('ABI must be a JSON array'); + } + return abi; + } catch (error) { + throw new Error(`Invalid ABI JSON: ${error instanceof Error ? error.message : String(error)}`); + } +} + +/** + * Get list of readable functions from an ABI + * @param abi The contract ABI + * @returns Array of read-only function names + */ +export function getReadableFunctions(abi: any[]): string[] { + return abi + .filter(item => + item.type === 'function' && + (item.stateMutability === 'view' || item.stateMutability === 'pure') + ) + .map(item => item.name) + .filter(Boolean); +} + +/** + * Get a specific function from an ABI + * @param abi The contract ABI + * @param functionName The function name to find + * @returns The function ABI object + */ +export function getFunctionFromABI(abi: any[], functionName: string): any { + const fn = abi.find(item => + item.type === 'function' && item.name === functionName + ); + + if (!fn) { + throw new Error(`Function "${functionName}" not found in ABI`); + } + + return fn; +} diff --git a/src/core/services/index.ts b/src/core/services/index.ts index f7da50a..14108ed 100644 --- a/src/core/services/index.ts +++ b/src/core/services/index.ts @@ -7,12 +7,14 @@ export * from './transactions.js'; export * from './contracts.js'; export * from './tokens.js'; export * from './ens.js'; +export * from './abi.js'; +export * from './wallet.js'; export { utils as helpers } from './utils.js'; // Re-export common types for convenience -export type { - Address, - Hash, +export type { + Address, + Hash, Hex, Block, TransactionReceipt, diff --git a/src/core/services/wallet.ts b/src/core/services/wallet.ts new file mode 100644 index 0000000..799bc10 --- /dev/null +++ b/src/core/services/wallet.ts @@ -0,0 +1,87 @@ +import { type Address, type Hex } from 'viem'; +import { privateKeyToAccount, mnemonicToAccount, type HDAccount, type PrivateKeyAccount } from 'viem/accounts'; + +/** + * Get the configured account from environment (private key or mnemonic) + * + * Configuration options: + * - EVM_PRIVATE_KEY: Hex private key (with or without 0x prefix) + * - EVM_MNEMONIC: BIP-39 mnemonic phrase (12 or 24 words) + * - EVM_ACCOUNT_INDEX: Optional account index for HD wallet derivation (default: 0) + */ +export const getConfiguredAccount = (): HDAccount | PrivateKeyAccount => { + const privateKey = process.env.EVM_PRIVATE_KEY; + const mnemonic = process.env.EVM_MNEMONIC; + const accountIndexStr = process.env.EVM_ACCOUNT_INDEX || '0'; + const accountIndex = parseInt(accountIndexStr, 10); + + // Validate account index + if (isNaN(accountIndex) || accountIndex < 0 || !Number.isInteger(accountIndex)) { + throw new Error( + `Invalid EVM_ACCOUNT_INDEX: "${accountIndexStr}". Must be a non-negative integer.` + ); + } + + if (privateKey) { + // Use private key if provided + const key = (privateKey.startsWith('0x') ? privateKey : `0x${privateKey}`) as Hex; + return privateKeyToAccount(key); + } else if (mnemonic) { + // Use mnemonic if provided + return mnemonicToAccount(mnemonic, { accountIndex }); + } else { + throw new Error( + "Neither EVM_PRIVATE_KEY nor EVM_MNEMONIC environment variable is set. " + + "Configure one of them to enable write operations.\n" + + "- EVM_PRIVATE_KEY: Your private key in hex format\n" + + "- EVM_MNEMONIC: Your 12 or 24 word mnemonic phrase\n" + + "- EVM_ACCOUNT_INDEX: (Optional) Account index for HD wallet (default: 0)" + ); + } +}; + +/** + * Helper to get the configured private key (for services that need it) + * + * For HDAccount (from mnemonic): extracts private key from HD key + * For PrivateKeyAccount: returns the original private key + */ +export const getConfiguredPrivateKey = (): Hex => { + const account = getConfiguredAccount(); + + // Check if this is an HDAccount (has getHdKey method) + if ('getHdKey' in account && typeof account.getHdKey === 'function') { + const hdKey = account.getHdKey(); + if (!hdKey.privateKey) { + throw new Error("Unable to derive private key from HD account - no private key in HD key"); + } + // Convert Uint8Array to hex string + const privateKeyHex = Buffer.from(hdKey.privateKey).toString('hex'); + return `0x${privateKeyHex}` as Hex; + } + + // For PrivateKeyAccount, re-read from environment since we created from it + if ('source' in account && account.source === 'privateKey') { + const privateKey = process.env.EVM_PRIVATE_KEY; + if (privateKey) { + return (privateKey.startsWith('0x') ? privateKey : `0x${privateKey}`) as Hex; + } + } + + throw new Error("Unable to extract private key from account"); +}; + +/** + * Helper to get wallet address + */ +export const getWalletAddressFromKey = (): Address => { + const account = getConfiguredAccount(); + return account.address; +}; + +/** + * Helper to get configured wallet object + */ +export const getConfiguredWallet = (): { address: Address } => { + return { address: getWalletAddressFromKey() }; +}; diff --git a/src/core/tools.ts b/src/core/tools.ts index a01d7a7..ee2764b 100644 --- a/src/core/tools.ts +++ b/src/core/tools.ts @@ -7,1190 +7,1087 @@ import { normalize } from 'viem/ens'; /** * Register all EVM-related tools with the MCP server - * - * All tools that accept Ethereum addresses also support ENS names (e.g., 'vitalik.eth'). + * + * SECURITY: Either EVM_PRIVATE_KEY or EVM_MNEMONIC environment variable must be set for write operations. + * Private keys and mnemonics are never passed as tool arguments for security reasons. + * Tools will use the configured wallet for all transactions. + * + * Configuration options: + * - EVM_PRIVATE_KEY: Hex private key (with or without 0x prefix) + * - EVM_MNEMONIC: BIP-39 mnemonic phrase (12 or 24 words) + * - EVM_ACCOUNT_INDEX: Optional account index for HD wallet derivation (default: 0) + * + * All tools that accept addresses also support ENS names (e.g., 'vitalik.eth'). * ENS names are automatically resolved to addresses using the Ethereum Name Service. - * + * * @param server The MCP server instance */ export function registerEVMTools(server: McpServer) { - // NETWORK INFORMATION TOOLS - - // Get chain information - server.tool( - "get_chain_info", - "Get information about an EVM network", + // Helpers are now imported from services/wallet.ts + const { getConfiguredPrivateKey, getWalletAddressFromKey, getConfiguredWallet } = services; + + // ============================================================================ + // WALLET INFORMATION TOOLS (Read-only) + // ============================================================================ + + server.registerTool( + "get_wallet_address", { - network: z.string().optional().describe("Network name (e.g., 'ethereum', 'optimism', 'arbitrum', 'base', etc.) or chain ID. Supports all EVM-compatible networks. Defaults to Ethereum mainnet.") + description: "Get the address of the configured wallet. Use this to verify which wallet is active.", + inputSchema: {}, + annotations: { + title: "Get Wallet Address", + readOnlyHint: true, + destructiveHint: false, + idempotentHint: true, + openWorldHint: false + } }, - async ({ network = "ethereum" }) => { + async () => { try { - const chainId = await services.getChainId(network); - const blockNumber = await services.getBlockNumber(network); - const rpcUrl = getRpcUrl(network); - + const address = getWalletAddressFromKey(); return { content: [{ type: "text", text: JSON.stringify({ - network, - chainId, - blockNumber: blockNumber.toString(), - rpcUrl + address, + message: "This is the wallet that will be used for all transactions" }, null, 2) }] }; } catch (error) { return { - content: [{ - type: "text", - text: `Error fetching chain info: ${error instanceof Error ? error.message : String(error)}` - }], + content: [{ type: "text", text: `Error: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } } ); - // ENS LOOKUP TOOL - - // Resolve ENS name to address - server.tool( - "resolve_ens", - "Resolve an ENS name to an Ethereum address", + // ============================================================================ + // NETWORK INFORMATION TOOLS (Read-only) + // ============================================================================ + + server.registerTool( + "get_chain_info", { - ensName: z.string().describe("ENS name to resolve (e.g., 'vitalik.eth')"), - network: z.string().optional().describe("Network name (e.g., 'ethereum', 'optimism', 'arbitrum', 'base', etc.) or chain ID. ENS resolution works best on Ethereum mainnet. Defaults to Ethereum mainnet.") + description: "Get information about an EVM network: chain ID, current block number, and RPC endpoint", + inputSchema: { + network: z.string().optional().describe("Network name (e.g., 'ethereum', 'optimism', 'arbitrum', 'base') or chain ID. Defaults to Ethereum mainnet.") + }, + annotations: { + title: "Get Chain Info", + readOnlyHint: true, + destructiveHint: false, + idempotentHint: true, + openWorldHint: true + } }, - async ({ ensName, network = "ethereum" }) => { + async ({ network = "ethereum" }) => { try { - // Validate that the input is an ENS name - if (!ensName.includes('.')) { - return { - content: [{ - type: "text", - text: `Error: Input "${ensName}" is not a valid ENS name. ENS names must contain a dot (e.g., 'name.eth').` - }], - isError: true - }; - } - - // Normalize the ENS name - const normalizedEns = normalize(ensName); - - // Resolve the ENS name to an address - const address = await services.resolveAddress(ensName, network); - + const chainId = await services.getChainId(network); + const blockNumber = await services.getBlockNumber(network); + const rpcUrl = getRpcUrl(network); + return { content: [{ type: "text", - text: JSON.stringify({ - ensName: ensName, - normalizedName: normalizedEns, - resolvedAddress: address, - network - }, null, 2) + text: JSON.stringify({ network, chainId, blockNumber: blockNumber.toString(), rpcUrl }, null, 2) }] }; } catch (error) { return { - content: [{ - type: "text", - text: `Error resolving ENS name: ${error instanceof Error ? error.message : String(error)}` - }], + content: [{ type: "text", text: `Error fetching chain info: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } } ); - // Get supported networks - server.tool( + server.registerTool( "get_supported_networks", - "Get a list of supported EVM networks", - {}, + { + description: "Get a list of all supported EVM networks", + inputSchema: {}, + annotations: { + title: "Get Supported Networks", + readOnlyHint: true, + destructiveHint: false, + idempotentHint: true, + openWorldHint: false + } + }, async () => { try { const networks = getSupportedNetworks(); - return { - content: [{ - type: "text", - text: JSON.stringify({ - supportedNetworks: networks - }, null, 2) - }] + content: [{ type: "text", text: JSON.stringify({ supportedNetworks: networks }, null, 2) }] }; } catch (error) { return { - content: [{ - type: "text", - text: `Error fetching supported networks: ${error instanceof Error ? error.message : String(error)}` - }], + content: [{ type: "text", text: `Error: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } } ); - // BLOCK TOOLS - - // Get block by number - server.tool( - "get_block_by_number", - "Get a block by its block number", + server.registerTool( + "get_gas_price", { - blockNumber: z.number().describe("The block number to fetch"), - network: z.string().optional().describe("Network name or chain ID. Defaults to Ethereum mainnet.") - }, - async ({ blockNumber, network = "ethereum" }) => { - try { - const block = await services.getBlockByNumber(blockNumber, network); - - return { - content: [{ - type: "text", - text: services.helpers.formatJson(block) - }] - }; - } catch (error) { - return { - content: [{ - type: "text", - text: `Error fetching block ${blockNumber}: ${error instanceof Error ? error.message : String(error)}` - }], - isError: true - }; + description: "Get current gas prices (base fee, standard, and fast) for a network", + inputSchema: { + network: z.string().optional().describe("Network name or chain ID. Defaults to Ethereum mainnet.") + }, + annotations: { + title: "Get Gas Prices", + readOnlyHint: true, + destructiveHint: false, + idempotentHint: false, + openWorldHint: true } - } - ); - - // Get latest block - server.tool( - "get_latest_block", - "Get the latest block from the EVM", - { - network: z.string().optional().describe("Network name or chain ID. Defaults to Ethereum mainnet.") }, async ({ network = "ethereum" }) => { try { - const block = await services.getLatestBlock(network); - - return { - content: [{ - type: "text", - text: services.helpers.formatJson(block) - }] - }; - } catch (error) { - return { - content: [{ - type: "text", - text: `Error fetching latest block: ${error instanceof Error ? error.message : String(error)}` - }], - isError: true - }; - } - } - ); + const client = await services.getPublicClient(network); + const [baseFee, priorityFee] = await Promise.all([ + client.getGasPrice(), + client.estimateMaxPriorityFeePerGas() + ]); - // BALANCE TOOLS - - // Get ETH balance - server.tool( - "get_balance", - "Get the native token balance (ETH, MATIC, etc.) for an address", - { - address: z.string().describe("The wallet address or ENS name (e.g., '0x1234...' or 'vitalik.eth') to check the balance for"), - network: z.string().optional().describe("Network name (e.g., 'ethereum', 'optimism', 'arbitrum', 'base', etc.) or chain ID. Supports all EVM-compatible networks. Defaults to Ethereum mainnet.") - }, - async ({ address, network = "ethereum" }) => { - try { - const balance = await services.getETHBalance(address, network); - return { content: [{ type: "text", text: JSON.stringify({ - address, network, - wei: balance.wei.toString(), - ether: balance.ether + baseFeePerGas: baseFee.toString(), + priorityFeePerGas: priorityFee?.toString() || "N/A", + currency: "wei" }, null, 2) }] }; } catch (error) { return { - content: [{ - type: "text", - text: `Error fetching balance: ${error instanceof Error ? error.message : String(error)}` - }], + content: [{ type: "text", text: `Error fetching gas prices: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } } ); - // Get ERC20 balance - server.tool( - "get_erc20_balance", - "Get the ERC20 token balance of an Ethereum address", + // ============================================================================ + // ENS TOOLS (Read-only) + // ============================================================================ + + server.registerTool( + "resolve_ens_name", { - address: z.string().describe("The Ethereum address to check"), - tokenAddress: z.string().describe("The ERC20 token contract address"), - network: z.string().optional().describe("Network name or chain ID. Defaults to Ethereum mainnet.") + description: "Resolve an ENS name to an Ethereum address", + inputSchema: { + ensName: z.string().describe("ENS name to resolve (e.g., 'vitalik.eth')"), + network: z.string().optional().describe("Network name or chain ID. ENS resolution works best on Ethereum mainnet. Defaults to Ethereum mainnet.") + }, + annotations: { + title: "Resolve ENS Name", + readOnlyHint: true, + destructiveHint: false, + idempotentHint: true, + openWorldHint: true + } }, - async ({ address, tokenAddress, network = "ethereum" }) => { + async ({ ensName, network = "ethereum" }) => { try { - const balance = await services.getERC20Balance( - tokenAddress as Address, - address as Address, - network - ); - + if (!ensName.includes('.')) { + return { + content: [{ type: "text", text: `Error: "${ensName}" is not a valid ENS name. ENS names must contain a dot (e.g., 'name.eth').` }], + isError: true + }; + } + const normalizedEns = normalize(ensName); + const address = await services.resolveAddress(ensName, network); + return { content: [{ type: "text", text: JSON.stringify({ - address, - tokenAddress, - network, - balance: { - raw: balance.raw.toString(), - formatted: balance.formatted, - decimals: balance.token.decimals - } + ensName, + normalizedName: normalizedEns, + resolvedAddress: address, + network }, null, 2) }] }; } catch (error) { return { - content: [{ - type: "text", - text: `Error fetching ERC20 balance for ${address}: ${error instanceof Error ? error.message : String(error)}` - }], + content: [{ type: "text", text: `Error resolving ENS name: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } } ); - // Get ERC20 token balance - server.tool( - "get_token_balance", - "Get the balance of an ERC20 token for an address", + server.registerTool( + "lookup_ens_address", { - tokenAddress: z.string().describe("The contract address or ENS name of the ERC20 token (e.g., '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48' for USDC or 'uniswap.eth')"), - ownerAddress: z.string().describe("The wallet address or ENS name to check the balance for (e.g., '0x1234...' or 'vitalik.eth')"), - network: z.string().optional().describe("Network name (e.g., 'ethereum', 'optimism', 'arbitrum', 'base', etc.) or chain ID. Supports all EVM-compatible networks. Defaults to Ethereum mainnet.") + description: "Lookup the ENS name for an Ethereum address (reverse resolution)", + inputSchema: { + address: z.string().describe("Ethereum address to lookup"), + network: z.string().optional().describe("Network name or chain ID. Defaults to Ethereum mainnet.") + }, + annotations: { + title: "Lookup ENS Address", + readOnlyHint: true, + destructiveHint: false, + idempotentHint: true, + openWorldHint: true + } }, - async ({ tokenAddress, ownerAddress, network = "ethereum" }) => { + async ({ address, network = "ethereum" }) => { try { - const balance = await services.getERC20Balance(tokenAddress, ownerAddress, network); - + const client = await services.getPublicClient(network); + const ensName = await client.getEnsName({ + address: address as Address + }); return { content: [{ type: "text", text: JSON.stringify({ - tokenAddress, - owner: ownerAddress, - network, - raw: balance.raw.toString(), - formatted: balance.formatted, - symbol: balance.token.symbol, - decimals: balance.token.decimals + address, + ensName: ensName || "No ENS name found", + network }, null, 2) }] }; } catch (error) { return { - content: [{ - type: "text", - text: `Error fetching token balance: ${error instanceof Error ? error.message : String(error)}` - }], + content: [{ type: "text", text: `Error looking up ENS name: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } } ); - // TRANSACTION TOOLS - - // Get transaction by hash - server.tool( - "get_transaction", - "Get detailed information about a specific transaction by its hash. Includes sender, recipient, value, data, and more.", - { - txHash: z.string().describe("The transaction hash to look up (e.g., '0x1234...')"), - network: z.string().optional().describe("Network name (e.g., 'ethereum', 'optimism', 'arbitrum', 'base', 'polygon') or chain ID. Defaults to Ethereum mainnet.") - }, - async ({ txHash, network = "ethereum" }) => { - try { - const tx = await services.getTransaction(txHash as Hash, network); - - return { - content: [{ - type: "text", - text: services.helpers.formatJson(tx) - }] - }; - } catch (error) { - return { - content: [{ - type: "text", - text: `Error fetching transaction ${txHash}: ${error instanceof Error ? error.message : String(error)}` - }], - isError: true - }; - } - } - ); + // ============================================================================ + // BLOCK TOOLS (Read-only) + // ============================================================================ - // Get transaction receipt - server.tool( - "get_transaction_receipt", - "Get a transaction receipt by its hash", + server.registerTool( + "get_block", { - txHash: z.string().describe("The transaction hash to look up"), - network: z.string().optional().describe("Network name or chain ID. Defaults to Ethereum mainnet.") - }, - async ({ txHash, network = "ethereum" }) => { - try { - const receipt = await services.getTransactionReceipt(txHash as Hash, network); - - return { - content: [{ - type: "text", - text: services.helpers.formatJson(receipt) - }] - }; - } catch (error) { - return { - content: [{ - type: "text", - text: `Error fetching transaction receipt ${txHash}: ${error instanceof Error ? error.message : String(error)}` - }], - isError: true - }; + description: "Get block details by block number or hash", + inputSchema: { + blockIdentifier: z.string().describe("Block number (as string) or block hash"), + network: z.string().optional().describe("Network name or chain ID. Defaults to Ethereum mainnet.") + }, + annotations: { + title: "Get Block", + readOnlyHint: true, + destructiveHint: false, + idempotentHint: true, + openWorldHint: true } - } - ); - - // Estimate gas - server.tool( - "estimate_gas", - "Estimate the gas cost for a transaction", - { - to: z.string().describe("The recipient address"), - value: z.string().optional().describe("The amount of ETH to send in ether (e.g., '0.1')"), - data: z.string().optional().describe("The transaction data as a hex string"), - network: z.string().optional().describe("Network name or chain ID. Defaults to Ethereum mainnet.") }, - async ({ to, value, data, network = "ethereum" }) => { + async ({ blockIdentifier, network = "ethereum" }) => { try { - const params: any = { to: to as Address }; - - if (value) { - params.value = services.helpers.parseEther(value); + let block; + if (blockIdentifier.startsWith("0x") && blockIdentifier.length === 66) { + // It's a hash + block = await services.getBlockByHash(blockIdentifier as Hash, network); + } else { + // It's a number + block = await services.getBlockByNumber(parseInt(blockIdentifier), network); } - - if (data) { - params.data = data as `0x${string}`; - } - - const gas = await services.estimateGas(params, network); - - return { - content: [{ - type: "text", - text: JSON.stringify({ - network, - estimatedGas: gas.toString() - }, null, 2) - }] - }; + return { content: [{ type: "text", text: services.helpers.formatJson(block) }] }; } catch (error) { return { - content: [{ - type: "text", - text: `Error estimating gas: ${error instanceof Error ? error.message : String(error)}` - }], + content: [{ type: "text", text: `Error fetching block: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } } ); - // TRANSFER TOOLS - - // Transfer ETH - server.tool( - "transfer_eth", - "Transfer native tokens (ETH, MATIC, etc.) to an address", + server.registerTool( + "get_latest_block", { - privateKey: z.string().describe("Private key of the sender account in hex format (with or without 0x prefix). SECURITY: This is used only for transaction signing and is not stored."), - to: z.string().describe("The recipient address or ENS name (e.g., '0x1234...' or 'vitalik.eth')"), - amount: z.string().describe("Amount to send in ETH (or the native token of the network), as a string (e.g., '0.1')"), - network: z.string().optional().describe("Network name (e.g., 'ethereum', 'optimism', 'arbitrum', 'base', etc.) or chain ID. Supports all EVM-compatible networks. Defaults to Ethereum mainnet.") + description: "Get the latest block from the network", + inputSchema: { + network: z.string().optional().describe("Network name or chain ID. Defaults to Ethereum mainnet.") + }, + annotations: { + title: "Get Latest Block", + readOnlyHint: true, + destructiveHint: false, + idempotentHint: false, + openWorldHint: true + } }, - async ({ privateKey, to, amount, network = "ethereum" }) => { + async ({ network = "ethereum" }) => { try { - const txHash = await services.transferETH(privateKey, to, amount, network); - - return { - content: [{ - type: "text", - text: JSON.stringify({ - success: true, - txHash, - to, - amount, - network - }, null, 2) - }] - }; + const block = await services.getLatestBlock(network); + return { content: [{ type: "text", text: services.helpers.formatJson(block) }] }; } catch (error) { return { - content: [{ - type: "text", - text: `Error transferring ETH: ${error instanceof Error ? error.message : String(error)}` - }], + content: [{ type: "text", text: `Error fetching latest block: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } } ); - // Transfer ERC20 - server.tool( - "transfer_erc20", - "Transfer ERC20 tokens to another address", + // ============================================================================ + // BALANCE TOOLS (Read-only) + // ============================================================================ + + server.registerTool( + "get_balance", { - privateKey: z.string().describe("Private key of the sending account (this is used for signing and is never stored)"), - tokenAddress: z.string().describe("The address of the ERC20 token contract"), - toAddress: z.string().describe("The recipient address"), - amount: z.string().describe("The amount of tokens to send (in token units, e.g., '10' for 10 tokens)"), - network: z.string().optional().describe("Network name (e.g., 'ethereum', 'optimism', 'arbitrum', 'base', etc.) or chain ID. Supports all EVM-compatible networks. Defaults to Ethereum mainnet.") + description: "Get the native token balance (ETH, MATIC, etc.) for an address", + inputSchema: { + address: z.string().describe("The wallet address or ENS name"), + network: z.string().optional().describe("Network name or chain ID. Defaults to Ethereum mainnet.") + }, + annotations: { + title: "Get Native Token Balance", + readOnlyHint: true, + destructiveHint: false, + idempotentHint: true, + openWorldHint: true + } }, - async ({ privateKey, tokenAddress, toAddress, amount, network = "ethereum" }) => { + async ({ address, network = "ethereum" }) => { try { - // Get the formattedKey with 0x prefix - const formattedKey = privateKey.startsWith('0x') - ? privateKey as `0x${string}` - : `0x${privateKey}` as `0x${string}`; - - const result = await services.transferERC20( - tokenAddress as Address, - toAddress as Address, - amount, - formattedKey, - network - ); - + const balance = await services.getETHBalance(address as Address, network); return { content: [{ type: "text", text: JSON.stringify({ - success: true, - txHash: result.txHash, network, - tokenAddress, - recipient: toAddress, - amount: result.amount.formatted, - symbol: result.token.symbol + address, + balance: { wei: balance.wei.toString(), ether: balance.ether } }, null, 2) }] }; } catch (error) { return { - content: [{ - type: "text", - text: `Error transferring ERC20 tokens: ${error instanceof Error ? error.message : String(error)}` - }], + content: [{ type: "text", text: `Error fetching balance: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } } ); - // Approve ERC20 token spending - server.tool( - "approve_token_spending", - "Approve another address (like a DeFi protocol or exchange) to spend your ERC20 tokens. This is often required before interacting with DeFi protocols.", + server.registerTool( + "get_token_balance", { - privateKey: z.string().describe("Private key of the token owner account in hex format (with or without 0x prefix). SECURITY: This is used only for transaction signing and is not stored."), - tokenAddress: z.string().describe("The contract address of the ERC20 token to approve for spending (e.g., '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48' for USDC on Ethereum)"), - spenderAddress: z.string().describe("The contract address being approved to spend your tokens (e.g., a DEX or lending protocol)"), - amount: z.string().describe("The amount of tokens to approve in token units, not wei (e.g., '1000' to approve spending 1000 tokens). Use a very large number for unlimited approval."), - network: z.string().optional().describe("Network name (e.g., 'ethereum', 'optimism', 'arbitrum', 'base', 'polygon') or chain ID. Defaults to Ethereum mainnet.") + description: "Get the ERC20 token balance for an address", + inputSchema: { + address: z.string().describe("The wallet address or ENS name"), + tokenAddress: z.string().describe("The ERC20 token contract address"), + network: z.string().optional().describe("Network name or chain ID. Defaults to Ethereum mainnet.") + }, + annotations: { + title: "Get ERC20 Token Balance", + readOnlyHint: true, + destructiveHint: false, + idempotentHint: true, + openWorldHint: true + } }, - async ({ privateKey, tokenAddress, spenderAddress, amount, network = "ethereum" }) => { + async ({ address, tokenAddress, network = "ethereum" }) => { try { - // Get the formattedKey with 0x prefix - const formattedKey = privateKey.startsWith('0x') - ? privateKey as `0x${string}` - : `0x${privateKey}` as `0x${string}`; - - const result = await services.approveERC20( - tokenAddress as Address, - spenderAddress as Address, - amount, - formattedKey, - network - ); - + const balance = await services.getERC20Balance(tokenAddress as Address, address as Address, network); return { content: [{ type: "text", text: JSON.stringify({ - success: true, - txHash: result.txHash, network, tokenAddress, - spender: spenderAddress, - amount: result.amount.formatted, - symbol: result.token.symbol + address, + balance: { + raw: balance.raw.toString(), + formatted: balance.formatted, + symbol: balance.token.symbol, + decimals: balance.token.decimals + } }, null, 2) }] }; } catch (error) { return { - content: [{ - type: "text", - text: `Error approving token spending: ${error instanceof Error ? error.message : String(error)}` - }], + content: [{ type: "text", text: `Error fetching token balance: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } } ); - // Transfer NFT (ERC721) - server.tool( - "transfer_nft", - "Transfer an NFT (ERC721 token) from one address to another. Requires the private key of the current owner for signing the transaction.", + server.registerTool( + "get_allowance", { - privateKey: z.string().describe("Private key of the NFT owner account in hex format (with or without 0x prefix). SECURITY: This is used only for transaction signing and is not stored."), - tokenAddress: z.string().describe("The contract address of the NFT collection (e.g., '0xBC4CA0EdA7647A8aB7C2061c2E118A18a936f13D' for Bored Ape Yacht Club)"), - tokenId: z.string().describe("The ID of the specific NFT to transfer (e.g., '1234')"), - toAddress: z.string().describe("The recipient wallet address that will receive the NFT"), - network: z.string().optional().describe("Network name (e.g., 'ethereum', 'optimism', 'arbitrum', 'base', 'polygon') or chain ID. Most NFTs are on Ethereum mainnet, which is the default.") - }, - async ({ privateKey, tokenAddress, tokenId, toAddress, network = "ethereum" }) => { - try { - // Get the formattedKey with 0x prefix - const formattedKey = privateKey.startsWith('0x') - ? privateKey as `0x${string}` - : `0x${privateKey}` as `0x${string}`; - - const result = await services.transferERC721( - tokenAddress as Address, - toAddress as Address, - BigInt(tokenId), - formattedKey, - network - ); - - return { - content: [{ - type: "text", - text: JSON.stringify({ - success: true, - txHash: result.txHash, - network, - collection: tokenAddress, - tokenId: result.tokenId, - recipient: toAddress, - name: result.token.name, - symbol: result.token.symbol - }, null, 2) - }] - }; - } catch (error) { - return { - content: [{ - type: "text", - text: `Error transferring NFT: ${error instanceof Error ? error.message : String(error)}` - }], - isError: true - }; + description: "Check the allowance granted to a spender for a token. This tells you how much of a token an address can spend on your behalf.", + inputSchema: { + tokenAddress: z.string().describe("The ERC20 token contract address"), + spenderAddress: z.string().describe("The address allowed to spend the token (usually a contract address)"), + ownerAddress: z.string().optional().describe("The owner address (defaults to the configured wallet)"), + network: z.string().optional().describe("Network name or chain ID. Defaults to Ethereum mainnet.") + }, + annotations: { + title: "Get Token Allowance", + readOnlyHint: true, + destructiveHint: false, + idempotentHint: true, + openWorldHint: true } - } - ); - - // Transfer ERC1155 token - server.tool( - "transfer_erc1155", - "Transfer ERC1155 tokens to another address. ERC1155 is a multi-token standard that can represent both fungible and non-fungible tokens in a single contract.", - { - privateKey: z.string().describe("Private key of the token owner account in hex format (with or without 0x prefix). SECURITY: This is used only for transaction signing and is not stored."), - tokenAddress: z.string().describe("The contract address of the ERC1155 token collection (e.g., '0x76BE3b62873462d2142405439777e971754E8E77')"), - tokenId: z.string().describe("The ID of the specific token to transfer (e.g., '1234')"), - amount: z.string().describe("The quantity of tokens to send (e.g., '1' for a single NFT or '10' for 10 fungible tokens)"), - toAddress: z.string().describe("The recipient wallet address that will receive the tokens"), - network: z.string().optional().describe("Network name (e.g., 'ethereum', 'optimism', 'arbitrum', 'base', 'polygon') or chain ID. ERC1155 tokens exist across many networks. Defaults to Ethereum mainnet.") }, - async ({ privateKey, tokenAddress, tokenId, amount, toAddress, network = "ethereum" }) => { + async ({ tokenAddress, spenderAddress, ownerAddress, network = "ethereum" }) => { try { - // Get the formattedKey with 0x prefix - const formattedKey = privateKey.startsWith('0x') - ? privateKey as `0x${string}` - : `0x${privateKey}` as `0x${string}`; - - const result = await services.transferERC1155( - tokenAddress as Address, - toAddress as Address, - BigInt(tokenId), - amount, - formattedKey, - network - ); - - return { - content: [{ - type: "text", - text: JSON.stringify({ - success: true, - txHash: result.txHash, - network, - contract: tokenAddress, - tokenId: result.tokenId, - amount: result.amount, - recipient: toAddress - }, null, 2) - }] - }; - } catch (error) { - return { - content: [{ - type: "text", - text: `Error transferring ERC1155 tokens: ${error instanceof Error ? error.message : String(error)}` - }], - isError: true - }; - } - } - ); + const owner = ownerAddress ? (ownerAddress as Address) : getConfiguredWallet().address; + const client = await services.getPublicClient(network); + + const allowance = await client.readContract({ + address: tokenAddress as Address, + abi: [ + { + name: 'allowance', + type: 'function', + inputs: [ + { name: 'owner', type: 'address' }, + { name: 'spender', type: 'address' } + ], + outputs: [{ name: '', type: 'uint256' }], + stateMutability: 'view' + } + ], + functionName: 'allowance', + args: [owner, spenderAddress as Address] + }); - // Transfer ERC20 tokens - server.tool( - "transfer_token", - "Transfer ERC20 tokens to an address", - { - privateKey: z.string().describe("Private key of the sender account in hex format (with or without 0x prefix). SECURITY: This is used only for transaction signing and is not stored."), - tokenAddress: z.string().describe("The contract address or ENS name of the ERC20 token to transfer (e.g., '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48' for USDC or 'uniswap.eth')"), - toAddress: z.string().describe("The recipient address or ENS name that will receive the tokens (e.g., '0x1234...' or 'vitalik.eth')"), - amount: z.string().describe("Amount of tokens to send as a string (e.g., '100' for 100 tokens). This will be adjusted for the token's decimals."), - network: z.string().optional().describe("Network name (e.g., 'ethereum', 'optimism', 'arbitrum', 'base', etc.) or chain ID. Supports all EVM-compatible networks. Defaults to Ethereum mainnet.") - }, - async ({ privateKey, tokenAddress, toAddress, amount, network = "ethereum" }) => { - try { - const result = await services.transferERC20( - tokenAddress, - toAddress, - amount, - privateKey, - network - ); - return { content: [{ type: "text", text: JSON.stringify({ - success: true, - txHash: result.txHash, + network, tokenAddress, - toAddress, - amount: result.amount.formatted, - symbol: result.token.symbol, - network + owner, + spenderAddress, + allowance: allowance.toString(), + message: allowance === 0n ? "No allowance set" : "Allowance is set" }, null, 2) }] }; } catch (error) { return { - content: [{ - type: "text", - text: `Error transferring tokens: ${error instanceof Error ? error.message : String(error)}` - }], + content: [{ type: "text", text: `Error fetching allowance: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } } ); - // CONTRACT TOOLS - - // Read contract - server.tool( - "read_contract", - "Read data from a smart contract by calling a view/pure function. This doesn't modify blockchain state and doesn't require gas or signing.", + // ============================================================================ + // TRANSACTION TOOLS (Read-only) + // ============================================================================ + + server.registerTool( + "get_transaction", { - contractAddress: z.string().describe("The address of the smart contract to interact with"), - abi: z.array(z.any()).describe("The ABI (Application Binary Interface) of the smart contract function, as a JSON array"), - functionName: z.string().describe("The name of the function to call on the contract (e.g., 'balanceOf')"), - args: z.array(z.any()).optional().describe("The arguments to pass to the function, as an array (e.g., ['0x1234...'])"), - network: z.string().optional().describe("Network name (e.g., 'ethereum', 'optimism', 'arbitrum', 'base', 'polygon') or chain ID. Defaults to Ethereum mainnet.") + description: "Get transaction details by transaction hash", + inputSchema: { + txHash: z.string().describe("Transaction hash (0x...)"), + network: z.string().optional().describe("Network name or chain ID. Defaults to Ethereum mainnet.") + }, + annotations: { + title: "Get Transaction", + readOnlyHint: true, + destructiveHint: false, + idempotentHint: true, + openWorldHint: true + } }, - async ({ contractAddress, abi, functionName, args = [], network = "ethereum" }) => { + async ({ txHash, network = "ethereum" }) => { try { - // Parse ABI if it's a string - const parsedAbi = typeof abi === 'string' ? JSON.parse(abi) : abi; - - const params = { - address: contractAddress as Address, - abi: parsedAbi, - functionName, - args - }; - - const result = await services.readContract(params, network); - - return { - content: [{ - type: "text", - text: services.helpers.formatJson(result) - }] - }; + const tx = await services.getTransaction(txHash as Hash, network); + return { content: [{ type: "text", text: services.helpers.formatJson(tx) }] }; } catch (error) { return { - content: [{ - type: "text", - text: `Error reading contract: ${error instanceof Error ? error.message : String(error)}` - }], + content: [{ type: "text", text: `Error fetching transaction: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } } ); - // Write to contract - server.tool( - "write_contract", - "Write data to a smart contract by calling a state-changing function. This modifies blockchain state and requires gas payment and transaction signing.", + server.registerTool( + "get_transaction_receipt", { - contractAddress: z.string().describe("The address of the smart contract to interact with"), - abi: z.array(z.any()).describe("The ABI (Application Binary Interface) of the smart contract function, as a JSON array"), - functionName: z.string().describe("The name of the function to call on the contract (e.g., 'transfer')"), - args: z.array(z.any()).describe("The arguments to pass to the function, as an array (e.g., ['0x1234...', '1000000000000000000'])"), - privateKey: z.string().describe("Private key of the sending account in hex format (with or without 0x prefix). SECURITY: This is used only for transaction signing and is not stored."), - network: z.string().optional().describe("Network name (e.g., 'ethereum', 'optimism', 'arbitrum', 'base', 'polygon') or chain ID. Defaults to Ethereum mainnet.") + description: "Get transaction receipt (confirmation status, gas used, logs). Use this to check if a transaction has been confirmed.", + inputSchema: { + txHash: z.string().describe("Transaction hash (0x...)"), + network: z.string().optional().describe("Network name or chain ID. Defaults to Ethereum mainnet.") + }, + annotations: { + title: "Get Transaction Receipt", + readOnlyHint: true, + destructiveHint: false, + idempotentHint: true, + openWorldHint: true + } }, - async ({ contractAddress, abi, functionName, args, privateKey, network = "ethereum" }) => { + async ({ txHash, network = "ethereum" }) => { try { - // Parse ABI if it's a string - const parsedAbi = typeof abi === 'string' ? JSON.parse(abi) : abi; - - const contractParams: Record = { - address: contractAddress as Address, - abi: parsedAbi, - functionName, - args - }; - - const txHash = await services.writeContract( - privateKey as Hex, - contractParams, - network - ); - - return { - content: [{ - type: "text", - text: JSON.stringify({ - network, - transactionHash: txHash, - message: "Contract write transaction sent successfully" - }, null, 2) - }] - }; + const client = await services.getPublicClient(network); + const receipt = await client.getTransactionReceipt({ + hash: txHash as Hash + }); + return { content: [{ type: "text", text: services.helpers.formatJson(receipt) }] }; } catch (error) { return { - content: [{ - type: "text", - text: `Error writing to contract: ${error instanceof Error ? error.message : String(error)}` - }], + content: [{ type: "text", text: `Error fetching transaction receipt: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } } ); - // Check if address is a contract - server.tool( - "is_contract", - "Check if an address is a smart contract or an externally owned account (EOA)", + server.registerTool( + "wait_for_transaction", { - address: z.string().describe("The wallet or contract address or ENS name to check (e.g., '0x1234...' or 'uniswap.eth')"), - network: z.string().optional().describe("Network name (e.g., 'ethereum', 'optimism', 'arbitrum', 'base', etc.) or chain ID. Supports all EVM-compatible networks. Defaults to Ethereum mainnet.") + description: "Wait for a transaction to be confirmed (mined). Polls the network until confirmation.", + inputSchema: { + txHash: z.string().describe("Transaction hash (0x...)"), + confirmations: z.number().optional().describe("Number of block confirmations required. Defaults to 1."), + network: z.string().optional().describe("Network name or chain ID. Defaults to Ethereum mainnet.") + }, + annotations: { + title: "Wait For Transaction", + readOnlyHint: true, + destructiveHint: false, + idempotentHint: false, + openWorldHint: true + } }, - async ({ address, network = "ethereum" }) => { + async ({ txHash, confirmations = 1, network = "ethereum" }) => { try { - const isContract = await services.isContract(address, network); - + const client = await services.getPublicClient(network); + const receipt = await client.waitForTransactionReceipt({ + hash: txHash as Hash, + confirmations + }); + return { content: [{ type: "text", text: JSON.stringify({ - address, network, - isContract, - type: isContract ? "Contract" : "Externally Owned Account (EOA)" + txHash, + status: receipt.status === 'success' ? 'confirmed' : 'failed', + blockNumber: receipt.blockNumber.toString(), + gasUsed: receipt.gasUsed.toString(), + confirmations }, null, 2) }] }; } catch (error) { return { - content: [{ - type: "text", - text: `Error checking if address is a contract: ${error instanceof Error ? error.message : String(error)}` - }], + content: [{ type: "text", text: `Error waiting for transaction: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } } ); - // Get ERC20 token information - server.tool( - "get_token_info", - "Get comprehensive information about an ERC20 token including name, symbol, decimals, total supply, and other metadata. Use this to analyze any token on EVM chains.", + // ============================================================================ + // SMART CONTRACT TOOLS + // ============================================================================ + + server.registerTool( + "get_contract_abi", { - tokenAddress: z.string().describe("The contract address of the ERC20 token (e.g., '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48' for USDC on Ethereum)"), - network: z.string().optional().describe("Network name (e.g., 'ethereum', 'optimism', 'arbitrum', 'base', 'polygon') or chain ID. Defaults to Ethereum mainnet.") + description: "Fetch a contract's full ABI from Etherscan/block explorers. Use this to understand verified contracts before interacting. Requires ETHERSCAN_API_KEY. Supports 30+ EVM networks. Works best with verified contracts on block explorers.", + inputSchema: { + contractAddress: z.string().describe("The contract address (0x...)"), + network: z.string().optional().describe("Network name or chain ID. Defaults to ethereum. Supported: ethereum, polygon, arbitrum, optimism, base, avalanche, gnosis, fantom, bsc, celo, scroll, linea, zksync, manta, blast, and testnets (sepolia, mumbai, arbitrum-sepolia, optimism-sepolia, base-sepolia, avalanche-fuji)") + }, + annotations: { + title: "Get Contract ABI", + readOnlyHint: true, + destructiveHint: false, + idempotentHint: true, + openWorldHint: true + } }, - async ({ tokenAddress, network = "ethereum" }) => { + async ({ contractAddress, network = "ethereum" }) => { try { - const tokenInfo = await services.getERC20TokenInfo(tokenAddress as Address, network); - + const abi = await services.fetchContractABI(contractAddress as Address, network); + const parsed = services.parseABI(abi); + const readableFunctions = services.getReadableFunctions(parsed); + return { content: [{ type: "text", text: JSON.stringify({ - address: tokenAddress, + contractAddress, network, - ...tokenInfo + abiFormat: "json", + readableFunctions, + totalFunctions: parsed.filter(i => i.type === 'function').length, + abi: parsed }, null, 2) }] }; } catch (error) { return { - content: [{ - type: "text", - text: `Error fetching token info: ${error instanceof Error ? error.message : String(error)}` - }], + content: [{ type: "text", text: `Error fetching ABI: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } } ); - // Get ERC20 token balance - server.tool( - "get_token_balance_erc20", - "Get ERC20 token balance for an address", + server.registerTool( + "read_contract", { - address: z.string().describe("The address to check balance for"), - tokenAddress: z.string().describe("The ERC20 token contract address"), - network: z.string().optional().describe("Network name or chain ID. Defaults to Ethereum mainnet.") + description: "Call read-only functions on a smart contract. Automatically fetches ABI from block explorer if not provided (requires ETHERSCAN_API_KEY). Falls back to common functions if contract is not verified. Use this to query contract state and data.", + inputSchema: { + contractAddress: z.string().describe("The contract address"), + functionName: z.string().describe("Function name (e.g., 'name', 'symbol', 'balanceOf', 'totalSupply', 'owner')"), + args: z.array(z.string()).optional().describe("Function arguments as strings (e.g., ['0xAddress'] for balanceOf)"), + abiJson: z.string().optional().describe("Full contract ABI as JSON string (optional - will auto-fetch verified contract ABI if not provided)"), + network: z.string().optional().describe("Network name or chain ID. Defaults to Ethereum mainnet.") + }, + annotations: { + title: "Read Smart Contract", + readOnlyHint: true, + destructiveHint: false, + idempotentHint: true, + openWorldHint: true + } }, - async ({ address, tokenAddress, network = "ethereum" }) => { + async ({ contractAddress, functionName, args = [], abiJson, network = "ethereum" }) => { try { - const balance = await services.getERC20Balance( - tokenAddress as Address, - address as Address, - network - ); - + const client = await services.getPublicClient(network); + + let abi: any[] | undefined; + let functionAbi: any; + + // If ABI is provided, use it + if (abiJson) { + try { + abi = services.parseABI(abiJson); + functionAbi = services.getFunctionFromABI(abi, functionName); + } catch (error) { + return { + content: [{ + type: "text", + text: `Error parsing provided ABI: ${error instanceof Error ? error.message : String(error)}` + }], + isError: true + }; + } + } else { + // Try to auto-fetch ABI from block explorer + try { + const fetchedAbi = await services.fetchContractABI(contractAddress as Address, network); + abi = services.parseABI(fetchedAbi); + functionAbi = services.getFunctionFromABI(abi, functionName); + } catch (fetchError) { + // Fall back to common function signatures + const commonFunctions: { [key: string]: any } = { + 'name': { inputs: [], outputs: [{ type: 'string' }] }, + 'symbol': { inputs: [], outputs: [{ type: 'string' }] }, + 'decimals': { inputs: [], outputs: [{ type: 'uint8' }] }, + 'totalSupply': { inputs: [], outputs: [{ type: 'uint256' }] }, + 'balanceOf': { inputs: [{ type: 'address' }], outputs: [{ type: 'uint256' }] }, + 'allowance': { inputs: [{ type: 'address' }, { type: 'address' }], outputs: [{ type: 'uint256' }] }, + }; + + if (!commonFunctions[functionName]) { + return { + content: [{ + type: "text", + text: `Error: Could not auto-fetch ABI (${fetchError instanceof Error ? fetchError.message : String(fetchError)}). Function '${functionName}' not in common signatures. Use get_contract_abi to fetch and provide the full ABI, or provide abiJson parameter.` + }], + isError: true + }; + } + + functionAbi = { + name: functionName, + type: 'function', + inputs: commonFunctions[functionName].inputs, + outputs: commonFunctions[functionName].outputs, + stateMutability: 'view' + }; + } + } + + const result = await client.readContract({ + address: contractAddress as Address, + abi: [functionAbi], + functionName: functionName, + args: args as any + }); + return { content: [{ type: "text", text: JSON.stringify({ - address, - tokenAddress, - network, - balance: { - raw: balance.raw.toString(), - formatted: balance.formatted, - decimals: balance.token.decimals - } + contractAddress, + function: functionName, + args: args.length > 0 ? args : undefined, + result: result?.toString(), + abiSource: abiJson ? 'provided' : 'auto-fetched or built-in' }, null, 2) }] }; } catch (error) { return { - content: [{ - type: "text", - text: `Error fetching ERC20 balance for ${address}: ${error instanceof Error ? error.message : String(error)}` - }], + content: [{ type: "text", text: `Error reading contract: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } } ); - // Get NFT (ERC721) information - server.tool( - "get_nft_info", - "Get detailed information about a specific NFT (ERC721 token), including collection name, symbol, token URI, and current owner if available.", + server.registerTool( + "write_contract", { - tokenAddress: z.string().describe("The contract address of the NFT collection (e.g., '0xBC4CA0EdA7647A8aB7C2061c2E118A18a936f13D' for Bored Ape Yacht Club)"), - tokenId: z.string().describe("The ID of the specific NFT token to query (e.g., '1234')"), - network: z.string().optional().describe("Network name (e.g., 'ethereum', 'optimism', 'arbitrum', 'base', 'polygon') or chain ID. Most NFTs are on Ethereum mainnet, which is the default.") + description: "Execute state-changing functions on a smart contract. Automatically fetches ABI from block explorer if not provided (requires ETHERSCAN_API_KEY). Use this to call any write function on verified contracts. Requires wallet to be configured (via private key or mnemonic).", + inputSchema: { + contractAddress: z.string().describe("The contract address"), + functionName: z.string().describe("Function name to call (e.g., 'mint', 'swap', 'stake', 'approve')"), + args: z.array(z.string()).optional().describe("Function arguments as strings (e.g., ['0xAddress', '1000000'])"), + value: z.string().optional().describe("ETH value to send with transaction in ether (e.g., '0.1' for payable functions)"), + abiJson: z.string().optional().describe("Full contract ABI as JSON string (optional - will auto-fetch verified contract ABI if not provided)"), + network: z.string().optional().describe("Network name or chain ID. Defaults to Ethereum mainnet.") + }, + annotations: { + title: "Write to Smart Contract", + readOnlyHint: false, + destructiveHint: true, + idempotentHint: false, + openWorldHint: true + } }, - async ({ tokenAddress, tokenId, network = "ethereum" }) => { + async ({ contractAddress, functionName, args = [], value, abiJson, network = "ethereum" }) => { try { - const nftInfo = await services.getERC721TokenMetadata( - tokenAddress as Address, - BigInt(tokenId), - network - ); - - // Check ownership separately - let owner = null; - try { - // This may fail if tokenId doesn't exist - owner = await services.getPublicClient(network).readContract({ - address: tokenAddress as Address, - abi: [{ - inputs: [{ type: 'uint256' }], - name: 'ownerOf', - outputs: [{ type: 'address' }], - stateMutability: 'view', - type: 'function' + const privateKey = getConfiguredPrivateKey(); + const senderAddress = getWalletAddressFromKey(); + const client = await services.getPublicClient(network); + + let abi: any[] | undefined; + let functionAbi: any; + + // If ABI is provided, use it + if (abiJson) { + try { + abi = services.parseABI(abiJson); + functionAbi = services.getFunctionFromABI(abi, functionName); + } catch (error) { + return { + content: [{ + type: "text", + text: `Error parsing provided ABI: ${error instanceof Error ? error.message : String(error)}` + }], + isError: true + }; + } + } else { + // Try to auto-fetch ABI from block explorer + try { + const fetchedAbi = await services.fetchContractABI(contractAddress as Address, network); + abi = services.parseABI(fetchedAbi); + functionAbi = services.getFunctionFromABI(abi, functionName); + } catch (fetchError) { + return { + content: [{ + type: "text", + text: `Error: Could not auto-fetch ABI (${fetchError instanceof Error ? fetchError.message : String(fetchError)}). Please provide the contract ABI using the abiJson parameter, or use get_contract_abi to fetch it first.` + }], + isError: true + }; + } + } + + // Validate that this is not a view/pure function + if (functionAbi.stateMutability === 'view' || functionAbi.stateMutability === 'pure') { + return { + content: [{ + type: "text", + text: `Error: Function '${functionName}' is a ${functionAbi.stateMutability} function and cannot modify state. Use read_contract instead.` }], - functionName: 'ownerOf', - args: [BigInt(tokenId)] - }); - } catch (e) { - // Ownership info not available + isError: true + }; + } + + // Prepare write parameters + const writeParams: any = { + address: contractAddress as Address, + abi: [functionAbi], + functionName: functionName, + args: args as any + }; + + // Add value if provided (for payable functions) + if (value) { + const { parseEther } = await import('viem'); + writeParams.value = parseEther(value); } - + + // Execute the write operation + const txHash = await services.writeContract(privateKey, writeParams, network); + return { content: [{ type: "text", text: JSON.stringify({ - contract: tokenAddress, - tokenId, network, - ...nftInfo, - owner: owner || 'Unknown' + contractAddress, + function: functionName, + args: args.length > 0 ? args : undefined, + value: value || undefined, + from: senderAddress, + txHash, + abiSource: abiJson ? 'provided' : 'auto-fetched', + message: "Transaction sent. Use get_transaction_receipt or wait_for_transaction to check confirmation." }, null, 2) }] }; } catch (error) { return { - content: [{ - type: "text", - text: `Error fetching NFT info: ${error instanceof Error ? error.message : String(error)}` - }], + content: [{ type: "text", text: `Error writing to contract: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } } ); - // Check NFT ownership - server.tool( - "check_nft_ownership", - "Check if an address owns a specific NFT", + // ============================================================================ + // TRANSFER TOOLS (Write operations) + // ============================================================================ + + server.registerTool( + "transfer_native", { - tokenAddress: z.string().describe("The contract address or ENS name of the NFT collection (e.g., '0xBC4CA0EdA7647A8aB7C2061c2E118A18a936f13D' for BAYC or 'boredapeyachtclub.eth')"), - tokenId: z.string().describe("The ID of the NFT to check (e.g., '1234')"), - ownerAddress: z.string().describe("The wallet address or ENS name to check ownership against (e.g., '0x1234...' or 'vitalik.eth')"), - network: z.string().optional().describe("Network name (e.g., 'ethereum', 'optimism', 'arbitrum', 'base', etc.) or chain ID. Supports all EVM-compatible networks. Defaults to Ethereum mainnet.") + description: "Transfer native tokens (ETH, MATIC, etc.) to an address. Uses the configured wallet.", + inputSchema: { + to: z.string().describe("Recipient address or ENS name"), + amount: z.string().describe("Amount to send in ether (e.g., '0.5' for 0.5 ETH)"), + network: z.string().optional().describe("Network name or chain ID. Defaults to Ethereum mainnet.") + }, + annotations: { + title: "Transfer Native Tokens", + readOnlyHint: false, + destructiveHint: true, + idempotentHint: false, + openWorldHint: true + } }, - async ({ tokenAddress, tokenId, ownerAddress, network = "ethereum" }) => { + async ({ to, amount, network = "ethereum" }) => { try { - const isOwner = await services.isNFTOwner( - tokenAddress, - ownerAddress, - BigInt(tokenId), - network - ); - + const privateKey = getConfiguredPrivateKey(); + const senderAddress = getWalletAddressFromKey(); + const txHash = await services.transferETH(privateKey, to as Address, amount, network); return { content: [{ type: "text", text: JSON.stringify({ - tokenAddress, - tokenId, - ownerAddress, network, - isOwner, - result: isOwner ? "Address owns this NFT" : "Address does not own this NFT" + from: senderAddress, + to, + amount, + txHash, + message: "Transaction sent. Use get_transaction_receipt to check confirmation." }, null, 2) }] }; } catch (error) { return { - content: [{ - type: "text", - text: `Error checking NFT ownership: ${error instanceof Error ? error.message : String(error)}` - }], + content: [{ type: "text", text: `Error transferring native tokens: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } } ); - // Add tool for getting ERC1155 token URI - server.tool( - "get_erc1155_token_uri", - "Get the metadata URI for an ERC1155 token (multi-token standard used for both fungible and non-fungible tokens). The URI typically points to JSON metadata about the token.", + server.registerTool( + "transfer_erc20", { - tokenAddress: z.string().describe("The contract address of the ERC1155 token collection (e.g., '0x76BE3b62873462d2142405439777e971754E8E77')"), - tokenId: z.string().describe("The ID of the specific token to query metadata for (e.g., '1234')"), - network: z.string().optional().describe("Network name (e.g., 'ethereum', 'optimism', 'arbitrum', 'base', 'polygon') or chain ID. ERC1155 tokens exist across many networks. Defaults to Ethereum mainnet.") + description: "Transfer ERC20 tokens to an address. Uses the configured wallet.", + inputSchema: { + tokenAddress: z.string().describe("The ERC20 token contract address"), + to: z.string().describe("Recipient address or ENS name"), + amount: z.string().describe("Amount to send (in token units, accounting for decimals)"), + network: z.string().optional().describe("Network name or chain ID. Defaults to Ethereum mainnet.") + }, + annotations: { + title: "Transfer ERC20 Tokens", + readOnlyHint: false, + destructiveHint: true, + idempotentHint: false, + openWorldHint: true + } }, - async ({ tokenAddress, tokenId, network = "ethereum" }) => { + async ({ tokenAddress, to, amount, network = "ethereum" }) => { try { - const uri = await services.getERC1155TokenURI( - tokenAddress as Address, - BigInt(tokenId), - network - ); - + const privateKey = getConfiguredPrivateKey(); + const senderAddress = getWalletAddressFromKey(); + const result = await services.transferERC20(tokenAddress as Address, to as Address, amount, privateKey, network); return { content: [{ type: "text", text: JSON.stringify({ - contract: tokenAddress, - tokenId, network, - uri + tokenAddress, + from: senderAddress, + to, + amount: result.amount.formatted, + symbol: result.token.symbol, + decimals: result.token.decimals, + txHash: result.txHash, + message: "Transaction sent. Use get_transaction_receipt to check confirmation." }, null, 2) }] }; } catch (error) { return { - content: [{ - type: "text", - text: `Error fetching ERC1155 token URI: ${error instanceof Error ? error.message : String(error)}` - }], + content: [{ type: "text", text: `Error transferring ERC20 tokens: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } } ); - // Add tool for getting ERC721 NFT balance - server.tool( - "get_nft_balance", - "Get the total number of NFTs owned by an address from a specific collection. This returns the count of NFTs, not individual token IDs.", + server.registerTool( + "approve_token_spending", { - tokenAddress: z.string().describe("The contract address of the NFT collection (e.g., '0xBC4CA0EdA7647A8aB7C2061c2E118A18a936f13D' for Bored Ape Yacht Club)"), - ownerAddress: z.string().describe("The wallet address to check the NFT balance for (e.g., '0x1234...')"), - network: z.string().optional().describe("Network name (e.g., 'ethereum', 'optimism', 'arbitrum', 'base', 'polygon') or chain ID. Most NFTs are on Ethereum mainnet, which is the default.") + description: "Approve a spender (contract) to spend tokens on your behalf. Required before interacting with DEXes, lending protocols, etc.", + inputSchema: { + tokenAddress: z.string().describe("The ERC20 token contract address"), + spenderAddress: z.string().describe("The address that will be allowed to spend tokens (usually a contract)"), + amount: z.string().describe("Amount to approve (in token units). Use '0' to revoke approval."), + network: z.string().optional().describe("Network name or chain ID. Defaults to Ethereum mainnet.") + }, + annotations: { + title: "Approve Token Spending", + readOnlyHint: false, + destructiveHint: false, + idempotentHint: false, + openWorldHint: true + } }, - async ({ tokenAddress, ownerAddress, network = "ethereum" }) => { + async ({ tokenAddress, spenderAddress, amount, network = "ethereum" }) => { try { - const balance = await services.getERC721Balance( - tokenAddress as Address, - ownerAddress as Address, - network - ); - + const privateKey = getConfiguredPrivateKey(); + const senderAddress = getWalletAddressFromKey(); + const txHash = await services.approveERC20(privateKey, tokenAddress as Address, spenderAddress as Address, amount, network); return { content: [{ type: "text", text: JSON.stringify({ - collection: tokenAddress, - owner: ownerAddress, network, - balance: balance.toString() + tokenAddress, + owner: senderAddress, + spender: spenderAddress, + approvalAmount: amount, + txHash, + message: "Approval transaction sent. Use get_transaction_receipt to check confirmation." }, null, 2) }] }; } catch (error) { return { - content: [{ - type: "text", - text: `Error fetching NFT balance: ${error instanceof Error ? error.message : String(error)}` - }], + content: [{ type: "text", text: `Error approving token spending: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } } ); - // Add tool for getting ERC1155 token balance - server.tool( - "get_erc1155_balance", - "Get the balance of a specific ERC1155 token ID owned by an address. ERC1155 allows multiple tokens of the same ID, so the balance can be greater than 1.", + // ============================================================================ + // NFT TOOLS (Read-only) + // ============================================================================ + + server.registerTool( + "get_nft_info", { - tokenAddress: z.string().describe("The contract address of the ERC1155 token collection (e.g., '0x76BE3b62873462d2142405439777e971754E8E77')"), - tokenId: z.string().describe("The ID of the specific token to check the balance for (e.g., '1234')"), - ownerAddress: z.string().describe("The wallet address to check the token balance for (e.g., '0x1234...')"), - network: z.string().optional().describe("Network name (e.g., 'ethereum', 'optimism', 'arbitrum', 'base', 'polygon') or chain ID. ERC1155 tokens exist across many networks. Defaults to Ethereum mainnet.") + description: "Get information about an ERC721 NFT including metadata URI", + inputSchema: { + contractAddress: z.string().describe("The NFT contract address"), + tokenId: z.string().describe("The NFT token ID"), + network: z.string().optional().describe("Network name or chain ID. Defaults to Ethereum mainnet.") + }, + annotations: { + title: "Get NFT Info", + readOnlyHint: true, + destructiveHint: false, + idempotentHint: true, + openWorldHint: true + } }, - async ({ tokenAddress, tokenId, ownerAddress, network = "ethereum" }) => { + async ({ contractAddress, tokenId, network = "ethereum" }) => { try { - const balance = await services.getERC1155Balance( - tokenAddress as Address, - ownerAddress as Address, - BigInt(tokenId), - network - ); - + const nftInfo = await services.getERC721TokenMetadata(contractAddress as Address, BigInt(tokenId), network); return { content: [{ type: "text", text: JSON.stringify({ - contract: tokenAddress, - tokenId, - owner: ownerAddress, network, - balance: balance.toString() + contract: contractAddress, + tokenId, + ...nftInfo }, null, 2) }] }; } catch (error) { return { - content: [{ - type: "text", - text: `Error fetching ERC1155 token balance: ${error instanceof Error ? error.message : String(error)}` - }], + content: [{ type: "text", text: `Error fetching NFT info: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } } ); - // WALLET TOOLS - - // Get address from private key - server.tool( - "get_address_from_private_key", - "Get the EVM address derived from a private key", + server.registerTool( + "get_erc1155_balance", { - privateKey: z.string().describe("Private key in hex format (with or without 0x prefix). SECURITY: This is used only for address derivation and is not stored.") + description: "Get ERC1155 token balance for an address", + inputSchema: { + contractAddress: z.string().describe("The ERC1155 contract address"), + tokenId: z.string().describe("The token ID"), + address: z.string().describe("The owner address or ENS name"), + network: z.string().optional().describe("Network name or chain ID. Defaults to Ethereum mainnet.") + }, + annotations: { + title: "Get ERC1155 Balance", + readOnlyHint: true, + destructiveHint: false, + idempotentHint: true, + openWorldHint: true + } }, - async ({ privateKey }) => { + async ({ contractAddress, tokenId, address, network = "ethereum" }) => { try { - // Ensure the private key has 0x prefix - const formattedKey = privateKey.startsWith('0x') ? privateKey as Hex : `0x${privateKey}` as Hex; - - const address = services.getAddressFromPrivateKey(formattedKey); - + const balance = await services.getERC1155Balance(contractAddress as Address, address as Address, BigInt(tokenId), network); return { content: [{ type: "text", text: JSON.stringify({ - address, - privateKey: "0x" + privateKey.replace(/^0x/, '') + network, + contract: contractAddress, + tokenId, + owner: address, + balance: balance.toString() }, null, 2) }] }; } catch (error) { return { - content: [{ - type: "text", - text: `Error deriving address from private key: ${error instanceof Error ? error.message : String(error)}` - }], + content: [{ type: "text", text: `Error fetching ERC1155 balance: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } } ); -} \ No newline at end of file +} diff --git a/src/server/http-server.ts b/src/server/http-server.ts index 216672c..cb20194 100644 --- a/src/server/http-server.ts +++ b/src/server/http-server.ts @@ -1,34 +1,43 @@ -import { config } from "dotenv"; -import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js"; +import { randomUUID } from "node:crypto"; +import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; import startServer from "./server.js"; import express, { Request, Response } from "express"; -import cors from "cors"; import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -// Environment variables - hardcoded values -const PORT = 3001; -const HOST = '0.0.0.0'; +// Environment variables +const PORT = parseInt(process.env.MCP_PORT || "3001", 10); +const HOST = process.env.MCP_HOST || "0.0.0.0"; console.error(`Configured to listen on ${HOST}:${PORT}`); // Setup Express const app = express(); -app.use(express.json()); -app.use(cors({ - origin: '*', - methods: ['GET', 'POST', 'OPTIONS'], - allowedHeaders: ['Content-Type', 'Authorization'], - credentials: true, - exposedHeaders: ['Content-Type', 'Access-Control-Allow-Origin'] -})); - -// Add OPTIONS handling for preflight requests -app.options('*', cors()); - -// Keep track of active connections with session IDs -const connections = new Map(); - -// Initialize the server +app.use(express.json({ limit: '10mb' })); // Prevent DoS attacks with huge payloads + +// Track active transports by session ID with cleanup +const transports = new Map(); +const sessionTimestamps = new Map(); +const SESSION_TIMEOUT_MS = 30 * 60 * 1000; // 30 minutes + +// Cleanup stale sessions periodically +setInterval(() => { + const now = Date.now(); + for (const [sessionId, timestamp] of sessionTimestamps.entries()) { + if (now - timestamp > SESSION_TIMEOUT_MS) { + console.error(`Cleaning up stale session: ${sessionId}`); + const transport = transports.get(sessionId); + if (transport) { + transport.close().catch(err => + console.error(`Error closing stale session ${sessionId}:`, err) + ); + } + transports.delete(sessionId); + sessionTimestamps.delete(sessionId); + } + } +}, 5 * 60 * 1000); // Check every 5 minutes + +// Initialize the MCP server let server: McpServer | null = null; startServer().then(s => { server = s; @@ -38,165 +47,174 @@ startServer().then(s => { process.exit(1); }); -// Define routes -// @ts-ignore -app.get("/sse", (req: Request, res: Response) => { - console.error(`Received SSE connection request from ${req.ip}`); - console.error(`Query parameters: ${JSON.stringify(req.query)}`); - - // Set CORS headers explicitly - res.setHeader('Access-Control-Allow-Origin', '*'); - res.setHeader('Access-Control-Allow-Methods', 'GET, OPTIONS'); - res.setHeader('Access-Control-Allow-Headers', 'Content-Type'); - +// Handle all MCP requests through POST /mcp +app.post("/mcp", async (req: Request, res: Response) => { + console.error(`Received POST /mcp request from ${req.ip}`); + if (!server) { - console.error("Server not initialized yet, rejecting SSE connection"); - return res.status(503).send("Server not initialized"); + console.error("Server not initialized yet"); + res.status(503).json({ error: "Server not initialized" }); + return; } - - // Generate a unique session ID if one is not provided - // The sessionId is crucial for mapping SSE connections to message handlers - const sessionId = generateSessionId(); - console.error(`Creating SSE session with ID: ${sessionId}`); - - // Set SSE headers - res.setHeader("Content-Type", "text/event-stream"); - res.setHeader("Cache-Control", "no-cache, no-transform"); - res.setHeader("Connection", "keep-alive"); - - // Create transport - handle before writing to response - try { - console.error(`Creating SSE transport for session: ${sessionId}`); - - // Create and store the transport keyed by session ID - // Note: The path must match what the client expects (typically "/messages") - const transport = new SSEServerTransport("/messages", res); - connections.set(sessionId, transport); - - // Handle connection close - req.on("close", () => { - console.error(`SSE connection closed for session: ${sessionId}`); - connections.delete(sessionId); - }); - - // Connect transport to server - this must happen before sending any data - server.connect(transport).then(() => { - // Send an initial event with the session ID for the client to use in messages - // Only send this after the connection is established - console.error(`SSE connection established for session: ${sessionId}`); - - // Send the session ID to the client - res.write(`data: ${JSON.stringify({ type: "session_init", sessionId })}\n\n`); - }).catch((error: Error) => { - console.error(`Error connecting transport to server: ${error}`); - connections.delete(sessionId); + + // Check for existing session + const sessionId = req.headers["mcp-session-id"] as string | undefined; + let transport: StreamableHTTPServerTransport; + + if (sessionId && transports.has(sessionId)) { + // Reuse existing transport for this session + transport = transports.get(sessionId)!; + sessionTimestamps.set(sessionId, Date.now()); // Update last activity + console.error(`Reusing transport for session: ${sessionId}`); + } else if (!sessionId) { + // New session - create transport with session ID generator + transport = new StreamableHTTPServerTransport({ + sessionIdGenerator: () => randomUUID(), + onsessioninitialized: (newSessionId) => { + console.error(`Session initialized: ${newSessionId}`); + transports.set(newSessionId, transport); + sessionTimestamps.set(newSessionId, Date.now()); + }, + onsessionclosed: (closedSessionId) => { + console.error(`Session closed: ${closedSessionId}`); + transports.delete(closedSessionId); + sessionTimestamps.delete(closedSessionId); + } }); + + // Connect the transport to the server + await server.connect(transport); + console.error("New transport connected to server"); + } else { + // Invalid session ID provided + console.error(`Invalid session ID: ${sessionId}`); + res.status(404).json({ error: "Session not found" }); + return; + } + + // Handle the request + try { + await transport.handleRequest(req, res, req.body); } catch (error) { - console.error(`Error creating SSE transport: ${error}`); - connections.delete(sessionId); - res.status(500).send(`Internal server error: ${error}`); + console.error(`Error handling request: ${error}`); + if (!res.headersSent) { + res.status(500).json({ error: `Internal server error: ${error}` }); + } } }); -// @ts-ignore -app.post("/messages", (req: Request, res: Response) => { - // Extract the session ID from the URL query parameters - let sessionId = req.query.sessionId?.toString(); - - // If no sessionId is provided and there's only one connection, use that - if (!sessionId && connections.size === 1) { - sessionId = Array.from(connections.keys())[0]; - console.error(`No sessionId provided, using the only active session: ${sessionId}`); - } - - console.error(`Received message for sessionId ${sessionId}`); - console.error(`Message body: ${JSON.stringify(req.body)}`); - - // Set CORS headers - res.setHeader('Access-Control-Allow-Origin', '*'); - res.setHeader('Access-Control-Allow-Methods', 'POST, OPTIONS'); - res.setHeader('Access-Control-Allow-Headers', 'Content-Type'); - +// Handle GET requests for SSE streams (server-to-client notifications) +app.get("/mcp", async (req: Request, res: Response) => { + console.error(`Received GET /mcp request from ${req.ip}`); + if (!server) { - console.error("Server not initialized yet"); - return res.status(503).json({ error: "Server not initialized" }); + res.status(503).json({ error: "Server not initialized" }); + return; } - - if (!sessionId) { - console.error("No session ID provided and multiple connections exist"); - return res.status(400).json({ - error: "No session ID provided. Please provide a sessionId query parameter or connect to /sse first.", - activeConnections: connections.size - }); + + const sessionId = req.headers["mcp-session-id"] as string | undefined; + + if (!sessionId || !transports.has(sessionId)) { + res.status(400).json({ error: "Invalid or missing session ID" }); + return; } - - const transport = connections.get(sessionId); - if (!transport) { - console.error(`Session not found: ${sessionId}`); - return res.status(404).json({ error: "Session not found" }); + + const transport = transports.get(sessionId)!; + + try { + await transport.handleRequest(req, res); + } catch (error) { + console.error(`Error handling SSE request: ${error}`); + if (!res.headersSent) { + res.status(500).json({ error: `Internal server error: ${error}` }); + } } - - console.error(`Handling message for session: ${sessionId}`); +}); + +// Handle DELETE requests to close sessions +app.delete("/mcp", async (req: Request, res: Response) => { + const sessionId = req.headers["mcp-session-id"] as string | undefined; + + if (!sessionId || !transports.has(sessionId)) { + res.status(404).json({ error: "Session not found" }); + return; + } + + const transport = transports.get(sessionId)!; + try { - transport.handlePostMessage(req, res).catch((error: Error) => { - console.error(`Error handling post message: ${error}`); - res.status(500).json({ error: `Internal server error: ${error.message}` }); - }); + await transport.handleRequest(req, res); } catch (error) { - console.error(`Exception handling post message: ${error}`); - res.status(500).json({ error: `Internal server error: ${error}` }); + console.error(`Error closing session: ${error}`); + if (!res.headersSent) { + res.status(500).json({ error: `Internal server error: ${error}` }); + } } }); -// Add a simple health check endpoint -app.get("/health", (req: Request, res: Response) => { - res.status(200).json({ +// Health check endpoint +app.get("/health", (_req: Request, res: Response) => { + res.status(200).json({ status: "ok", server: server ? "initialized" : "initializing", - activeConnections: connections.size, - connectedSessionIds: Array.from(connections.keys()) + activeSessions: transports.size, + sessionIds: Array.from(transports.keys()) }); }); -// Add a root endpoint for basic info -app.get("/", (req: Request, res: Response) => { +// Root endpoint for basic info +app.get("/", (_req: Request, res: Response) => { res.status(200).json({ - name: "MCP Server", - version: "1.0.0", + name: "EVM MCP Server", + version: "2.0.0", + protocol: "MCP 2025-06-18", + transport: "Streamable HTTP", endpoints: { - sse: "/sse", - messages: "/messages", + mcp: "/mcp", health: "/health" }, status: server ? "ready" : "initializing", - activeConnections: connections.size + activeSessions: transports.size }); }); -// Helper function to generate a UUID-like session ID -function generateSessionId(): string { - return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { - const r = Math.random() * 16 | 0; - const v = c === 'x' ? r : (r & 0x3 | 0x8); - return v.toString(16); - }); -} - // Handle process termination gracefully -process.on('SIGINT', () => { +process.on('SIGINT', async () => { console.error('Shutting down server...'); - connections.forEach((transport, sessionId) => { - console.error(`Closing connection for session: ${sessionId}`); - }); + + // Close all active transports + for (const [sessionId, transport] of transports) { + console.error(`Closing transport for session: ${sessionId}`); + await transport.close(); + } + transports.clear(); + process.exit(0); }); -// Start the HTTP server on a different port (3001) to avoid conflicts +process.on('SIGTERM', async () => { + console.error('Received SIGTERM, shutting down...'); + + for (const [sessionId, transport] of transports) { + console.error(`Closing transport for session: ${sessionId}`); + await transport.close(); + } + transports.clear(); + + process.exit(0); +}); + +// Start the HTTP server const httpServer = app.listen(PORT, HOST, () => { - console.error(`Template MCP Server running at http://${HOST}:${PORT}`); - console.error(`SSE endpoint: http://${HOST}:${PORT}/sse`); - console.error(`Messages endpoint: http://${HOST}:${PORT}/messages (sessionId optional if only one connection)`); + console.error(`EVM MCP Server running at http://${HOST}:${PORT}`); + console.error(`MCP endpoint: http://${HOST}:${PORT}/mcp`); console.error(`Health check: http://${HOST}:${PORT}/health`); + console.error(`Protocol: MCP 2025-06-18 (Streamable HTTP)`); }).on('error', (err: Error) => { console.error(`Server error: ${err}`); -}); \ No newline at end of file + process.exit(1); +}); + +// Set server timeout to prevent hanging connections +httpServer.timeout = 120000; // 2 minutes +httpServer.keepAliveTimeout = 65000; // 65 seconds diff --git a/src/server/server.ts b/src/server/server.ts index 81cf3a5..fe86134 100644 --- a/src/server/server.ts +++ b/src/server/server.ts @@ -7,22 +7,40 @@ import { getSupportedNetworks } from "../core/chains.js"; // Create and start the MCP server async function startServer() { try { - // Create a new MCP server instance - const server = new McpServer({ - name: "EVM-Server", - version: "1.0.0" - }); + // Create a new MCP server instance with capabilities + const server = new McpServer( + { + name: "evm-mcp-server", + version: "2.0.0" + }, + { + capabilities: { + tools: { + listChanged: true + }, + resources: { + subscribe: false, + listChanged: true + }, + prompts: { + listChanged: true + }, + logging: {} + } + } + ); // Register all resources, tools, and prompts registerEVMResources(server); registerEVMTools(server); registerEVMPrompts(server); - + // Log server information - console.error(`EVM MCP Server initialized`); - console.error(`Supported networks: ${getSupportedNetworks().join(", ")}`); + console.error(`EVM MCP Server v2.0.0 initialized`); + console.error(`Protocol: MCP 2025-06-18`); + console.error(`Supported networks: ${getSupportedNetworks().length} networks`); console.error("Server is ready to handle requests"); - + return server; } catch (error) { console.error("Failed to initialize server:", error); @@ -31,4 +49,4 @@ async function startServer() { } // Export the server creation function -export default startServer; \ No newline at end of file +export default startServer; diff --git a/tsconfig.json b/tsconfig.json index b0b3d32..d2dcaca 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,16 +1,20 @@ { "compilerOptions": { "target": "ES2022", + "lib": ["ES2022"], "module": "NodeNext", "moduleResolution": "NodeNext", "esModuleInterop": true, "strict": true, + "strictNullChecks": true, "skipLibCheck": true, "outDir": "dist", "sourceMap": true, "declaration": true, - "resolveJsonModule": true + "resolveJsonModule": true, + "forceConsistentCasingInFileNames": true, + "noEmit": true }, "include": ["src/**/*"], - "exclude": ["node_modules", "dist"] + "exclude": ["node_modules", "dist", "build"] }