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
JavaScript
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)}`,
},
],
};
}
}));
}