diff --git a/README.md b/README.md index 322b31a..8c36429 100644 --- a/README.md +++ b/README.md @@ -56,12 +56,14 @@ All services are exposed through a consistent interface of MCP tools, resources, ### Token services - **ERC20 Tokens** + - Get token metadata (name, symbol, decimals, supply) - Check token balances - Transfer tokens between addresses - Approve spending allowances - **NFTs (ERC721)** + - Get collection and token metadata - Verify token ownership - Transfer NFTs between addresses @@ -89,6 +91,14 @@ All services are exposed through a consistent interface of MCP tools, resources, - **Transaction status** and receipt information - **Error handling** with descriptive messages +### Message Signing Capabilities + +- **Personal Message Signing** - Sign arbitrary messages for authentication and verification +- **EIP-712 Typed Data Signing** - Sign structured data for gasless transactions and meta-transactions +- **SIWE Support** - Enable Sign-In With Ethereum authentication flows +- **Permit Signatures** - Create off-chain approvals for gasless token operations +- **Meta-Transaction Support** - Sign transaction data for relay services and gasless transfers + ### AI-Guided Workflows (Prompts) - **Transaction preparation** - Guidance for planning and executing transfers @@ -102,6 +112,7 @@ All services are exposed through a consistent interface of MCP tools, resources, ## 🌐 Supported Networks ### Mainnets + - Ethereum (ETH) - Optimism (OP) - Arbitrum (ARB) @@ -135,6 +146,7 @@ All services are exposed through a consistent interface of MCP tools, resources, - Lumia ### Testnets + - Sepolia - Optimism Sepolia - Arbitrum Sepolia @@ -204,18 +216,23 @@ export EVM_ACCOUNT_INDEX="0" # Optional: Account index for HD wallet derivation ``` The mnemonic option supports hierarchical deterministic (HD) wallet derivation: + - Uses BIP-39 standard mnemonic phrases (12 or 24 words) - Supports BIP-44 derivation path: `m/44'/60'/0'/0/{accountIndex}` - `EVM_ACCOUNT_INDEX` allows you to derive different accounts from the same mnemonic - Default account index is 0 (first account) **Wallet is used for:** + - Transferring native tokens (`transfer_native` tool) - Transferring ERC20 tokens (`transfer_erc20` tool) - Approving token spending (`approve_token_spending` tool) - Writing to smart contracts (`write_contract` tool) +- Signing messages for authentication (`sign_message` tool) +- Signing structured data for gasless transactions (`sign_typed_data` tool) + +⚠️ **Security**: -⚠️ **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 @@ -228,11 +245,13 @@ 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 @@ -298,6 +317,7 @@ To connect to the MCP server from Cursor: 3. Scroll down to "MCP Servers" section 4. Click "Add new MCP server" 5. Enter the following details: + - Server name: `evm-mcp-server` - Type: `command` - Command: `npx @mcpdotdirect/evm-mcp-server` @@ -315,18 +335,11 @@ For a more portable configuration that you can share with your team or use acros "mcpServers": { "evm-mcp-server": { "command": "npx", - "args": [ - "-y", - "@mcpdotdirect/evm-mcp-server" - ] + "args": ["-y", "@mcpdotdirect/evm-mcp-server"] }, "evm-mcp-http": { "command": "npx", - "args": [ - "-y", - "@mcpdotdirect/evm-mcp-server", - "--http" - ] + "args": ["-y", "@mcpdotdirect/evm-mcp-server", "--http"] } } } @@ -353,11 +366,13 @@ If you're developing a web application and want to connect to the HTTP server wi ``` This connects directly to the HTTP server's SSE endpoint, which is useful for: + - Web applications that need to connect to the MCP server from the browser - Environments where running local commands isn't ideal - Sharing a single MCP server instance among multiple users or applications To use this configuration: + 1. Create a `.cursor` directory in your project root if it doesn't exist 2. Save the above JSON as `mcp.json` in the `.cursor` directory 3. Restart Cursor or open your project @@ -375,14 +390,14 @@ async function main() { try { // Get ETH balance for an address using ENS console.log("Getting ETH balance for vitalik.eth..."); - + // When using with Cursor, you can simply ask Cursor to: // "Check the ETH balance of vitalik.eth on mainnet" // Or "Transfer 0.1 ETH from my wallet to vitalik.eth" - - // Cursor will use the MCP server to execute these operations + + // Cursor will use the MCP server to execute these operations // without requiring any additional code from you - + // This is the power of the MCP integration - your AI assistant // can directly interact with blockchain data and operations } catch (error) { @@ -425,7 +440,7 @@ const mcp = new McpClient("http://localhost:3000"); const result = await mcp.invokeTool("get-token-balance", { tokenAddress: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", // USDC on Ethereum ownerAddress: "vitalik.eth", // ENS name instead of address - network: "ethereum" + network: "ethereum", }); console.log(result); @@ -448,7 +463,7 @@ const mcp = new McpClient("http://localhost:3000"); const result = await mcp.invokeTool("resolve-ens", { ensName: "vitalik.eth", - network: "ethereum" + network: "ethereum", }); console.log(result); @@ -460,73 +475,120 @@ console.log(result); // } ``` +### Example: Batch Multiple Calls with Multicall + +```javascript +// Example of using multicall to batch multiple contract reads in a single RPC call +const mcp = new McpClient("http://localhost:3000"); + +const result = await mcp.invokeTool("multicall", { + network: "ethereum", + calls: [ + { + contractAddress: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", // USDC + functionName: "balanceOf", + args: ["0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045"], + }, + { + contractAddress: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", // USDC + functionName: "symbol", + }, + { + contractAddress: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", // USDC + functionName: "decimals", + }, + ], +}); + +console.log(result); +// { +// network: "ethereum", +// totalCalls: 3, +// successfulCalls: 3, +// failedCalls: 0, +// results: [ +// { contractAddress: "0xA0b...", functionName: "balanceOf", result: "1000000000", status: "success" }, +// { contractAddress: "0xA0b...", functionName: "symbol", result: "USDC", status: "success" }, +// { contractAddress: "0xA0b...", functionName: "decimals", result: "6", status: "success" } +// ] +// } +``` + ## 📚 API Reference ### Tools -The server provides 22 focused MCP tools for agents. **All tools that accept address parameters support both Ethereum addresses and ENS names.** +The server provides 25 focused MCP tools for agents. **All tools that accept address parameters support both Ethereum addresses and ENS names.** #### Wallet Information -| Tool Name | Description | Key Parameters | -|-----------|-------------|----------------| -| `get_wallet_address` | Get the address of the configured wallet (from EVM_PRIVATE_KEY) | none | +| Tool Name | Description | Key Parameters | +| -------------------- | --------------------------------------------------------------- | -------------- | +| `get_wallet_address` | Get the address of the configured wallet (from EVM_PRIVATE_KEY) | none | #### Network Information -| Tool Name | Description | Key Parameters | -|-----------|-------------|----------------| -| `get_chain_info` | Get network information | `network` | -| `get_supported_networks` | List all supported EVM networks | none | -| `get_gas_price` | Get current gas prices on a network | `network` | +| 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` | +| 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` | +| 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` | +| 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` | +| Tool Name | Description | Key Parameters | +| ------------------ | --------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------ | +| `get_contract_abi` | Fetch contract ABI from block explorer (60+ networks) | `contractAddress` (address/ENS), `network` | +| `read_contract` | Read smart contract state (auto-fetches ABI if needed) | `contractAddress`, `functionName`, `args[]`, `abiJson` (optional), `network` | +| `write_contract` | Execute state-changing functions (auto-fetches ABI if needed) | `contractAddress`, `functionName`, `args[]`, `value` (optional), `abiJson` (optional), `network` | +| `multicall` | Batch multiple read calls into a single RPC request (uses Multicall3) | `calls[]` (array of contract calls), `allowFailure` (optional), `network` | #### Token Transfers -| Tool Name | Description | Key Parameters | -|-----------|-------------|----------------| -| `transfer_native` | Send native tokens (ETH, etc.) | `to` (address/ENS), `amount`, `network` | -| `transfer_erc20` | Transfer ERC20 tokens | `tokenAddress` (address/ENS), `to` (address/ENS), `amount`, `network` | -| `approve_token_spending` | Approve token allowances | `tokenAddress` (address/ENS), `spenderAddress` (address/ENS), `amount`, `network` | +| Tool Name | Description | Key Parameters | +| ------------------------ | ------------------------------ | --------------------------------------------------------------------------------- | +| `transfer_native` | Send native tokens (ETH, etc.) | `to` (address/ENS), `amount`, `network` | +| `transfer_erc20` | Transfer ERC20 tokens | `tokenAddress` (address/ENS), `to` (address/ENS), `amount`, `network` | +| `approve_token_spending` | Approve token allowances | `tokenAddress` (address/ENS), `spenderAddress` (address/ENS), `amount`, `network` | #### NFT Services -| Tool Name | Description | Key Parameters | -|-----------|-------------|----------------| -| `get_nft_info` | Get NFT (ERC721) metadata | `tokenAddress` (address/ENS), `tokenId`, `network` | -| `get_erc1155_balance` | Check ERC1155 balance | `tokenAddress` (address/ENS), `tokenId`, `ownerAddress` (address/ENS), `network` | +| Tool Name | Description | Key Parameters | +| --------------------- | ------------------------- | -------------------------------------------------------------------------------- | +| `get_nft_info` | Get NFT (ERC721) metadata | `tokenAddress` (address/ENS), `tokenId`, `network` | +| `get_erc1155_balance` | Check ERC1155 balance | `tokenAddress` (address/ENS), `tokenId`, `ownerAddress` (address/ENS), `network` | + +#### Message Signing + +| Tool Name | Description | Key Parameters | +| ----------------- | ---------------------------------------------------------------------------------------- | ------------------------------------------------------- | +| `sign_message` | Sign arbitrary messages for authentication and verification (SIWE, off-chain signatures) | `message` | +| `sign_typed_data` | Sign EIP-712 structured data for gasless transactions, permits, and meta-transactions | `domainJson`, `typesJson`, `primaryType`, `messageJson` | ### Resources @@ -534,26 +596,26 @@ The server exposes blockchain data through the following MCP resource URIs. All #### Blockchain Resources -| Resource URI Pattern | Description | -|-----------|-------------| -| `evm://{network}/chain` | Chain information for a specific network | -| `evm://chain` | Ethereum mainnet chain information | -| `evm://{network}/block/{blockNumber}` | Block data by number | -| `evm://{network}/block/latest` | Latest block data | -| `evm://{network}/address/{address}/balance` | Native token balance | -| `evm://{network}/tx/{txHash}` | Transaction details | -| `evm://{network}/tx/{txHash}/receipt` | Transaction receipt with logs | +| Resource URI Pattern | Description | +| ------------------------------------------- | ---------------------------------------- | +| `evm://{network}/chain` | Chain information for a specific network | +| `evm://chain` | Ethereum mainnet chain information | +| `evm://{network}/block/{blockNumber}` | Block data by number | +| `evm://{network}/block/latest` | Latest block data | +| `evm://{network}/address/{address}/balance` | Native token balance | +| `evm://{network}/tx/{txHash}` | Transaction details | +| `evm://{network}/tx/{txHash}/receipt` | Transaction receipt with logs | #### Token Resources -| Resource URI Pattern | Description | -|-----------|-------------| -| `evm://{network}/token/{tokenAddress}` | ERC20 token information | -| `evm://{network}/token/{tokenAddress}/balanceOf/{address}` | ERC20 token balance | -| `evm://{network}/nft/{tokenAddress}/{tokenId}` | NFT (ERC721) token information | -| `evm://{network}/nft/{tokenAddress}/{tokenId}/isOwnedBy/{address}` | NFT ownership verification | -| `evm://{network}/erc1155/{tokenAddress}/{tokenId}/uri` | ERC1155 token URI | -| `evm://{network}/erc1155/{tokenAddress}/{tokenId}/balanceOf/{address}` | ERC1155 token balance | +| Resource URI Pattern | Description | +| ---------------------------------------------------------------------- | ------------------------------ | +| `evm://{network}/token/{tokenAddress}` | ERC20 token information | +| `evm://{network}/token/{tokenAddress}/balanceOf/{address}` | ERC20 token balance | +| `evm://{network}/nft/{tokenAddress}/{tokenId}` | NFT (ERC721) token information | +| `evm://{network}/nft/{tokenAddress}/{tokenId}/isOwnedBy/{address}` | NFT ownership verification | +| `evm://{network}/erc1155/{tokenAddress}/{tokenId}/uri` | ERC1155 token URI | +| `evm://{network}/erc1155/{tokenAddress}/{tokenId}/balanceOf/{address}` | ERC1155 token balance | ## 🔒 Security Considerations diff --git a/src/core/services/contracts.ts b/src/core/services/contracts.ts index faebb67..650d054 100644 --- a/src/core/services/contracts.ts +++ b/src/core/services/contracts.ts @@ -1,6 +1,6 @@ -import { - type Address, - type Hash, +import { + type Address, + type Hash, type Hex, type ReadContractParameters, type GetLogsParameters, @@ -46,8 +46,33 @@ export async function getLogs(params: GetLogsParameters, network = 'ethereum'): export async function isContract(addressOrEns: string, network = 'ethereum'): Promise { // Resolve ENS name to address if needed const address = await resolveAddress(addressOrEns, network); - + const client = getPublicClient(network); const code = await client.getBytecode({ address }); return code !== undefined && code !== '0x'; +} + +/** + * Batch multiple contract read calls into a single RPC request using Multicall3 + * @param contracts Array of contract calls to batch + * @param allowFailure If true, returns partial results even if some calls fail + * @param network Network name or chain ID + * @returns Array of results with status + */ +export async function multicall( + contracts: Array<{ + address: Address; + abi: any[]; + functionName: string; + args?: any[]; + }>, + allowFailure = true, + network = 'ethereum' +): Promise { + const client = getPublicClient(network); + + return await client.multicall({ + contracts: contracts as any, + allowFailure + }); } \ No newline at end of file diff --git a/src/core/services/wallet.ts b/src/core/services/wallet.ts index 799bc10..c55dbeb 100644 --- a/src/core/services/wallet.ts +++ b/src/core/services/wallet.ts @@ -55,8 +55,10 @@ export const getConfiguredPrivateKey = (): Hex => { 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'); + // Convert Uint8Array to hex string (compatible with Bun and Node) + const privateKeyHex = Array.from(hdKey.privateKey) + .map(byte => byte.toString(16).padStart(2, '0')) + .join(''); return `0x${privateKeyHex}` as Hex; } @@ -85,3 +87,52 @@ export const getWalletAddressFromKey = (): Address => { export const getConfiguredWallet = (): { address: Address } => { return { address: getWalletAddressFromKey() }; }; + +/** + * Sign an arbitrary message using the configured wallet + * @param message The message to sign (can be a string or hex data) + * @returns The signature as a hex string + */ +export const signMessage = async (message: string): Promise => { + const account = getConfiguredAccount(); + + // Use the account's signMessage method directly + const signature = await account.signMessage({ + message: message + }); + + return signature; +}; + +/** + * Sign typed data (EIP-712) using the configured wallet + * @param domain The EIP-712 domain + * @param types The types definition (excluding EIP712Domain) + * @param primaryType The primary type name + * @param message The message data to sign + * @returns The signature as a hex string + */ +export const signTypedData = async ( + domain: { + name?: string; + version?: string; + chainId?: number; + verifyingContract?: Address; + salt?: `0x${string}`; + }, + types: Record>, + primaryType: string, + message: Record +): Promise => { + const account = getConfiguredAccount(); + + // Use the account's signTypedData method + const signature = await account.signTypedData({ + domain, + types, + primaryType, + message + }); + + return signature; +}; diff --git a/src/core/tools.ts b/src/core/tools.ts index ee2764b..f10ed2d 100644 --- a/src/core/tools.ts +++ b/src/core/tools.ts @@ -863,6 +863,130 @@ export function registerEVMTools(server: McpServer) { } ); + server.registerTool( + "multicall", + { + description: "Batch multiple contract read calls into a single RPC request. Significantly reduces latency and RPC usage when querying multiple functions. Uses the Multicall3 contract deployed on all major networks. Perfect for portfolio analysis, price aggregation, and querying multiple contract states efficiently.", + inputSchema: { + calls: z.array(z.object({ + contractAddress: z.string().describe("The contract address"), + functionName: z.string().describe("Function name to call"), + args: z.array(z.string()).optional().describe("Function arguments as strings"), + abiJson: z.string().optional().describe("Contract ABI as JSON string (optional - will auto-fetch if not provided)") + })).describe("Array of contract calls to batch together"), + allowFailure: z.boolean().optional().describe("If true, returns partial results even if some calls fail. Defaults to true."), + network: z.string().optional().describe("Network name or chain ID. Defaults to Ethereum mainnet.") + }, + annotations: { + title: "Multicall (Batch Read)", + readOnlyHint: true, + destructiveHint: false, + idempotentHint: true, + openWorldHint: true + } + }, + async ({ calls, allowFailure = true, network = "ethereum" }) => { + try { + // Build contracts array with ABIs + const contractsWithAbis = await Promise.all( + calls.map(async (call) => { + let abi: any[]; + let functionAbi: any; + + // If ABI is provided, use it + if (call.abiJson) { + try { + abi = services.parseABI(call.abiJson); + functionAbi = services.getFunctionFromABI(abi, call.functionName); + } catch (error) { + throw new Error(`Error parsing ABI for ${call.contractAddress}: ${error instanceof Error ? error.message : String(error)}`); + } + } else { + // Try to auto-fetch ABI + try { + const fetchedAbi = await services.fetchContractABI(call.contractAddress as Address, network); + abi = services.parseABI(fetchedAbi); + functionAbi = services.getFunctionFromABI(abi, call.functionName); + } catch (fetchError) { + // Fall back to common function signatures + const commonFunctions: { [key: string]: any } = { + 'name': { inputs: [], outputs: [{ type: 'string' }], stateMutability: 'view' }, + 'symbol': { inputs: [], outputs: [{ type: 'string' }], stateMutability: 'view' }, + 'decimals': { inputs: [], outputs: [{ type: 'uint8' }], stateMutability: 'view' }, + 'totalSupply': { inputs: [], outputs: [{ type: 'uint256' }], stateMutability: 'view' }, + 'balanceOf': { inputs: [{ type: 'address' }], outputs: [{ type: 'uint256' }], stateMutability: 'view' }, + 'allowance': { inputs: [{ type: 'address' }, { type: 'address' }], outputs: [{ type: 'uint256' }], stateMutability: 'view' }, + }; + + if (!commonFunctions[call.functionName]) { + throw new Error(`Could not auto-fetch ABI for ${call.contractAddress}. Function '${call.functionName}' not in common signatures. Please provide abiJson parameter.`); + } + + functionAbi = { + name: call.functionName, + type: 'function', + inputs: commonFunctions[call.functionName].inputs, + outputs: commonFunctions[call.functionName].outputs, + stateMutability: 'view' + }; + } + } + + return { + address: call.contractAddress as Address, + abi: [functionAbi], + functionName: call.functionName, + args: call.args || [] + }; + }) + ); + + // Execute multicall + const results = await services.multicall(contractsWithAbis, allowFailure, network); + + // Format results + const formattedResults = results.map((result: any, index: number) => { + const call = calls[index]; + if (result.status === 'success') { + return { + contractAddress: call.contractAddress, + functionName: call.functionName, + args: call.args, + result: result.result?.toString(), + status: 'success' + }; + } else { + return { + contractAddress: call.contractAddress, + functionName: call.functionName, + args: call.args, + error: result.error?.message || 'Unknown error', + status: 'failure' + }; + } + }); + + return { + content: [{ + type: "text", + text: JSON.stringify({ + network, + totalCalls: calls.length, + successfulCalls: formattedResults.filter((r: any) => r.status === 'success').length, + failedCalls: formattedResults.filter((r: any) => r.status === 'failure').length, + results: formattedResults + }, null, 2) + }] + }; + } catch (error) { + return { + content: [{ type: "text", text: `Error executing multicall: ${error instanceof Error ? error.message : String(error)}` }], + isError: true + }; + } + } + ); + // ============================================================================ // TRANSFER TOOLS (Write operations) // ============================================================================ @@ -981,7 +1105,7 @@ export function registerEVMTools(server: McpServer) { try { const privateKey = getConfiguredPrivateKey(); const senderAddress = getWalletAddressFromKey(); - const txHash = await services.approveERC20(privateKey, tokenAddress as Address, spenderAddress as Address, amount, network); + const txHash = await services.approveERC20(tokenAddress as Address, spenderAddress as Address, amount, privateKey, network); return { content: [{ type: "text", @@ -1090,4 +1214,110 @@ export function registerEVMTools(server: McpServer) { } } ); + + // ============================================================================ + // MESSAGE SIGNING TOOLS (Write operations) + // ============================================================================ + + server.registerTool( + "sign_message", + { + description: "Sign an arbitrary message using the configured wallet. Useful for authentication (SIWE), meta-transactions, and off-chain signatures. The signature can be verified on-chain or off-chain.", + inputSchema: { + message: z.string().describe("The message to sign (plain text or hex-encoded data)") + }, + annotations: { + title: "Sign Message", + readOnlyHint: false, + destructiveHint: false, + idempotentHint: true, + openWorldHint: false + } + }, + async ({ message }) => { + try { + const senderAddress = getWalletAddressFromKey(); + const signature = await services.signMessage(message); + return { + content: [{ + type: "text", + text: JSON.stringify({ + message, + signature, + signer: senderAddress, + messageType: "personal_sign" + }, null, 2) + }] + }; + } catch (error) { + return { + content: [{ type: "text", text: `Error signing message: ${error instanceof Error ? error.message : String(error)}` }], + isError: true + }; + } + } + ); + + server.registerTool( + "sign_typed_data", + { + description: "Sign structured data (EIP-712) using the configured wallet. Used for gasless transactions, meta-transactions, permit signatures, and protocol-specific signatures. The signature follows the EIP-712 standard.", + inputSchema: { + domainJson: z.string().describe("EIP-712 domain as JSON string with fields: name, version, chainId, verifyingContract, salt (all optional)"), + typesJson: z.string().describe("EIP-712 types definition as JSON string (exclude EIP712Domain type - it's added automatically)"), + primaryType: z.string().describe("The primary type name (e.g., 'Mail', 'Permit', 'MetaTransaction')"), + messageJson: z.string().describe("The message data to sign as JSON string") + }, + annotations: { + title: "Sign Typed Data (EIP-712)", + readOnlyHint: false, + destructiveHint: false, + idempotentHint: true, + openWorldHint: false + } + }, + async ({ domainJson, typesJson, primaryType, messageJson }) => { + try { + const senderAddress = getWalletAddressFromKey(); + + // Parse JSON inputs + let domain, types, message; + try { + domain = JSON.parse(domainJson); + types = JSON.parse(typesJson); + message = JSON.parse(messageJson); + } catch (parseError) { + return { + content: [{ + type: "text", + text: `Error parsing JSON inputs: ${parseError instanceof Error ? parseError.message : String(parseError)}` + }], + isError: true + }; + } + + const signature = await services.signTypedData(domain, types, primaryType, message); + + return { + content: [{ + type: "text", + text: JSON.stringify({ + domain, + types, + primaryType, + message, + signature, + signer: senderAddress, + messageType: "EIP-712" + }, null, 2) + }] + }; + } catch (error) { + return { + content: [{ type: "text", text: `Error signing typed data: ${error instanceof Error ? error.message : String(error)}` }], + isError: true + }; + } + } + ); }