MCP EVM Signer

  • src
#!/usr/bin/env node import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { z } from 'zod'; import * as crypto from './crypto.js'; import * as ethereum from './ethereum.js'; import config from './config.js'; // Define interface types for the tool handlers interface CreateWalletParams {} interface ImportWalletParams { privateKey: string; } interface ListWalletsParams {} interface CheckBalanceParams { address: string; network?: string; } interface GetTransactionsParams { address: string; limit?: number; network?: string; } interface SendTransactionParams { fromAddress: string; toAddress: string; amount: string; network?: string; } interface DeployContractParams { fromAddress: string; abi: string; bytecode: string; constructorArgs?: string; network?: string; } interface CallContractParams { contractAddress: string; abi: string; method: string; args?: string; network?: string; } interface ExecuteContractParams { fromAddress: string; contractAddress: string; abi: string; method: string; args?: string; network?: string; } async function main() { console.error('Starting MCP EVM Signer server...'); // Create the MCP server const server = new McpServer({ name: 'evm-signer', version: '1.0.0', }); // === Wallet Management Tools === // Create a new wallet server.tool( 'create-wallet', 'Create a new Ethereum wallet', {}, async ({}: CreateWalletParams) => { try { const wallet = crypto.createWallet(); await crypto.saveWallet(wallet); return { content: [{ type: 'text', text: JSON.stringify({ address: wallet.address, message: 'Wallet created and saved successfully.', privateKey: wallet.privateKey }, null, 2) }] }; } catch (error: any) { return { content: [{ type: 'text', text: `Error creating wallet: ${error.message}` }], isError: true }; } } ); // Import an existing wallet server.tool( 'import-wallet', 'Import an existing wallet from a private key', { privateKey: z.string().describe('Private key (with or without 0x prefix)') }, async ({ privateKey }: ImportWalletParams) => { try { const wallet = crypto.importWallet(privateKey); await crypto.saveWallet(wallet); return { content: [{ type: 'text', text: JSON.stringify({ address: wallet.address, message: 'Wallet imported and saved successfully.' }, null, 2) }] }; } catch (error: any) { return { content: [{ type: 'text', text: `Error importing wallet: ${error.message}` }], isError: true }; } } ); // List all wallets server.tool( 'list-wallets', 'List all saved wallets', {}, async ({}: ListWalletsParams) => { try { const addresses = await crypto.getWalletAddresses(); return { content: [{ type: 'text', text: JSON.stringify({ wallets: addresses, count: addresses.length }, null, 2) }] }; } catch (error: any) { return { content: [{ type: 'text', text: `Error listing wallets: ${error.message}` }], isError: true }; } } ); // === Blockchain Interaction Tools === // Check balance server.tool( 'check-balance', 'Check the ETH balance of an Ethereum address', { address: z.string().describe('Ethereum address (0x format)'), network: z.string().optional().describe('Network name (e.g. mainnet, goerli, sepolia)') }, async ({ address, network }: CheckBalanceParams) => { try { const balance = await ethereum.checkBalance( address, network || config.infura.defaultNetwork ); return { content: [{ type: 'text', text: JSON.stringify({ address, network: network || config.infura.defaultNetwork, balance: `${balance} ETH` }, null, 2) }] }; } catch (error: any) { return { content: [{ type: 'text', text: `Error checking balance: ${error.message}` }], isError: true }; } } ); // Get recent transactions server.tool( 'get-transactions', 'Get recent transactions for an Ethereum address', { address: z.string().describe('Ethereum address (0x format)'), limit: z.number().optional().describe('Number of transactions to return (max 100)'), network: z.string().optional().describe('Network name (e.g. mainnet, goerli, sepolia)') }, async ({ address, limit = 10, network }: GetTransactionsParams) => { try { const transactions = await ethereum.getTransactions( address, Math.min(limit, 100), network || config.infura.defaultNetwork ); return { content: [{ type: 'text', text: JSON.stringify({ address, network: network || config.infura.defaultNetwork, transactions, count: transactions.length }, null, 2) }] }; } catch (error: any) { return { content: [{ type: 'text', text: `Error getting transactions: ${error.message}` }], isError: true }; } } ); // Send transaction server.tool( 'send-transaction', 'Send ETH to an address', { fromAddress: z.string().describe('Your wallet address'), toAddress: z.string().describe('Recipient address'), amount: z.string().describe('Amount of ETH to send'), network: z.string().optional().describe('Network name (e.g. mainnet, goerli, sepolia)') }, async ({ fromAddress, toAddress, amount, network }: SendTransactionParams) => { try { const result = await ethereum.sendTransaction( fromAddress, toAddress, amount, network || config.infura.defaultNetwork ); return { content: [{ type: 'text', text: JSON.stringify({ message: `Sent ${amount} ETH from ${fromAddress} to ${toAddress}`, transaction: result.hash, explorer: result.explorer, network: network || config.infura.defaultNetwork }, null, 2) }] }; } catch (error: any) { return { content: [{ type: 'text', text: `Error sending transaction: ${error.message}` }], isError: true }; } } ); // Deploy contract server.tool( 'deploy-contract', 'Deploy a smart contract', { fromAddress: z.string().describe('Your wallet address'), abi: z.string().describe('Contract ABI (JSON)'), bytecode: z.string().describe('Contract bytecode'), constructorArgs: z.string().optional().describe('Constructor arguments as JSON array'), network: z.string().optional().describe('Network name (e.g. mainnet, goerli, sepolia)') }, async ({ fromAddress, abi, bytecode, constructorArgs, network }: DeployContractParams) => { try { let parsedAbi; try { parsedAbi = JSON.parse(abi); } catch (e) { throw new Error('Invalid ABI JSON'); } let parsedArgs: any[] = []; if (constructorArgs) { try { parsedArgs = JSON.parse(constructorArgs); if (!Array.isArray(parsedArgs)) { throw new Error('Constructor arguments must be an array'); } } catch (e) { throw new Error('Invalid constructor arguments JSON'); } } const result = await ethereum.deployContract( fromAddress, parsedAbi, bytecode, parsedArgs, network || config.infura.defaultNetwork ); return { content: [{ type: 'text', text: JSON.stringify({ message: 'Contract deployed successfully', contractAddress: result.address, deploymentTransaction: result.deploymentHash, explorer: result.explorer, network: network || config.infura.defaultNetwork }, null, 2) }] }; } catch (error: any) { return { content: [{ type: 'text', text: `Error deploying contract: ${error.message}` }], isError: true }; } } ); // Call contract (read-only) server.tool( 'call-contract', 'Call a contract method (read-only)', { contractAddress: z.string().describe('Contract address'), abi: z.string().describe('Contract ABI (JSON)'), method: z.string().describe('Method name'), args: z.string().optional().describe('Method arguments as JSON array'), network: z.string().optional().describe('Network name (e.g. mainnet, goerli, sepolia)') }, async ({ contractAddress, abi, method, args, network }: CallContractParams) => { try { let parsedAbi; try { parsedAbi = JSON.parse(abi); } catch (e) { throw new Error('Invalid ABI JSON'); } let parsedArgs: any[] = []; if (args) { try { parsedArgs = JSON.parse(args); if (!Array.isArray(parsedArgs)) { throw new Error('Method arguments must be an array'); } } catch (e) { throw new Error('Invalid method arguments JSON'); } } const result = await ethereum.callContractMethod( contractAddress, parsedAbi, method, parsedArgs, network || config.infura.defaultNetwork ); // Format the result let formattedResult; if (typeof result === 'object' && result !== null) { // Handle BigInts or other objects formattedResult = JSON.stringify(result, (_, value) => typeof value === 'bigint' ? value.toString() : value ); } else { formattedResult = String(result); } return { content: [{ type: 'text', text: JSON.stringify({ contractAddress, method, result: formattedResult, network: network || config.infura.defaultNetwork }, null, 2) }] }; } catch (error: any) { return { content: [{ type: 'text', text: `Error calling contract method: ${error.message}` }], isError: true }; } } ); // Execute contract (write) server.tool( 'execute-contract', 'Execute a contract method (write)', { fromAddress: z.string().describe('Your wallet address'), contractAddress: z.string().describe('Contract address'), abi: z.string().describe('Contract ABI (JSON)'), method: z.string().describe('Method name'), args: z.string().optional().describe('Method arguments as JSON array'), network: z.string().optional().describe('Network name (e.g. mainnet, goerli, sepolia)') }, async ({ fromAddress, contractAddress, abi, method, args, network }: ExecuteContractParams) => { try { let parsedAbi; try { parsedAbi = JSON.parse(abi); } catch (e) { throw new Error('Invalid ABI JSON'); } let parsedArgs: any[] = []; if (args) { try { parsedArgs = JSON.parse(args); if (!Array.isArray(parsedArgs)) { throw new Error('Method arguments must be an array'); } } catch (e) { throw new Error('Invalid method arguments JSON'); } } const result = await ethereum.executeContractMethod( fromAddress, contractAddress, parsedAbi, method, parsedArgs, network || config.infura.defaultNetwork ); return { content: [{ type: 'text', text: JSON.stringify({ message: `Successfully executed ${method} on contract ${contractAddress}`, transaction: result.hash, explorer: result.explorer, network: network || config.infura.defaultNetwork }, null, 2) }] }; } catch (error: any) { return { content: [{ type: 'text', text: `Error executing contract method: ${error.message}` }], isError: true }; } } ); // Start the server with stdio transport const transport = new StdioServerTransport(); await server.connect(transport); console.error('MCP EVM Signer server running'); } main().catch(error => { console.error('Fatal error:', error); process.exit(1); });