├── .gitignore ├── LICENSE ├── README.md ├── assets ├── analysis_gif.gif ├── send_tx.png └── tools.png ├── bun.lockb ├── funding.json ├── package.json ├── src └── index.ts └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | # Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore 2 | 3 | # Logs 4 | 5 | logs 6 | _.log 7 | npm-debug.log_ 8 | yarn-debug.log* 9 | yarn-error.log* 10 | lerna-debug.log* 11 | .pnpm-debug.log* 12 | 13 | # Caches 14 | 15 | .cache 16 | 17 | # Diagnostic reports (https://nodejs.org/api/report.html) 18 | 19 | report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json 20 | 21 | # Runtime data 22 | 23 | pids 24 | _.pid 25 | _.seed 26 | *.pid.lock 27 | 28 | # Directory for instrumented libs generated by jscoverage/JSCover 29 | 30 | lib-cov 31 | 32 | # Coverage directory used by tools like istanbul 33 | 34 | coverage 35 | *.lcov 36 | 37 | # nyc test coverage 38 | 39 | .nyc_output 40 | 41 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 42 | 43 | .grunt 44 | 45 | # Bower dependency directory (https://bower.io/) 46 | 47 | bower_components 48 | 49 | # node-waf configuration 50 | 51 | .lock-wscript 52 | 53 | # Compiled binary addons (https://nodejs.org/api/addons.html) 54 | 55 | build/Release 56 | 57 | # Dependency directories 58 | 59 | node_modules/ 60 | jspm_packages/ 61 | 62 | # Snowpack dependency directory (https://snowpack.dev/) 63 | 64 | web_modules/ 65 | 66 | # TypeScript cache 67 | 68 | *.tsbuildinfo 69 | 70 | # Optional npm cache directory 71 | 72 | .npm 73 | 74 | # Optional eslint cache 75 | 76 | .eslintcache 77 | 78 | # Optional stylelint cache 79 | 80 | .stylelintcache 81 | 82 | # Microbundle cache 83 | 84 | .rpt2_cache/ 85 | .rts2_cache_cjs/ 86 | .rts2_cache_es/ 87 | .rts2_cache_umd/ 88 | 89 | # Optional REPL history 90 | 91 | .node_repl_history 92 | 93 | # Output of 'npm pack' 94 | 95 | *.tgz 96 | 97 | # Yarn Integrity file 98 | 99 | .yarn-integrity 100 | 101 | # dotenv environment variable files 102 | 103 | .env 104 | .env.development.local 105 | .env.test.local 106 | .env.production.local 107 | .env.local 108 | 109 | # parcel-bundler cache (https://parceljs.org/) 110 | 111 | .parcel-cache 112 | 113 | # Next.js build output 114 | 115 | .next 116 | out 117 | 118 | # Nuxt.js build / generate output 119 | 120 | .nuxt 121 | dist 122 | 123 | # Gatsby files 124 | 125 | # Comment in the public line in if your project uses Gatsby and not Next.js 126 | 127 | # https://nextjs.org/blog/next-9-1#public-directory-support 128 | 129 | # public 130 | 131 | # vuepress build output 132 | 133 | .vuepress/dist 134 | 135 | # vuepress v2.x temp and cache directory 136 | 137 | .temp 138 | 139 | # Docusaurus cache and generated files 140 | 141 | .docusaurus 142 | 143 | # Serverless directories 144 | 145 | .serverless/ 146 | 147 | # FuseBox cache 148 | 149 | .fusebox/ 150 | 151 | # DynamoDB Local files 152 | 153 | .dynamodb/ 154 | 155 | # TernJS port file 156 | 157 | .tern-port 158 | 159 | # Stores VSCode versions used for testing VSCode extensions 160 | 161 | .vscode-test 162 | 163 | # yarn v2 164 | 165 | .yarn/cache 166 | .yarn/unplugged 167 | .yarn/build-state.yml 168 | .yarn/install-state.gz 169 | .pnp.* 170 | 171 | # IntelliJ based IDEs 172 | .idea 173 | 174 | # Finder (MacOS) folder config 175 | .DS_Store 176 | 177 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Alpha AI Technologies Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Foundry MCP Server 2 | 3 | A simple, lightweight and fast MCP (Model Context Protocol) server that provides Solidity development capabilities using the Foundry toolchain (Forge, Cast, and Anvil). 4 | 5 | ![Foundry MCP Demo](./assets/analysis_gif.gif) 6 | 7 | ## Overview 8 | 9 | This server connects LLM assistants to the Foundry ecosystem, enabling them to: 10 | 11 | - Interact with nodes (local Anvil instances or remote RPC endpoints) 12 | - Analyze smart contracts and blockchain data 13 | - Perform common EVM operations using Cast 14 | - Manage, deploy, and execute Solidity code and scripts 15 | - Work with a persistent Forge workspace 16 | 17 | ## Features 18 | 19 | ### Network Interaction 20 | 21 | - Start and manage local Anvil instances 22 | - Connect to any remote network (just specify the RPC) 23 | - Get network/chain information 24 | 25 | ### Contract Interaction 26 | 27 | - Call contract functions (read-only) 28 | - Send transactions to contracts (if `PRIVATE_KEY` is configured) 29 | - Get transaction receipts 30 | - Read contract storage 31 | - Analyze transaction traces 32 | - Retrieve contract ABIs and sources from block explorers 33 | 34 | ### Solidity Development 35 | 36 | - Maintain a dedicated Forge workspace 37 | - Create and edit Solidity files 38 | - Install dependencies 39 | - Run Forge scripts 40 | - Deploy contracts 41 | 42 | ### Utility Functions 43 | 44 | - Calculate contract addresses 45 | - Check contract bytecode size 46 | - Estimate gas costs 47 | - Convert between units (hex to decimals, etc.,) 48 | - Generate wallets 49 | - Get event logs 50 | - Lookup function and event signatures 51 | 52 | ## Usage 53 | 54 | The server is designed to be used as an MCP tool provider for MCP Clients. When connected to a client, it enables the clients(claude desktop, cursor, client, etc.,) to perform Solidity and onchain operations directly. 55 | 56 | 57 | #### Requirements 58 | 59 | - [Node.js v18+](https://nodejs.org) 60 | - [Foundry toolchain](https://book.getfoundry.sh/) (Forge, Cast, Anvil) 61 | 62 | ### Manual Setup 63 | 64 | 1. Ensure Foundry tools (Forge, Cast, Anvil) are installed on your system: 65 | ``` 66 | curl -L https://foundry.paradigm.xyz | bash 67 | foundryup 68 | ``` 69 | 2. Clone and build the server. 70 | 71 | ```sh 72 | bun i && bun build ./src/index.ts --outdir ./dist --target node 73 | 74 | 3. Update your client config (eg: Claude desktop): 75 | 76 | ```json 77 | "mcpServers": { 78 | "foundry": { 79 | "command": "node", 80 | "args": [ 81 | "path/to/foundry-mcp-server/dist/index.js" 82 | ], 83 | "env" :{ 84 | "PRIVATE_KEY": "0x1234", 85 | } 86 | } 87 | } 88 | ``` 89 | 90 | > [!NOTE] 91 | > `PRIVATE_KEY` is optional 92 | 93 | 94 | ### Setup using NPM Package 95 | - Coming soon 96 | 97 | #### Configuration 98 | 99 | The server supports the following environment variables: 100 | 101 | - `RPC_URL`: Default RPC URL to use when none is specified (optional) 102 | - `PRIVATE_KEY`: Private key to use for transactions (optional) 103 | 104 | > [!CAUTION] 105 | > Do not add keys with mainnet funds. Even though the code uses it safely, LLMs can hallicunate and send malicious transactions. 106 | > Use it only for testing/development purposes. DO NOT trust the LLM!! 107 | 108 | ### Workspace 109 | 110 | The server maintains a persistent Forge workspace at `~/.mcp-foundry-workspace` for all Solidity files, scripts, and dependencies. 111 | 112 | ## Tools 113 | 114 | ### Anvil 115 | 116 | - `anvil_start`: Start a new Anvil instance 117 | - `anvil_stop`: Stop a running Anvil instance 118 | - `anvil_status`: Check if Anvil is running and get its status 119 | 120 | ### Cast 121 | 122 | - `cast_call`: Call a contract function (read-only) 123 | - `cast_send`: Send a transaction to a contract function 124 | - `cast_balance`: Check the ETH balance of an address 125 | - `cast_receipt`: Get the transaction receipt 126 | - `cast_storage`: Read contract storage at a specific slot 127 | - `cast_run`: Run a published transaction in a local environment 128 | - `cast_logs`: Get logs by signature or topic 129 | - `cast_sig`: Get the selector for a function or event signature 130 | - `cast_4byte`: Lookup function or event signature from the 4byte directory 131 | - `cast_chain`: Get information about the current chain 132 | 133 | ### Forge 134 | 135 | - `forge_script`: Run a Forge script from the workspace 136 | - `install_dependency`: Install a dependency for the Forge workspace 137 | 138 | ### File Management 139 | 140 | - `create_solidity_file`: Create or update a Solidity file in the workspace 141 | - `read_file`: Read the content of a file from the workspace 142 | - `list_files`: List files in the workspace 143 | 144 | ### Utilities 145 | 146 | - `convert_eth_units`: Convert between EVM units (wei, gwei, hex) 147 | - `compute_address`: Compute the address of a contract that would be deployed 148 | - `contract_size`: Get the bytecode size of a deployed contract 149 | - `estimate_gas`: Estimate the gas cost of a transaction 150 | 151 | ## Usage in Claude Desktop App 🎯 152 | 153 | Once the installation is complete, and the Claude desktop app is configured, you must completely close and re-open the Claude desktop app to see the tavily-mcp server. You should see a hammer icon in the bottom left of the app, indicating available MCP tools, you can click on the hammer icon to see more details on the available tools. 154 | 155 | ![Alt text](./assets/tools.png) 156 | 157 | Now claude will have complete access to the foundry-mcp server. If you insert the below examples into the Claude desktop app, you should see the foundry-mcp server tools in action. 158 | 159 | ### Examples 160 | 161 | 1. **Transaction analysis**: 162 | ``` 163 | Can you analyze the transaction and explain what it does? 164 | https://etherscan.io/tx/0xcb73ad3116f19358e2e649d4dc801b7ae0590a47b8bb2e57a8e98b6daa5fb14b 165 | ``` 166 | 167 | 2. **Querying Balances**: 168 | ``` 169 | Query the mainnet ETH and USDT balances for the wallet 0x195F46025a6926968a1b3275822096eB12D97E70. 170 | ``` 171 | 3. **Sending transactions**: 172 | ``` 173 | Transfer 0.5 USDC to 0x195F46025a6926968a1b3275822096eB12D97E70 on Mainnet. 174 | ``` 175 | 176 | 4. **Deploying contracts/Running scripts**: 177 | ``` 178 | Deploy a mock ERC20 contract to a local anvil instance and name it "Fire Coin". 179 | ``` 180 | 181 | 182 | ## Acknowledgments ✨ 183 | 184 | - [Model Context Protocol](https://modelcontextprotocol.io) for the MCP specification 185 | - [Anthropic](https://anthropic.com) for Claude Desktop 186 | 187 | ## Disclaimer 188 | 189 | _The software is being provided as is. No guarantee, representation or warranty is being made, express or implied, as to the safety or correctness of the software. They have not been audited and as such there can be no assurance they will work as intended, and users may experience delays, failures, errors, omissions, loss of transmitted information or loss of funds. The creators are not liable for any of the foregoing. Users should proceed with caution and use at their own risk._ -------------------------------------------------------------------------------- /assets/analysis_gif.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PraneshASP/foundry-mcp-server/efac2b4bca065bf8afdd4d20f88ab6462d71f9f0/assets/analysis_gif.gif -------------------------------------------------------------------------------- /assets/send_tx.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PraneshASP/foundry-mcp-server/efac2b4bca065bf8afdd4d20f88ab6462d71f9f0/assets/send_tx.png -------------------------------------------------------------------------------- /assets/tools.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PraneshASP/foundry-mcp-server/efac2b4bca065bf8afdd4d20f88ab6462d71f9f0/assets/tools.png -------------------------------------------------------------------------------- /bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PraneshASP/foundry-mcp-server/efac2b4bca065bf8afdd4d20f88ab6462d71f9f0/bun.lockb -------------------------------------------------------------------------------- /funding.json: -------------------------------------------------------------------------------- 1 | { 2 | "opRetro": { 3 | "projectId": "0xa873dcdcc84e0022edd2f7d5cc8646667b87bf06609d506cd8e39dbd7412fa59" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "foundry-mcp-server", 3 | "module": "src/index.ts", 4 | "type": "module", 5 | "main": "dist/index.js", 6 | "scripts": { 7 | "build": "tsc", 8 | "start": "node dist/index.js" 9 | }, 10 | "devDependencies": { 11 | "@types/bun": "latest" 12 | }, 13 | "peerDependencies": { 14 | "typescript": "^5.0.0" 15 | }, 16 | "dependencies": { 17 | "@modelcontextprotocol/sdk": "^1.7.0", 18 | "dotenv": "^16.4.7", 19 | "zod": "^3.24.2" 20 | } 21 | } -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js"; 2 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; 3 | import { z } from "zod"; 4 | import { exec } from "child_process"; 5 | import { promisify } from "util"; 6 | import * as path from "path"; 7 | import * as fs from "fs/promises"; 8 | import * as os from "os"; 9 | import * as dotenv from "dotenv" 10 | 11 | dotenv.config(); 12 | 13 | const execAsync = promisify(exec); 14 | 15 | const server = new McpServer({ 16 | name: "Foundry MCP Server", 17 | version: "0.1.0" 18 | }, { 19 | instructions: ` 20 | This server provides tools for Solidity developers using the Foundry toolkit: 21 | - forge: Smart contract development framework 22 | - cast: EVM nodes RPC client and utility tool 23 | - anvil: Local EVM test node 24 | 25 | You can interact with local or remote EVM chains, deploy contracts, perform common operations, and analyze smart contract code. 26 | ` 27 | }); 28 | 29 | const FOUNDRY_WORKSPACE = path.join(os.homedir(), '.mcp-foundry-workspace'); 30 | 31 | async function ensureWorkspaceInitialized() { 32 | try { 33 | await fs.mkdir(FOUNDRY_WORKSPACE, { recursive: true }); 34 | 35 | const isForgeProject = await fs.access(path.join(FOUNDRY_WORKSPACE, 'foundry.toml')) 36 | .then(() => true) 37 | .catch(() => false); 38 | 39 | if (!isForgeProject) { 40 | await executeCommand(`cd ${FOUNDRY_WORKSPACE} && ${forgePath} init --no-git`); 41 | } 42 | 43 | return FOUNDRY_WORKSPACE; 44 | } catch (error) { 45 | console.error("Error initializing workspace:", error); 46 | throw error; 47 | } 48 | } 49 | 50 | const getBinaryPaths = () => { 51 | const homeDir = os.homedir(); 52 | 53 | const FOUNDRY_BIN = path.join(homeDir, '.foundry', 'bin'); 54 | 55 | return { 56 | castPath: path.join(FOUNDRY_BIN, "cast"), 57 | forgePath: path.join(FOUNDRY_BIN, "forge"), 58 | anvilPath: path.join(FOUNDRY_BIN, "anvil"), 59 | homeDir 60 | }; 61 | }; 62 | 63 | const { castPath, forgePath, anvilPath, homeDir } = getBinaryPaths(); 64 | 65 | const DEFAULT_RPC_URL = process.env.RPC_URL || "http://localhost:8545"; 66 | 67 | const FOUNDRY_NOT_INSTALLED_ERROR = "Foundry tools are not installed. Please install Foundry: https://book.getfoundry.sh/getting-started/installation"; 68 | 69 | 70 | async function checkFoundryInstalled() { 71 | try { 72 | await execAsync(`${forgePath} --version`); 73 | return true; 74 | } catch (error) { 75 | console.error("Foundry tools check failed:", error); 76 | return false; 77 | } 78 | } 79 | 80 | 81 | async function executeCommand(command) { 82 | try { 83 | const { stdout, stderr } = await execAsync(command); 84 | if (stderr && !stdout) { 85 | return { success: false, message: stderr }; 86 | } 87 | return { success: true, message: stdout }; 88 | } catch (error) { 89 | const errorMessage = error instanceof Error ? error.message : String(error); 90 | return { success: false, message: errorMessage }; 91 | } 92 | } 93 | 94 | async function resolveRpcUrl(rpcUrl) { 95 | if (!rpcUrl) { 96 | return DEFAULT_RPC_URL; 97 | } 98 | 99 | // Handle alias lookup in foundry config 100 | if (!rpcUrl.startsWith('http')) { 101 | try { 102 | // Try to find the RPC endpoint in foundry config 103 | const configPath = path.join(homeDir, '.foundry', 'config.toml'); 104 | const configExists = await fs.access(configPath).then(() => true).catch(() => false); 105 | 106 | if (configExists) { 107 | const configContent = await fs.readFile(configPath, 'utf8'); 108 | const rpcMatch = new RegExp(`\\[rpc_endpoints\\][\\s\\S]*?${rpcUrl}\\s*=\\s*["']([^"']+)["']`).exec(configContent); 109 | 110 | if (rpcMatch && rpcMatch[1]) { 111 | return rpcMatch[1]; 112 | } 113 | } 114 | } catch (error) { 115 | console.error("Error resolving RPC from config:", error); 116 | } 117 | } 118 | 119 | return rpcUrl; 120 | } 121 | 122 | 123 | async function getAnvilInfo() { 124 | try { 125 | const { stdout } = await execAsync('ps aux | grep anvil | grep -v grep'); 126 | if (!stdout) { 127 | return { running: false }; 128 | } 129 | 130 | const portMatch = stdout.match(/--port\s+(\d+)/); 131 | const port = portMatch ? portMatch[1] : '8545'; 132 | 133 | return { 134 | running: true, 135 | port, 136 | url: `http://localhost:${port}` 137 | }; 138 | } catch (error) { 139 | return { running: false }; 140 | } 141 | } 142 | 143 | //=================================================================================================== 144 | // RESOURCES 145 | //=================================================================================================== 146 | 147 | // Resource: Anvil status 148 | server.resource( 149 | "anvil_status", 150 | "anvil://status", 151 | async (uri) => { 152 | const info = await getAnvilInfo(); 153 | return { 154 | contents: [{ 155 | uri: uri.href, 156 | text: JSON.stringify(info, null, 2) 157 | }] 158 | }; 159 | } 160 | ) 161 | 162 | // Resource: Contract source from Etherscan 163 | server.resource( 164 | "contract_source", 165 | new ResourceTemplate("contract://{address}/source", { list: undefined }), 166 | async (uri, { address }) => { 167 | try { 168 | const command = `${castPath} etherscan-source ${address}`; 169 | const { success, message } = await executeCommand(command); 170 | 171 | if (success) { 172 | return { 173 | contents: [{ 174 | uri: uri.href, 175 | text: message 176 | }] 177 | }; 178 | } else { 179 | return { 180 | contents: [{ 181 | uri: uri.href, 182 | text: JSON.stringify({ error: "Could not retrieve contract source", details: message }) 183 | }] 184 | }; 185 | } 186 | } catch (error) { 187 | return { 188 | contents: [{ 189 | uri: uri.href, 190 | text: JSON.stringify({ error: "Failed to retrieve contract source" }) 191 | }] 192 | }; 193 | } 194 | } 195 | ); 196 | 197 | //=================================================================================================== 198 | // CAST TOOLS 199 | //=================================================================================================== 200 | 201 | // Tool: Call a contract function (read-only) 202 | server.tool( 203 | "cast_call", 204 | "Call a contract function (read-only)", 205 | { 206 | contractAddress: z.string().describe("Address of the contract"), 207 | functionSignature: z.string().describe("Function signature (e.g., 'balanceOf(address)')"), 208 | args: z.array(z.string()).optional().describe("Function arguments"), 209 | rpcUrl: z.string().optional().describe("JSON-RPC URL (default: http://localhost:8545)"), 210 | blockNumber: z.string().optional().describe("Block number (e.g., 'latest', 'earliest', or a number)"), 211 | from: z.string().optional().describe("Address to perform the call as") 212 | }, 213 | async ({ contractAddress, functionSignature, args = [], rpcUrl, blockNumber, from }) => { 214 | const installed = await checkFoundryInstalled(); 215 | if (!installed) { 216 | return { 217 | content: [{ type: "text", text: FOUNDRY_NOT_INSTALLED_ERROR }], 218 | isError: true 219 | }; 220 | } 221 | 222 | const resolvedRpcUrl = await resolveRpcUrl(rpcUrl); 223 | let command = `${castPath} call ${contractAddress} "${functionSignature}"`; 224 | 225 | if (args.length > 0) { 226 | command += " " + args.join(" "); 227 | } 228 | 229 | if (resolvedRpcUrl) { 230 | command += ` --rpc-url "${resolvedRpcUrl}"`; 231 | } 232 | 233 | if (blockNumber) { 234 | command += ` --block ${blockNumber}`; 235 | } 236 | 237 | if (from) { 238 | command += ` --from ${from}`; 239 | } 240 | 241 | const result = await executeCommand(command); 242 | 243 | let formattedOutput = result.message; 244 | if (result.success) { 245 | // Try to detect arrays and format them better 246 | if (formattedOutput.includes('\n') && !formattedOutput.includes('Error')) { 247 | formattedOutput = formattedOutput.split('\n') 248 | .map(line => line.trim()) 249 | .filter(line => line.length > 0) 250 | .join('\n'); 251 | } 252 | } 253 | 254 | return { 255 | content: [{ 256 | type: "text", 257 | text: result.success 258 | ? `Call to ${contractAddress}.${functionSignature.split('(')[0]} result:\n${formattedOutput}` 259 | : `Call failed: ${result.message}` 260 | }], 261 | isError: !result.success 262 | }; 263 | } 264 | ); 265 | 266 | // Tool: Send a transaction to a contract function 267 | server.tool( 268 | "cast_send", 269 | "Send a transaction to a contract function", 270 | { 271 | contractAddress: z.string().describe("Address of the contract"), 272 | functionSignature: z.string().describe("Function signature (e.g., 'transfer(address,uint256)')"), 273 | args: z.array(z.string()).optional().describe("Function arguments"), 274 | from: z.string().optional().describe("Sender address or private key"), 275 | value: z.string().optional().describe("Ether value to send with the transaction (in wei)"), 276 | rpcUrl: z.string().optional().describe("JSON-RPC URL (default: http://localhost:8545)"), 277 | gasLimit: z.string().optional().describe("Gas limit for the transaction"), 278 | gasPrice: z.string().optional().describe("Gas price for the transaction (in wei)"), 279 | confirmations: z.number().optional().describe("Number of confirmations to wait for") 280 | }, 281 | async ({ contractAddress, functionSignature, args = [], from, value, rpcUrl, gasLimit, gasPrice, confirmations }) => { 282 | const installed = await checkFoundryInstalled(); 283 | if (!installed) { 284 | return { 285 | content: [{ type: "text", text: FOUNDRY_NOT_INSTALLED_ERROR }], 286 | isError: true 287 | }; 288 | } 289 | 290 | const resolvedRpcUrl = await resolveRpcUrl(rpcUrl); 291 | const privateKey = process.env.PRIVATE_KEY; 292 | let command = `${castPath} send ${contractAddress} "${functionSignature}" --private-key ${[privateKey]}`; 293 | 294 | if (args.length > 0) { 295 | command += " " + args.join(" "); 296 | } 297 | 298 | if (from) { 299 | command += ` --from ${from}`; 300 | } 301 | 302 | if (value) { 303 | command += ` --value ${value}`; 304 | } 305 | 306 | if (resolvedRpcUrl) { 307 | command += ` --rpc-url "${resolvedRpcUrl}"`; 308 | } 309 | 310 | if (gasLimit) { 311 | command += ` --gas-limit ${gasLimit}`; 312 | } 313 | 314 | if (gasPrice) { 315 | command += ` --gas-price ${gasPrice}`; 316 | } 317 | 318 | if (confirmations) { 319 | command += ` --confirmations ${confirmations}`; 320 | } 321 | 322 | const result = await executeCommand(command); 323 | 324 | return { 325 | content: [{ 326 | type: "text", 327 | text: result.success 328 | ? `Transaction sent successfully:\n${result.message}` 329 | : `Transaction failed: ${result.message}` 330 | }], 331 | isError: !result.success 332 | }; 333 | } 334 | ); 335 | 336 | // Tool: Check the ETH balance of an address 337 | server.tool( 338 | "cast_balance", 339 | "Check the ETH balance of an address", 340 | { 341 | address: z.string().describe("Ethereum address to check balance for"), 342 | rpcUrl: z.string().optional().describe("JSON-RPC URL (default: http://localhost:8545)"), 343 | blockNumber: z.string().optional().describe("Block number (e.g., 'latest', 'earliest', or a number)"), 344 | formatEther: z.boolean().optional().describe("Format the balance in Ether (default: wei)") 345 | }, 346 | async ({ address, rpcUrl, blockNumber, formatEther = false }) => { 347 | const installed = await checkFoundryInstalled(); 348 | if (!installed) { 349 | return { 350 | content: [{ type: "text", text: FOUNDRY_NOT_INSTALLED_ERROR }], 351 | isError: true 352 | }; 353 | } 354 | 355 | const resolvedRpcUrl = await resolveRpcUrl(rpcUrl); 356 | let command = `${castPath} balance ${address}`; 357 | 358 | if (resolvedRpcUrl) { 359 | command += ` --rpc-url "${resolvedRpcUrl}"`; 360 | } 361 | 362 | if (blockNumber) { 363 | command += ` --block ${blockNumber}`; 364 | } 365 | 366 | if (formatEther) { 367 | command += " --ether"; 368 | } 369 | 370 | const result = await executeCommand(command); 371 | const unit = formatEther ? "ETH" : "wei"; 372 | 373 | return { 374 | content: [{ 375 | type: "text", 376 | text: result.success 377 | ? `Balance of ${address}: ${result.message.trim()} ${unit}` 378 | : `Failed to get balance: ${result.message}` 379 | }], 380 | isError: !result.success 381 | }; 382 | } 383 | ); 384 | 385 | // Tool: Get transaction receipt 386 | server.tool( 387 | "cast_receipt", 388 | "Get the transaction receipt", 389 | { 390 | txHash: z.string().describe("Transaction hash"), 391 | rpcUrl: z.string().optional().describe("JSON-RPC URL (default: http://localhost:8545)"), 392 | confirmations: z.number().optional().describe("Number of confirmations to wait for"), 393 | field: z.string().optional().describe("Specific field to extract (e.g., 'blockNumber', 'status')") 394 | }, 395 | async ({ txHash, rpcUrl, confirmations, field }) => { 396 | const installed = await checkFoundryInstalled(); 397 | if (!installed) { 398 | return { 399 | content: [{ type: "text", text: FOUNDRY_NOT_INSTALLED_ERROR }], 400 | isError: true 401 | }; 402 | } 403 | 404 | const resolvedRpcUrl = await resolveRpcUrl(rpcUrl); 405 | let command = `${castPath} receipt ${txHash}`; 406 | 407 | if (resolvedRpcUrl) { 408 | command += ` --rpc-url "${resolvedRpcUrl}"`; 409 | } 410 | 411 | if (confirmations) { 412 | command += ` --confirmations ${confirmations}`; 413 | } 414 | 415 | if (field) { 416 | command += ` ${field}`; 417 | } 418 | 419 | const result = await executeCommand(command); 420 | 421 | return { 422 | content: [{ 423 | type: "text", 424 | text: result.success 425 | ? `Transaction receipt for ${txHash}${field ? ` (${field})` : ""}:\n${result.message}` 426 | : `Failed to get receipt: ${result.message}` 427 | }], 428 | isError: !result.success 429 | }; 430 | } 431 | ); 432 | 433 | // Tool: Read a contract's storage at a given slot 434 | server.tool( 435 | "cast_storage", 436 | "Read contract storage at a specific slot", 437 | { 438 | address: z.string().describe("Contract address"), 439 | slot: z.string().describe("Storage slot to read"), 440 | rpcUrl: z.string().optional().describe("JSON-RPC URL (default: http://localhost:8545)"), 441 | blockNumber: z.string().optional().describe("Block number (e.g., 'latest', 'earliest', or a number)") 442 | }, 443 | async ({ address, slot, rpcUrl, blockNumber }) => { 444 | const installed = await checkFoundryInstalled(); 445 | if (!installed) { 446 | return { 447 | content: [{ type: "text", text: FOUNDRY_NOT_INSTALLED_ERROR }], 448 | isError: true 449 | }; 450 | } 451 | 452 | const resolvedRpcUrl = await resolveRpcUrl(rpcUrl); 453 | let command = `${castPath} storage ${address} ${slot}`; 454 | 455 | if (resolvedRpcUrl) { 456 | command += ` --rpc-url "${resolvedRpcUrl}"`; 457 | } 458 | 459 | if (blockNumber) { 460 | command += ` --block ${blockNumber}`; 461 | } 462 | 463 | const result = await executeCommand(command); 464 | 465 | return { 466 | content: [{ 467 | type: "text", 468 | text: result.success 469 | ? `Storage at ${address} slot ${slot}: ${result.message.trim()}` 470 | : `Failed to read storage: ${result.message}` 471 | }], 472 | isError: !result.success 473 | }; 474 | } 475 | ); 476 | 477 | // Tool: Run a published transaction in a local environment and print the trace 478 | server.tool( 479 | "cast_run", 480 | "Runs a published transaction in a local environment and prints the trace", 481 | { 482 | txHash: z.string().describe("Transaction hash to replay"), 483 | rpcUrl: z.string().describe("JSON-RPC URL"), 484 | quick: z.boolean().optional().describe("Execute the transaction only with the state from the previous block"), 485 | debug: z.boolean().optional().describe("Open the transaction in the debugger"), 486 | labels: z.array(z.string()).optional().describe("Label addresses in the trace (format:
: