diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..1536c12 --- /dev/null +++ b/.env.example @@ -0,0 +1,13 @@ +# Network RPC URLs +SOLANA_RPC_URL=https://api.mainnet-beta.solana.com +ETH_RPC_URL=https://eth-mainnet.g.alchemy.com/v2/your-api-key +ARBITRUM_RPC_URL=https://arb1.arbitrum.io/rpc +BASE_RPC_URL=https://mainnet.base.org +OPTIMISM_RPC_URL=https://mainnet.optimism.io +BSC_RPC_URL=https://bsc-dataseed.binance.org +POLYGON_RPC_URL=https://polygon-rpc.com +AVALANCHE_RPC_URL=https://api.avax.network/ext/bc/C/rpc + +# Private keys (replace with your own) +ETH_PRIVATE_KEY=your-ethereum-private-key +SOLANA_PRIVATE_KEY=your-solana-private-key \ No newline at end of file diff --git a/README.md b/README.md index 39bf966..70a0fd9 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Web3 MCP -A Model-Context-Protocol server for interacting with multiple blockchains including Solana and Ethereum. This server provides simple RPC endpoints for common blockchain operations. +A Model-Context-Protocol server for interacting with multiple blockchains including Solana and Ethereum. This server provides simple RPC endpoints for common blockchain operations, allowing secure interactions with various blockchains through environment variables. ## Features @@ -9,52 +9,119 @@ Solana Operations: - Check SOL account balances - Get detailed account information - Display keypair information -- Transfer SOL between accounts +- Transfer SOL between accounts (using private key from .env) - View SPL token balances - Get detailed SPL token account information -Ethereum Operations: -- Check ETH account balances +Ethereum & EVM Chain Operations: +- Check native token balances across multiple networks - Check ERC-20 token balances +- Send native tokens (using private key from .env) +- Send ERC-20 tokens (using private key from .env) +- Approve ERC-20 token spending (using private key from .env) -## Quickstart +Supported EVM Networks: +- Ethereum +- Base +- Arbitrum +- Optimism +- BSC (Binance Smart Chain) +- Polygon +- Avalanche -clone and install dependencies: +## Setup +1. Clone and install dependencies: ```bash git clone https://github.com/strangelove-ventures/web3-mcp.git cd web3-mcp npm install ``` -build the tool +2. Create a .env file in the root directory: +```bash +cp .env.example .env +``` +3. Configure your environment variables in .env: +```env +# Network RPC URLs (optional - will use public endpoints if not specified) +SOLANA_RPC_URL=https://api.mainnet-beta.solana.com +ETH_RPC_URL=https://eth-mainnet.g.alchemy.com/v2/your-api-key +ARBITRUM_RPC_URL=https://arb1.arbitrum.io/rpc +BASE_RPC_URL=https://mainnet.base.org +OPTIMISM_RPC_URL=https://mainnet.optimism.io +BSC_RPC_URL=https://bsc-dataseed.binance.org +POLYGON_RPC_URL=https://polygon-rpc.com +AVALANCHE_RPC_URL=https://api.avax.network/ext/bc/C/rpc + +# Private keys (required for transactions) +ETH_PRIVATE_KEY=your-ethereum-private-key +SOLANA_PRIVATE_KEY=your-base58-encoded-solana-private-key +``` + +4. Build the tool: ```bash npm run build ``` -add the tool to your claude_desktop_config.json -``` -"web3-rpc": { - "command": "node", - "args": [ - "/PATH/TO/web3-mcp/build/index.js" - ] - } +5. Add the tool to your claude_desktop_config.json: +```json +{ + "web3-rpc": { + "command": "node", + "args": [ + "/PATH/TO/web3-mcp/build/index.js" + ] + } +} ``` -## Usage +## Usage Examples Ask Claude: -- whats the latest slot on solana? -- whats the balance of 62QXuWZ3WT6ws1ZFxJobVDVXn6bEsiYpLo5yG612U6u3? -- Here's my test key [REPLACE WITH SECRET KEY]. let's transfer 0.001 SOL to [REPLACE WITH PUBLIC ADDRESS] -- what's the balance of 0x556437c4d22ceaeeebf82006b85bdcc0ae67d933? -## Security Note +### Solana Operations +- "What's the latest slot on Solana?" +- "What's the balance of 62QXuWZ3WT6ws1ZFxJobVDVXn6bEsiYpLo5yG612U6u3?" +- "Transfer 0.001 SOL to Cg6cVS4tjkxHthm3K9BHhmvqF7kSz8GnXqqYXnHBzGXd" +- "Show me my SPL token balances for address 62QXuWZ3WT6ws1ZFxJobVDVXn6bEsiYpLo5yG612U6u3" -Only use this with a test wallet with a small amount of funds. +### EVM Operations +- "What's the ETH balance of 0x6b96f801ae7f7112e659181a84d2e22ebd0bc72a?" +- "Check the USDC balance for 0x6b96f801ae7f7112e659181a84d2e22ebd0bc72a on Ethereum" +- "Send 0.001 ETH to 0x6b96f801ae7f7112e659181a84d2e22ebd0bc72a" +- "What's the current gas price on Arbitrum?" +- "Send 100 USDC to 0x6b96f801ae7f7112e659181a84d2e22ebd0bc72a on Polygon" -## RPC Endpoint +## Security Notes -The server connects to Solana's mainnet at `https://api.mainnet-beta.solana.com`. To use a different network (like devnet or testnet), modify the `SOLANA_RPC` constant in `src/index.ts`. +1. **Environment Variables**: All private keys are stored in the .env file and never exposed in the conversation history +2. **Private Keys**: Only use this with test wallets containing small amounts of funds +3. **RPC Endpoints**: Custom RPC endpoints can be configured in the .env file for better reliability and rate limits +4. **.env Security**: The .env file is automatically ignored by git to prevent accidental exposure of private keys + +## Advanced Configuration + +### Custom RPC Endpoints +You can configure custom RPC endpoints in your .env file for better reliability and higher rate limits. If not specified, the tool will fall back to public RPC endpoints. + +### Network Selection +For EVM operations, you can specify the network by name (ethereum, base, arbitrum, optimism, bsc, polygon, avalanche). The tool will automatically use the appropriate RPC endpoint and network configuration. + +## Development + +To modify or extend the tool: + +1. Source code is in the `src` directory +2. Chain-specific code is in `src/chains` +3. Run `npm run build` after making changes +4. Use TypeScript for all new code + +## Contributing + +Contributions are welcome! Please submit pull requests with any improvements or bug fixes. + +## License + +Apache-2.0 License \ No newline at end of file diff --git a/package.json b/package.json index c804f0d..e07671e 100644 --- a/package.json +++ b/package.json @@ -50,6 +50,7 @@ "@solana/spl-token": "^0.4.1", "@solana/web3.js": "^1.87.6", "bs58": "^5.0.0", + "dotenv": "^16.4.5", "ethers": "^6.11.1", "zod": "^3.24.1" }, diff --git a/src/chains/ethereum.ts b/src/chains/ethereum.ts index 8faa240..b0cd4fe 100644 --- a/src/chains/ethereum.ts +++ b/src/chains/ethereum.ts @@ -14,49 +14,49 @@ interface NetworkConfig { const NETWORKS: { [key: string]: NetworkConfig } = { ethereum: { name: "Ethereum", - rpc: "https://eth-mainnet.g.alchemy.com/v2/demo", + rpc: process.env.ETH_RPC_URL || "https://eth-mainnet.g.alchemy.com/v2/demo", chainId: 1, currencySymbol: "ETH", explorer: "https://etherscan.io" }, base: { name: "Base", - rpc: "https://mainnet.base.org", + rpc: process.env.BASE_RPC_URL || "https://mainnet.base.org", chainId: 8453, currencySymbol: "ETH", explorer: "https://basescan.org" }, arbitrum: { name: "Arbitrum", - rpc: "https://arb1.arbitrum.io/rpc", + rpc: process.env.ARBITRUM_RPC_URL || "https://arb1.arbitrum.io/rpc", chainId: 42161, currencySymbol: "ETH", explorer: "https://arbiscan.io" }, optimism: { name: "Optimism", - rpc: "https://mainnet.optimism.io", + rpc: process.env.OPTIMISM_RPC_URL || "https://mainnet.optimism.io", chainId: 10, currencySymbol: "ETH", explorer: "https://optimistic.etherscan.io" }, bsc: { name: "BNB Smart Chain", - rpc: "https://bsc-dataseed.binance.org", + rpc: process.env.BSC_RPC_URL || "https://bsc-dataseed.binance.org", chainId: 56, currencySymbol: "BNB", explorer: "https://bscscan.com" }, polygon: { name: "Polygon", - rpc: "https://polygon-rpc.com", + rpc: process.env.POLYGON_RPC_URL || "https://polygon-rpc.com", chainId: 137, currencySymbol: "MATIC", explorer: "https://polygonscan.com" }, avalanche: { name: "Avalanche", - rpc: "https://api.avax.network/ext/bc/C/rpc", + rpc: process.env.AVALANCHE_RPC_URL || "https://api.avax.network/ext/bc/C/rpc", chainId: 43114, currencySymbol: "AVAX", explorer: "https://snowtrace.io" @@ -243,14 +243,13 @@ export function registerEthereumTools(server: McpServer) { // Send native tokens on any EVM network server.tool( "sendEvmTransaction", - "Send native tokens on any supported EVM network", + "Send native tokens on any supported EVM network (using private key from .env)", { - privateKey: z.string().describe("Sender's private key"), toAddress: z.string().describe("Recipient's address"), amount: z.string().describe("Amount to send (in native tokens)"), network: z.string().describe("Network name (ethereum, base, arbitrum, optimism, bsc, polygon, avalanche)"), }, - async ({ privateKey, toAddress, amount, network }) => { + async ({ toAddress, amount, network }) => { try { if (!NETWORKS[network]) { return { @@ -266,8 +265,13 @@ export function registerEthereumTools(server: McpServer) { const provider = providers[network]; const networkConfig = NETWORKS[network]; + // Get private key from environment variables + if (!process.env.ETH_PRIVATE_KEY) { + throw new Error('ETH_PRIVATE_KEY not found in environment variables'); + } + // Create wallet from private key - const wallet = new Wallet(privateKey, provider); + const wallet = new Wallet(process.env.ETH_PRIVATE_KEY, provider); const fromAddress = wallet.address; // Get current gas price and nonce @@ -315,15 +319,14 @@ export function registerEthereumTools(server: McpServer) { // Send ERC-20 tokens on any EVM network server.tool( "sendEvmToken", - "Send ERC-20 tokens on any supported EVM network", + "Send ERC-20 tokens on any supported EVM network (using private key from .env)", { - privateKey: z.string().describe("Sender's private key"), toAddress: z.string().describe("Recipient's address"), tokenAddress: z.string().describe("Token contract address"), amount: z.string().describe("Amount to send (in token units)"), network: z.string().describe("Network name (ethereum, base, arbitrum, optimism, bsc, polygon, avalanche)"), }, - async ({ privateKey, toAddress, tokenAddress, amount, network }) => { + async ({ toAddress, tokenAddress, amount, network }) => { try { if (!NETWORKS[network]) { return { @@ -339,8 +342,13 @@ export function registerEthereumTools(server: McpServer) { const provider = providers[network]; const networkConfig = NETWORKS[network]; + // Get private key from environment variables + if (!process.env.ETH_PRIVATE_KEY) { + throw new Error('ETH_PRIVATE_KEY not found in environment variables'); + } + // Create wallet from private key - const wallet = new Wallet(privateKey, provider); + const wallet = new Wallet(process.env.ETH_PRIVATE_KEY, provider); const fromAddress = wallet.address; // Create contract instance @@ -385,15 +393,14 @@ export function registerEthereumTools(server: McpServer) { // Approve ERC-20 token spending server.tool( "approveEvmToken", - "Approve ERC-20 token spending on any supported EVM network", + "Approve ERC-20 token spending on any supported EVM network (using private key from .env)", { - privateKey: z.string().describe("Token holder's private key"), spenderAddress: z.string().describe("Address to approve for spending"), tokenAddress: z.string().describe("Token contract address"), amount: z.string().optional().describe("Amount to approve (in token units, defaults to unlimited)"), network: z.string().describe("Network name (ethereum, base, arbitrum, optimism, bsc, polygon, avalanche)"), }, - async ({ privateKey, spenderAddress, tokenAddress, amount, network }) => { + async ({ spenderAddress, tokenAddress, amount, network }) => { try { if (!NETWORKS[network]) { return { @@ -409,8 +416,13 @@ export function registerEthereumTools(server: McpServer) { const provider = providers[network]; const networkConfig = NETWORKS[network]; + // Get private key from environment variables + if (!process.env.ETH_PRIVATE_KEY) { + throw new Error('ETH_PRIVATE_KEY not found in environment variables'); + } + // Create wallet from private key - const wallet = new Wallet(privateKey, provider); + const wallet = new Wallet(process.env.ETH_PRIVATE_KEY, provider); // Create contract instance const contract = new Contract(tokenAddress, ERC20_ABI, wallet); diff --git a/src/chains/solana.ts b/src/chains/solana.ts index 36ba4a5..b47a0a7 100644 --- a/src/chains/solana.ts +++ b/src/chains/solana.ts @@ -1,3 +1,6 @@ +import { config } from 'dotenv'; +config(); + import { Connection, PublicKey, @@ -15,7 +18,7 @@ import { z } from "zod"; import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import bs58 from 'bs58'; -const SOLANA_RPC = "https://api.mainnet-beta.solana.com"; +const SOLANA_RPC = process.env.SOLANA_RPC_URL || "https://api.mainnet-beta.solana.com"; // Initialize Solana connection const solanaConnection = new Connection(SOLANA_RPC, 'confirmed'); @@ -47,6 +50,13 @@ async function getTokenAccounts(walletAddress: string) { } export function registerSolanaTools(server: McpServer) { + // Debug: Check environment variables + console.error('Debug - ENV vars available:', Object.keys(process.env)); + console.error('Debug - SOLANA_PRIVATE_KEY exists:', !!process.env.SOLANA_PRIVATE_KEY); + if (!process.env.SOLANA_PRIVATE_KEY) { + console.error('Warning: SOLANA_PRIVATE_KEY not found in environment'); + } + server.tool( "getSlot", "Get the current slot", @@ -223,58 +233,111 @@ Data (${encoding}): ${formattedData}`, server.tool( "transfer", - "Transfer SOL from your keypair to another address", + "Transfer SOL from your keypair (using private key from .env) to another address", { - secretKey: z.string().describe("Your keypair's secret key (as comma-separated numbers or JSON array)"), toAddress: z.string().describe("Destination wallet address"), amount: z.number().positive().describe("Amount of SOL to send"), }, - async ({ secretKey, toAddress, amount }) => { + async ({ toAddress, amount }) => { try { - let fromKeypair: Keypair; - try { - const decoded = Uint8Array.from(secretKey.split(',').map(num => parseInt(num.trim()))); - fromKeypair = Keypair.fromSecretKey(decoded); - } catch { - fromKeypair = Keypair.fromSecretKey(Uint8Array.from(JSON.parse(secretKey))); + if (!process.env.SOLANA_PRIVATE_KEY) { + throw new Error('SOLANA_PRIVATE_KEY not found in environment variables'); } + let fromKeypair: Keypair; + try { + // Try to decode the private key from base58 + const privateKeyBytes = bs58.decode(process.env.SOLANA_PRIVATE_KEY); + fromKeypair = Keypair.fromSecretKey(new Uint8Array(privateKeyBytes)); + + // Verify the keypair is valid + if (!fromKeypair.publicKey) { + throw new Error('Invalid keypair generated'); + } + } catch (error) { + console.error('Error decoding private key:', error); + throw new Error('Failed to create keypair from private key. Please ensure the private key is a valid base58-encoded string.'); + } + + // Convert SOL to lamports const lamports = amount * LAMPORTS_PER_SOL; - const transaction = new Transaction().add( + // Validate destination address + let destinationPubkey: PublicKey; + try { + destinationPubkey = new PublicKey(toAddress); + } catch (error) { + throw new Error('Invalid destination address'); + } + + // Build transaction + const transaction = new Transaction(); + + // Get recent blockhash + const { blockhash } = await solanaConnection.getLatestBlockhash('finalized'); + transaction.recentBlockhash = blockhash; + transaction.feePayer = fromKeypair.publicKey; + + // Add transfer instruction + transaction.add( SystemProgram.transfer({ fromPubkey: fromKeypair.publicKey, - toPubkey: new PublicKey(toAddress), + toPubkey: destinationPubkey, lamports, }) ); - const signature = await sendAndConfirmTransaction( - solanaConnection, - transaction, - [fromKeypair] - ); + // Sign and send transaction + try { + const signature = await sendAndConfirmTransaction( + solanaConnection, + transaction, + [fromKeypair], + { commitment: 'confirmed' } + ); - return { - content: [ - { - type: "text", - text: `Transfer successful! + // Verify the transaction was successful + const confirmedTransaction = await solanaConnection.getTransaction(signature, { + commitment: 'confirmed', + }); + + if (!confirmedTransaction?.meta?.err) { + return { + content: [ + { + type: "text", + text: `Transfer successful! From: ${fromKeypair.publicKey.toBase58()} To: ${toAddress} Amount: ${amount} SOL Transaction signature: ${signature} Explorer URL: https://explorer.solana.com/tx/${signature}`, - }, - ], - }; + }, + ], + }; + } else { + throw new Error('Transaction failed on-chain'); + } + } catch (err) { + console.error('Transaction error:', err); + const errorMessage = err instanceof Error ? err.message : 'Unknown error occurred'; + return { + content: [ + { + type: "text", + text: `Failed to transfer SOL: ${errorMessage}. Please check your wallet has sufficient SOL for the transfer and transaction fees.`, + }, + ], + }; + } } catch (err) { - const error = err as Error; + console.error('Setup error:', err); + const errorMessage = err instanceof Error ? err.message : 'Unknown error occurred'; return { content: [ { type: "text", - text: `Failed to transfer SOL: ${error.message}`, + text: `Failed to setup transfer: ${errorMessage}`, }, ], }; diff --git a/src/env.d.ts b/src/env.d.ts new file mode 100644 index 0000000..08a3da0 --- /dev/null +++ b/src/env.d.ts @@ -0,0 +1,17 @@ +declare namespace NodeJS { + interface ProcessEnv { + // Solana + SOLANA_RPC_URL: string; + SOLANA_PRIVATE_KEY: string; + + // Ethereum and other EVM chains + ETH_RPC_URL: string; + ETH_PRIVATE_KEY: string; + BASE_RPC_URL: string; + ARBITRUM_RPC_URL: string; + OPTIMISM_RPC_URL: string; + BSC_RPC_URL: string; + POLYGON_RPC_URL: string; + AVALANCHE_RPC_URL: string; + } +} \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index b7d9c5c..5174d98 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,3 +1,25 @@ +import { config } from 'dotenv'; + +import { fileURLToPath } from 'url'; +import { dirname, resolve } from 'path'; +import { existsSync } from 'fs'; + +// Get directory name for ES module +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +// Check for .env file +const envPath = resolve(__dirname, '../.env'); +console.error('Looking for .env file at:', envPath); +console.error('.env file exists:', existsSync(envPath)); + +// Load environment variables +const result = config({ path: envPath }); +if (result.error) { + console.error('Error loading .env file:', result.error); + process.exit(1); +} + import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { registerSolanaTools } from "./chains/solana.js";