secrets are now secure

This commit is contained in:
Alex Cheng
2025-02-10 15:59:33 -06:00
parent 77c4543f55
commit 20b44f3b6a
7 changed files with 265 additions and 70 deletions

13
.env.example Normal file
View File

@@ -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

103
README.md
View File

@@ -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
```
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

View File

@@ -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"
},

View File

@@ -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);

View File

@@ -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,38 +233,75 @@ 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,
})
);
// Sign and send transaction
try {
const signature = await sendAndConfirmTransaction(
solanaConnection,
transaction,
[fromKeypair]
[fromKeypair],
{ commitment: 'confirmed' }
);
// Verify the transaction was successful
const confirmedTransaction = await solanaConnection.getTransaction(signature, {
commitment: 'confirmed',
});
if (!confirmedTransaction?.meta?.err) {
return {
content: [
{
@@ -268,13 +315,29 @@ Explorer URL: https://explorer.solana.com/tx/${signature}`,
},
],
};
} else {
throw new Error('Transaction failed on-chain');
}
} catch (err) {
const error = err as Error;
console.error('Transaction 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 transfer SOL: ${errorMessage}. Please check your wallet has sufficient SOL for the transfer and transaction fees.`,
},
],
};
}
} catch (err) {
console.error('Setup error:', err);
const errorMessage = err instanceof Error ? err.message : 'Unknown error occurred';
return {
content: [
{
type: "text",
text: `Failed to setup transfer: ${errorMessage}`,
},
],
};

17
src/env.d.ts vendored Normal file
View File

@@ -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;
}
}

View File

@@ -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";