UNPKG

arbitrum-mcp-tools

Version:

A comprehensive collection of Model Context Protocol (MCP) tools for interacting with the Arbitrum blockchain. Enables AI assistants like Claude, Cursor, Windsurf, VS Code, Gemini CLI, and OpenAI Codex to perform blockchain operations including account an

721 lines (720 loc) 30 kB
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; import { z } from "zod"; import { execSync } from "child_process"; import { handleError } from "../common.js"; import * as fs from "fs"; // Helper function to execute cargo stylus commands with directory handling function executeCargoStylusCommand(command_1, cwd_1) { return __awaiter(this, arguments, void 0, function* (command, cwd, args = []) { try { const fullCommand = `cargo stylus ${command} ${args.join(" ")}`; const options = { encoding: "utf8", }; if (cwd) { // Check if directory exists if (!fs.existsSync(cwd)) { fs.mkdirSync(cwd, { recursive: true }); } options.cwd = cwd; } const output = execSync(fullCommand, options); return output; } catch (error) { if (error.stdout) { return error.stdout; } throw new Error(`Failed to execute cargo stylus command: ${error.message}`); } }); } export function registerStylusTools(server) { // 1. Create a new Stylus project server.tool("createStylusProject", "Create a new Cargo Stylus project", { projectName: z.string().describe("Project name"), path: z .string() .optional() .describe("Path to create the project (optional)"), }, (_a) => __awaiter(this, [_a], void 0, function* ({ projectName, path: projectPath }) { try { // For 'new' command, we need to be in the parent directory where we want to create the project const output = yield executeCargoStylusCommand(`new ${projectName}`, projectPath); return { content: [ { type: "text", text: `Project created successfully:\n\n${output}`, }, ], }; } catch (error) { return { content: [ { type: "text", text: `Error creating Stylus project: ${handleError(error)}`, }, ], }; } })); // 2. Initialize a Stylus project server.tool("initStylusProject", "Initialize a Stylus project in the current directory", { path: z .string() .optional() .describe("Path to initialize the project (optional)"), }, (_a) => __awaiter(this, [_a], void 0, function* ({ path: projectPath }) { try { // For 'init' command, we need to be in the directory where we want to initialize the project const output = yield executeCargoStylusCommand("init", projectPath); return { content: [ { type: "text", text: `Project initialized successfully:\n\n${output}`, }, ], }; } catch (error) { return { content: [ { type: "text", text: `Error initializing Stylus project: ${handleError(error)}`, }, ], }; } })); // 3. Export a Solidity ABI server.tool("exportStylusAbi", "Export a Solidity ABI for a Stylus contract", { path: z .string() .optional() .describe("Path to the Stylus project (optional)"), }, (_a) => __awaiter(this, [_a], void 0, function* ({ path: projectPath }) { try { // We need to be in the project directory const output = yield executeCargoStylusCommand("export-abi", projectPath); return { content: [ { type: "text", text: `ABI exported successfully:\n\n${output}`, }, ], }; } catch (error) { return { content: [ { type: "text", text: `Error exporting Stylus ABI: ${handleError(error)}`, }, ], }; } })); // 4. Check a Stylus contract server.tool("checkStylusContract", "Check if a Stylus contract is valid for deployment", { path: z .string() .optional() .describe("Path to the Stylus project (optional)"), }, (_a) => __awaiter(this, [_a], void 0, function* ({ path: projectPath }) { try { // We need to be in the project directory const output = yield executeCargoStylusCommand("check", projectPath); return { content: [ { type: "text", text: `Contract check results:\n\n${output}`, }, ], }; } catch (error) { return { content: [ { type: "text", text: `Error checking Stylus contract: ${handleError(error)}`, }, ], }; } })); // 5. Deploy a Stylus contract server.tool("deployStylusContract", "Deploy a Stylus contract to the Arbitrum network. Uses STYLUS_PRIVATE_KEY, STYLUS_PRIVATE_KEY_PATH, or STYLUS_KEYSTORE_PATH from environment variables for authentication.", { endpoint: z.string().describe("RPC endpoint URL"), path: z .string() .optional() .describe("Path to the Stylus project (optional)"), estimateGas: z .boolean() .optional() .describe("Only estimate gas instead of deploying (optional)"), }, (_a) => __awaiter(this, [_a], void 0, function* ({ endpoint, path: projectPath, estimateGas }) { try { const args = [`--endpoint=${endpoint}`]; // Get authentication method from environment variables const privateKey = process.env.STYLUS_PRIVATE_KEY; const privateKeyPath = process.env.STYLUS_PRIVATE_KEY_PATH; const keystorePath = process.env.STYLUS_KEYSTORE_PATH; // At least one authentication method must be provided if (privateKey) { args.push(`--private-key=${privateKey}`); } else if (privateKeyPath) { args.push(`--private-key-path=${privateKeyPath}`); } else if (keystorePath) { args.push(`--keystore-path=${keystorePath}`); } else { throw new Error("Authentication required: Set one of STYLUS_PRIVATE_KEY, STYLUS_PRIVATE_KEY_PATH, or STYLUS_KEYSTORE_PATH environment variables"); } if (estimateGas) { args.push("--estimate-gas"); } // We need to be in the project directory const output = yield executeCargoStylusCommand("deploy", projectPath, args); return { content: [ { type: "text", text: `${estimateGas ? "Gas estimation" : "Deployment"} results:\n\n${output}`, }, ], }; } catch (error) { return { content: [ { type: "text", text: `Error ${estimateGas ? "estimating gas" : "deploying"} Stylus contract: ${handleError(error)}`, }, ], }; } })); // 5.b Deploy multiple Stylus contracts server.tool("deployMultipleStylusContracts", "Deploy multiple Stylus contracts to the Arbitrum network in sequence. Uses STYLUS_PRIVATE_KEY, STYLUS_PRIVATE_KEY_PATH, or STYLUS_KEYSTORE_PATH from environment variables for authentication.", { endpoint: z.string().describe("RPC endpoint URL"), projectPaths: z .array(z.string()) .describe("Array of paths to Stylus projects that should be deployed"), estimateGas: z .boolean() .optional() .describe("Only estimate gas instead of deploying (optional)"), }, (_a) => __awaiter(this, [_a], void 0, function* ({ endpoint, projectPaths, estimateGas }) { // Aggregate outputs for all deployments const outputs = []; // Prepare common CLI arguments (endpoint + auth + optional estimate flag) const baseArgs = [`--endpoint=${endpoint}`]; // Determine authentication method once const privateKey = process.env.STYLUS_PRIVATE_KEY; const privateKeyPath = process.env.STYLUS_PRIVATE_KEY_PATH; const keystorePath = process.env.STYLUS_KEYSTORE_PATH; if (privateKey) { baseArgs.push(`--private-key=${privateKey}`); } else if (privateKeyPath) { baseArgs.push(`--private-key-path=${privateKeyPath}`); } else if (keystorePath) { baseArgs.push(`--keystore-path=${keystorePath}`); } else { return { content: [ { type: "text", text: "Authentication required: Set one of STYLUS_PRIVATE_KEY, STYLUS_PRIVATE_KEY_PATH, or STYLUS_KEYSTORE_PATH environment variables", }, ], }; } if (estimateGas) { baseArgs.push("--estimate-gas"); } // Iterate over each project path and attempt deployment for (const projectPath of projectPaths) { try { const output = yield executeCargoStylusCommand("deploy", projectPath, baseArgs); outputs.push(`Project: ${projectPath}\n${estimateGas ? "Gas estimation" : "Deployment"} results:\n${output}`); } catch (error) { outputs.push(`Project: ${projectPath}\nError ${estimateGas ? "estimating gas" : "deploying"} Stylus contract: ${handleError(error)}`); } } return { content: [ { type: "text", text: outputs.join("\n\n-----------------------------\n\n"), }, ], }; })); // 6. Verify a Stylus contract deployment server.tool("verifyStylusContract", "Verify the deployment of a Stylus contract", { deploymentTx: z.string().describe("Deployment transaction hash"), endpoint: z.string().optional().describe("RPC endpoint URL (optional)"), path: z .string() .optional() .describe("Path to the Stylus project (optional)"), }, (_a) => __awaiter(this, [_a], void 0, function* ({ deploymentTx, endpoint, path: projectPath }) { try { const args = [`--deployment-tx=${deploymentTx}`]; if (endpoint) { args.push(`--endpoint=${endpoint}`); } // We need to be in the project directory const output = yield executeCargoStylusCommand("verify", projectPath, args); return { content: [ { type: "text", text: `Verification results:\n\n${output}`, }, ], }; } catch (error) { return { content: [ { type: "text", text: `Error verifying Stylus contract: ${handleError(error)}`, }, ], }; } })); // 7. Activate a deployed Stylus contract server.tool("activateStylusContract", "Activate an already deployed Stylus contract. Uses STYLUS_PRIVATE_KEY, STYLUS_PRIVATE_KEY_PATH, or STYLUS_KEYSTORE_PATH from environment variables for authentication.", { contractAddress: z.string().describe("Deployed contract address"), endpoint: z.string().describe("RPC endpoint URL"), path: z .string() .optional() .describe("Path to the Stylus project (optional)"), }, (_a) => __awaiter(this, [_a], void 0, function* ({ contractAddress, endpoint, path: projectPath }) { try { const args = [`--address=${contractAddress}`, `--endpoint=${endpoint}`]; // Get authentication method from environment variables const privateKey = process.env.STYLUS_PRIVATE_KEY; const privateKeyPath = process.env.STYLUS_PRIVATE_KEY_PATH; const keystorePath = process.env.STYLUS_KEYSTORE_PATH; // At least one authentication method must be provided if (privateKey) { args.push(`--private-key=${privateKey}`); } else if (privateKeyPath) { args.push(`--private-key-path=${privateKeyPath}`); } else if (keystorePath) { args.push(`--keystore-path=${keystorePath}`); } else { throw new Error("Authentication required: Set one of STYLUS_PRIVATE_KEY, STYLUS_PRIVATE_KEY_PATH, or STYLUS_KEYSTORE_PATH environment variables"); } // We need to be in the project directory const output = yield executeCargoStylusCommand("activate", projectPath, args); return { content: [ { type: "text", text: `Activation results:\n\n${output}`, }, ], }; } catch (error) { return { content: [ { type: "text", text: `Error activating Stylus contract: ${handleError(error)}`, }, ], }; } })); // 8. Cache a Stylus contract server.tool("cacheStylusContract", "Cache a contract using the Stylus CacheManager. Uses STYLUS_PRIVATE_KEY, STYLUS_PRIVATE_KEY_PATH, or STYLUS_KEYSTORE_PATH from environment variables for authentication.", { subcommand: z .enum(["bid", "status", "suggest-bid", "help"]) .describe("Cache subcommand to execute"), contractAddress: z.string().describe("Contract address to cache"), endpoint: z.string().describe("RPC endpoint URL"), path: z .string() .optional() .describe("Path to the Stylus project (optional)"), bidAmount: z .string() .optional() .describe("Bid amount (required for 'bid' subcommand)"), }, (_a) => __awaiter(this, [_a], void 0, function* ({ subcommand, contractAddress, endpoint, path: projectPath, bidAmount, }) { try { const args = [`--endpoint=${endpoint}`]; // Only add authentication for subcommands that require it (bid) if (subcommand === "bid") { // Get authentication method from environment variables const privateKey = process.env.STYLUS_PRIVATE_KEY; const privateKeyPath = process.env.STYLUS_PRIVATE_KEY_PATH; const keystorePath = process.env.STYLUS_KEYSTORE_PATH; // At least one authentication method must be provided for bid if (privateKey) { args.push(`--private-key=${privateKey}`); } else if (privateKeyPath) { args.push(`--private-key-path=${privateKeyPath}`); } else if (keystorePath) { args.push(`--keystore-path=${keystorePath}`); } else { throw new Error("Authentication required for 'bid' subcommand: Set one of STYLUS_PRIVATE_KEY, STYLUS_PRIVATE_KEY_PATH, or STYLUS_KEYSTORE_PATH environment variables"); } } // Add positional arguments in the correct order based on subcommand if (subcommand === "bid") { if (!bidAmount) { throw new Error("Bid amount is required for 'bid' subcommand"); } // For bid: cargo stylus cache bid --endpoint <ENDPOINT> --private-key <KEY> <ADDRESS> <BID> args.push(contractAddress); args.push(bidAmount); } else if (subcommand === "suggest-bid") { // For suggest-bid: cargo stylus cache suggest-bid --endpoint <ENDPOINT> <ADDRESS> args.push(contractAddress); } else if (subcommand === "status") { // For status: cargo stylus cache status --endpoint <ENDPOINT> // No additional arguments needed according to the help output } else if (subcommand === "help") { // For help: cargo stylus cache help // No additional arguments needed } // We need to be in the project directory const output = yield executeCargoStylusCommand(`cache ${subcommand}`, projectPath, args); return { content: [ { type: "text", text: `Cache ${subcommand} results:\n\n${output}`, }, ], }; } catch (error) { return { content: [ { type: "text", text: `Error caching Stylus contract: ${handleError(error)}`, }, ], }; } })); server.tool("prepareStylusCgen", "Prepare ABI JSON for cargo stylus cgen command", { projectPath: z.string().describe("The project path"), outputPath: z .string() .describe("The output file for cgen-compatible JSON"), rustFeatures: z .string() .optional() .describe("Rust crate's features list. Required to include feature specific ABI"), }, (_a) => __awaiter(this, [_a], void 0, function* ({ projectPath, outputPath, rustFeatures }) { try { const args = ["--json"]; if (rustFeatures) { args.push(`--rust-features="${rustFeatures}"`); } // Export ABI first const abiOutput = yield executeCargoStylusCommand("export-abi", projectPath, args); // Parse the output to extract contract name and ABI const lines = abiOutput.split("\n"); let abiJson = ""; let extractedContractName = "Contract"; // default fallback for (let i = 0; i < lines.length; i++) { const line = lines[i]; // Look for contract name in header if (line.includes("=======") && line.includes(":")) { const headerMatch = line.match(/=======\s*<[^>]*>:(\w+)\s*=======/); if (headerMatch) { extractedContractName = headerMatch[1]; } } // Look for ABI JSON (starts with [ and contains "type") if (line.trim().startsWith("[") && line.includes('"type"')) { abiJson = line.trim(); break; } } if (!abiJson) { throw new Error("Could not extract ABI JSON from export-abi output"); } // Parse and validate ABI let abi; try { abi = JSON.parse(abiJson); } catch (parseError) { throw new Error(`Invalid ABI JSON: ${parseError}`); } // Create cgen-compatible structure const cgenFormat = { contracts: { [`${extractedContractName}.sol`]: { [extractedContractName]: { abi: abi, }, }, }, }; const formattedJson = JSON.stringify(cgenFormat, null, 2); // Write to output file const fs = yield import("fs"); yield fs.promises.writeFile(outputPath, formattedJson); return { content: [ { type: "text", text: `Successfully prepared cgen-compatible ABI JSON for contract "${extractedContractName}".\nOutput saved to: ${outputPath}\n\nYou can now run:\ncargo stylus cgen ${outputPath} ./generated/\n\nGenerated structure:\n${formattedJson}`, }, ], }; } catch (error) { return { content: [ { type: "text", text: `Error preparing cgen ABI: ${handleError(error)}`, }, ], }; } })); // 9. Generate C code bindings server.tool("generateStylusBindings", "Generate C code bindings for a Stylus contract from project source", { projectPath: z.string().describe("The Stylus project path"), outDir: z .string() .describe("Output directory for the generated C bindings"), rustFeatures: z .string() .optional() .describe("Rust crate's features list for ABI generation"), abiOutputPath: z .string() .optional() .describe("Path to save the prepared ABI JSON (optional, temp file used if not provided)"), keepAbiFile: z .boolean() .optional() .default(false) .describe("Keep the generated ABI JSON file after C binding generation"), }, (_a) => __awaiter(this, [_a], void 0, function* ({ projectPath, outDir, rustFeatures, abiOutputPath, keepAbiFile, }) { try { let generatedAbiPath = ""; // Generate ABI from the project const args = ["--json"]; if (rustFeatures) { args.push(`--rust-features="${rustFeatures}"`); } // Export ABI first const abiOutput = yield executeCargoStylusCommand("export-abi", projectPath, args); // Parse the output to extract contract name and ABI const lines = abiOutput.split("\n"); let abiJson = ""; let extractedContractName = "Contract"; // default fallback for (let i = 0; i < lines.length; i++) { const line = lines[i]; // Look for contract name in header if (line.includes("=======") && line.includes(":")) { const headerMatch = line.match(/=======\s*<[^>]*>:(\w+)\s*=======/); if (headerMatch) { extractedContractName = headerMatch[1]; } } // Look for ABI JSON (starts with [ and contains "type") if (line.trim().startsWith("[") && line.includes('"type"')) { abiJson = line.trim(); break; } } if (!abiJson) { throw new Error("Could not extract ABI JSON from export-abi output"); } // Parse and validate ABI let abi; try { abi = JSON.parse(abiJson); } catch (parseError) { throw new Error(`Invalid ABI JSON: ${parseError}`); } // Create cgen-compatible structure const cgenFormat = { contracts: { [`${extractedContractName}.sol`]: { [extractedContractName]: { abi: abi, }, }, }, }; const formattedJson = JSON.stringify(cgenFormat, null, 2); // Determine output path for ABI JSON const fs = yield import("fs"); const path = yield import("path"); if (abiOutputPath) { generatedAbiPath = abiOutputPath; } else { // Use temp file const os = yield import("os"); generatedAbiPath = path.join(os.tmpdir(), `${extractedContractName}_cgen_abi.json`); } // Write ABI JSON file yield fs.promises.writeFile(generatedAbiPath, formattedJson); // Now generate C bindings using the ABI JSON const cgenOutput = yield executeCargoStylusCommand(`cgen ${generatedAbiPath} ${outDir}`); // Clean up temp file if not keeping it if (generatedAbiPath && !keepAbiFile && !abiOutputPath) { try { const fs = yield import("fs"); yield fs.promises.unlink(generatedAbiPath); } catch (cleanupError) { // Non-fatal error, just log it console.warn(`Warning: Could not clean up temp ABI file: ${cleanupError}`); } } let resultMessage = `C bindings generated successfully!\n\nOutput directory: ${outDir}\n\n${cgenOutput}`; if (keepAbiFile || abiOutputPath) { resultMessage += `\n\nABI JSON saved to: ${generatedAbiPath}`; } resultMessage += `\n\nGenerated from project: ${projectPath}`; return { content: [ { type: "text", text: resultMessage, }, ], }; } catch (error) { return { content: [ { type: "text", text: `Error generating C bindings: ${handleError(error)}`, }, ], }; } })); // 10. Replay a transaction in GDB server.tool("replayStylusTransaction", "Replay a Stylus transaction in GDB debugger", { txHash: z.string().describe("Transaction hash to replay"), endpoint: z.string().optional().describe("RPC endpoint URL (optional)"), path: z .string() .optional() .describe("Path to the Stylus project (optional)"), }, (_a) => __awaiter(this, [_a], void 0, function* ({ txHash, endpoint, path: projectPath }) { try { const args = [`--tx=${txHash}`]; if (endpoint) { args.push(`--endpoint=${endpoint}`); } args.push("--use-native-tracer"); // We need to be in the project directory const output = yield executeCargoStylusCommand("replay", projectPath, args); return { content: [ { type: "text", text: `Transaction replay results:\n\n${output}`, }, ], }; } catch (error) { return { content: [ { type: "text", text: `Error replaying transaction: ${handleError(error)}`, }, ], }; } })); // 11. Trace a transaction server.tool("traceStylusTransaction", "Trace a Stylus transaction", { txHash: z.string().describe("Transaction hash to trace"), endpoint: z.string().optional().describe("RPC endpoint URL (optional)"), path: z .string() .optional() .describe("Path to the Stylus project (optional)"), }, (_a) => __awaiter(this, [_a], void 0, function* ({ txHash, endpoint, path: projectPath }) { try { const args = [`--tx=${txHash}`]; if (endpoint) { args.push(`--endpoint=${endpoint}`); } // We need to be in the project directory const output = yield executeCargoStylusCommand("trace", projectPath, args); return { content: [ { type: "text", text: `Transaction trace results:\n\n${output}`, }, ], }; } catch (error) { return { content: [ { type: "text", text: `Error tracing transaction: ${handleError(error)}`, }, ], }; } })); }