import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
import { config } from 'dotenv';
import { EtherscanService, SupportedNetwork, NETWORK_CHAIN_IDS } from './services/etherscanService.js';
import {
getAllNetworks,
getChainId,
isValidNetwork,
type NetworkSlug,
NETWORKS
} from './config/networks.js';
import { z } from 'zod';
// Load environment variables
config();
const apiKey = process.env.ETHERSCAN_API_KEY;
const network = (process.env.NETWORK || 'mainnet') as SupportedNetwork;
if (!apiKey) {
throw new Error('ETHERSCAN_API_KEY environment variable is required');
}
// Initialize Etherscan service
const etherscanService = new EtherscanService(apiKey, network);
// Create server instance
const server = new Server(
{
name: "mcp-etherscan-server",
version: "1.0.0",
},
{
capabilities: {
tools: {},
},
}
);
// Get all network slugs for dynamic schema validation
const networkSlugs = Object.keys(NETWORKS) as [NetworkSlug, ...NetworkSlug[]];
// Define schemas for validation
const NetworkSchema = z.union([
z.enum(networkSlugs),
z.number().int().positive()
]).optional();
const BlockTypeSchema = z.enum(['blocks', 'uncles'] as const);
const SortOrderSchema = z.enum(['asc', 'desc'] as const);
const AddressSchema = z.object({
address: z.string().regex(/^0x[a-fA-F0-9]{40}$/, 'Invalid Ethereum address format'),
network: NetworkSchema.optional(),
});
const TransactionHistorySchema = z.object({
address: z.string().regex(/^0x[a-fA-F0-9]{40}$/, 'Invalid Ethereum address format'),
network: NetworkSchema.optional(),
limit: z.number().min(1).max(100).optional(),
});
const TokenTransferSchema = z.object({
address: z.string().regex(/^0x[a-fA-F0-9]{40}$/, 'Invalid Ethereum address format'),
network: NetworkSchema.optional(),
limit: z.number().min(1).max(100).optional(),
});
const ContractSchema = z.object({
address: z.string().regex(/^0x[a-fA-F0-9]{40}$/, 'Invalid Ethereum address format'),
network: NetworkSchema.optional(),
});
const MinedBlocksSchema = z.object({
address: z.string().regex(/^0x[a-fA-F0-9]{40}$/, 'Invalid Ethereum address format'),
network: NetworkSchema.optional(),
blockType: BlockTypeSchema.optional(),
page: z.number().min(1).optional(),
offset: z.number().min(1).max(100).optional(),
startBlock: z.number().min(0).optional(),
endBlock: z.number().min(0).optional(),
});
const InternalTransactionsSchema = z.object({
address: z.string().regex(/^0x[a-fA-F0-9]{40}$/, 'Invalid Ethereum address format'),
network: NetworkSchema.optional(),
page: z.number().min(1).optional(),
offset: z.number().min(1).max(100).optional(),
startBlock: z.number().min(0).optional(),
endBlock: z.number().min(0).optional(),
});
const BlockSchema = z.object({
blockNumber: z.number().min(0),
network: NetworkSchema.optional(),
});
const ContractCreationSchema = z.object({
address: z.string().regex(/^0x[a-fA-F0-9]{40}$/),
network: NetworkSchema.optional(),
});
const VerifyContractSchema = z.object({
sourceCode: z.string(),
contractAddress: z.string().regex(/^0x[a-fA-F0-9]{40}$/),
contractName: z.string(),
compilerVersion: z.string(),
optimization: z.boolean().optional(),
optimizationRuns: z.number().min(1).optional(),
constructorArguments: z.string().optional(),
evmVersion: z.string().optional(),
licenseType: z.string().optional(),
network: NetworkSchema.optional(),
});
const CheckVerificationSchema = z.object({
guid: z.string(),
network: NetworkSchema.optional(),
});
const VerifyProxySchema = z.object({
address: z.string().regex(/^0x[a-fA-F0-9]{40}$/),
expectedImplementation: z.string().regex(/^0x[a-fA-F0-9]{40}$/).optional(),
network: NetworkSchema.optional(),
});
const VerifiedContractsSchema = z.object({
page: z.number().min(1).optional(),
offset: z.number().min(1).max(100).optional(),
sortBy: z.string().optional(),
network: NetworkSchema,
});
// Helper function to create service instance based on network parameter
function getServiceForRequest(args: Record<string, unknown>): EtherscanService {
if (!apiKey) {
throw new Error('ETHERSCAN_API_KEY environment variable is required');
}
if (args && 'network' in args && args.network !== undefined) {
const networkOrChainId = args.network as string | number;
return new EtherscanService(apiKey, networkOrChainId);
}
return etherscanService;
}
// List available tools
server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
{
name: "check-balance",
description: "Check the ETH balance of an Ethereum address across 70+ supported networks",
inputSchema: {
type: "object",
properties: {
address: {
type: "string",
description: "Ethereum address (0x format)",
pattern: "^0x[a-fA-F0-9]{40}$"
},
network: {
type: ["string", "number"],
description: "Network name (e.g., 'ethereum', 'arbitrum', 'base') or chain ID number (default: ethereum mainnet)"
},
},
required: ["address"],
},
},
{
name: "get-transactions",
description: "Get recent transactions for an Ethereum address across 70+ supported networks",
inputSchema: {
type: "object",
properties: {
address: {
type: "string",
description: "Ethereum address (0x format)",
pattern: "^0x[a-fA-F0-9]{40}$"
},
network: {
type: ["string", "number"],
description: "Network name or chain ID (default: ethereum mainnet)"
},
limit: {
type: "number",
description: "Number of transactions to return (max 100)",
minimum: 1,
maximum: 100
},
},
required: ["address"],
},
},
{
name: "get-token-transfers",
description: "Get ERC20 token transfers for an Ethereum address",
inputSchema: {
type: "object",
properties: {
address: {
type: "string",
description: "Ethereum address (0x format)",
pattern: "^0x[a-fA-F0-9]{40}$"
},
network: {
type: ["string", "number"],
description: "Network name or chain ID (default: ethereum mainnet)"
},
limit: {
type: "number",
description: "Number of transfers to return (max 100)",
minimum: 1,
maximum: 100
},
},
required: ["address"],
},
},
{
name: "get-contract-abi",
description: "Get the ABI for a smart contract",
inputSchema: {
type: "object",
properties: {
address: {
type: "string",
description: "Contract address (0x format)",
pattern: "^0x[a-fA-F0-9]{40}$"
},
network: {
type: ["string", "number"],
description: "Network name or chain ID (default: ethereum mainnet)"
},
},
required: ["address"],
},
},
{
name: "get-gas-prices",
description: "Get current gas prices in Gwei",
inputSchema: {
type: "object",
properties: {
network: {
type: ["string", "number"],
description: "Network name or chain ID (default: ethereum mainnet)"
},
},
},
},
{
name: "get-ens-name",
description: "Get the ENS name for an Ethereum address (mainnet only)",
inputSchema: {
type: "object",
properties: {
address: {
type: "string",
description: "Ethereum address (0x format)",
pattern: "^0x[a-fA-F0-9]{40}$"
},
},
required: ["address"],
},
},
{
name: "get-mined-blocks",
description: "Get blocks mined by an address",
inputSchema: {
type: "object",
properties: {
address: {
type: "string",
description: "Ethereum address (0x format)",
pattern: "^0x[a-fA-F0-9]{40}$"
},
network: {
type: ["string", "number"],
description: "Network name or chain ID (default: ethereum mainnet)"
},
blockType: {
type: "string",
description: "Type of blocks to query",
enum: ["blocks", "uncles"],
default: "blocks"
},
page: {
type: "number",
description: "Page number for pagination",
minimum: 1
},
offset: {
type: "number",
description: "Number of results per page (max 100)",
minimum: 1,
maximum: 100
},
startBlock: {
type: "number",
description: "Starting block number",
minimum: 0
},
endBlock: {
type: "number",
description: "Ending block number",
minimum: 0
}
},
required: ["address"],
},
},
{
name: "get-internal-transactions",
description: "Get internal transactions for an Ethereum address",
inputSchema: {
type: "object",
properties: {
address: {
type: "string",
description: "Ethereum address (0x format)",
pattern: "^0x[a-fA-F0-9]{40}$"
},
network: {
type: ["string", "number"],
description: "Network name or chain ID (default: ethereum mainnet)"
},
page: {
type: "number",
description: "Page number for pagination",
minimum: 1
},
offset: {
type: "number",
description: "Number of results per page (max 100)",
minimum: 1,
maximum: 100
},
startBlock: {
type: "number",
description: "Starting block number",
minimum: 0
},
endBlock: {
type: "number",
description: "Ending block number",
minimum: 0
}
},
required: ["address"],
},
},
{
name: "get-block-details",
description: "Get detailed information about a specific block",
inputSchema: {
type: "object",
properties: {
blockNumber: {
type: "number",
description: "Block number to query",
minimum: 0
},
network: {
type: ["string", "number"],
description: "Network name or chain ID (default: ethereum mainnet)"
},
},
required: ["blockNumber"],
},
},
{
name: "get-block-reward",
description: "Get reward information for a specific block",
inputSchema: {
type: "object",
properties: {
blockNumber: {
type: "number",
description: "Block number to query",
minimum: 0
},
network: {
type: ["string", "number"],
description: "Network name or chain ID (default: ethereum mainnet)"
},
},
required: ["blockNumber"],
},
},
{
name: "get-contract-source",
description: "Get the source code and metadata for a verified smart contract",
inputSchema: {
type: "object",
properties: {
address: {
type: "string",
description: "Contract address (0x format)",
pattern: "^0x[a-fA-F0-9]{40}$"
},
network: {
type: ["string", "number"],
description: "Network name or chain ID (default: ethereum mainnet)"
},
},
required: ["address"],
},
},
{
name: "get-contract-creation",
description: "Get creation information for a specific contract address",
inputSchema: {
type: "object",
properties: {
address: {
type: "string",
description: "Contract address (0x format)",
pattern: "^0x[a-fA-F0-9]{40}$"
},
network: {
type: ["string", "number"],
description: "Network name or chain ID (default: ethereum mainnet)"
},
},
required: ["address"],
},
},
{
name: "verify-contract",
description: "Submit a contract for source code verification",
inputSchema: {
type: "object",
properties: {
sourceCode: {
type: "string",
description: "Contract source code"
},
contractAddress: {
type: "string",
description: "Contract address (0x format)",
pattern: "^0x[a-fA-F0-9]{40}$"
},
contractName: {
type: "string",
description: "Contract name"
},
compilerVersion: {
type: "string",
description: "Compiler version (e.g. v0.8.0)"
},
optimization: {
type: "boolean",
description: "Whether optimization was used"
},
optimizationRuns: {
type: "number",
description: "Number of optimization runs",
minimum: 1
},
constructorArguments: {
type: "string",
description: "ABI-encoded constructor arguments"
},
evmVersion: {
type: "string",
description: "EVM version"
},
licenseType: {
type: "string",
description: "License type"
},
network: {
type: ["string", "number"],
description: "Network name or chain ID (default: ethereum mainnet)"
},
},
required: ["sourceCode", "contractAddress", "contractName", "compilerVersion"],
},
},
{
name: "check-verification",
description: "Check the status of a contract verification request",
inputSchema: {
type: "object",
properties: {
guid: {
type: "string",
description: "GUID from verification request"
},
network: {
type: ["string", "number"],
description: "Network name or chain ID (default: ethereum mainnet)"
},
},
required: ["guid"],
},
},
{
name: "verify-proxy",
description: "Submit a proxy contract for verification",
inputSchema: {
type: "object",
properties: {
address: {
type: "string",
description: "Proxy contract address (0x format)",
pattern: "^0x[a-fA-F0-9]{40}$"
},
expectedImplementation: {
type: "string",
description: "Expected implementation address (0x format)",
pattern: "^0x[a-fA-F0-9]{40}$"
},
network: {
type: ["string", "number"],
description: "Network name or chain ID (default: ethereum mainnet)"
},
},
required: ["address"],
},
},
{
name: "get-verified-contracts",
description: "Get recently verified contracts",
inputSchema: {
type: "object",
properties: {
page: {
type: "number",
description: "Page number for pagination",
minimum: 1
},
offset: {
type: "number",
description: "Number of results per page (max 100)",
minimum: 1,
maximum: 100
},
sortBy: {
type: "string",
description: "Sort by field",
enum: ['timestamp', 'name', 'address', 'compiler', 'balance', 'txCount', 'license', 'optimization']
}
},
required: ["page"],
},
},
{
name: "get-beacon-withdrawals",
description: "Get beacon chain withdrawals for an Ethereum address (ETH 2.0 staking withdrawals)",
inputSchema: {
type: "object",
properties: {
address: {
type: "string",
description: "Ethereum address",
pattern: "^0x[a-fA-F0-9]{40}$"
},
network: {
type: ["string", "number"],
description: "Network name or chain ID"
},
page: {
type: "number",
description: "Page number for pagination",
minimum: 1
},
offset: {
type: "number",
description: "Number of results per page",
minimum: 1,
maximum: 100
},
startBlock: {
type: "number",
description: "Starting block number",
minimum: 0
},
endBlock: {
type: "number",
description: "Ending block number",
minimum: 0
}
},
required: ["address"],
},
},
{
name: "get-token-info",
description: "Get comprehensive information about an ERC20/ERC721/ERC1155 token including metadata and social links",
inputSchema: {
type: "object",
properties: {
contractAddress: {
type: "string",
description: "Token contract address",
pattern: "^0x[a-fA-F0-9]{40}$"
},
network: {
type: ["string", "number"],
description: "Network name or chain ID"
}
},
required: ["contractAddress"],
},
},
{
name: "get-token-holders",
description: "Get top token holders for an ERC20 token with their balances and ownership percentage",
inputSchema: {
type: "object",
properties: {
contractAddress: {
type: "string",
description: "Token contract address",
pattern: "^0x[a-fA-F0-9]{40}$"
},
network: {
type: ["string", "number"],
description: "Network name or chain ID"
},
page: {
type: "number",
description: "Page number for pagination",
minimum: 1
},
offset: {
type: "number",
description: "Number of results per page",
minimum: 1,
maximum: 100
}
},
required: ["contractAddress"],
},
},
{
name: "get-token-portfolio",
description: "Get all token balances for an address across ERC20/ERC721/ERC1155 standards",
inputSchema: {
type: "object",
properties: {
address: {
type: "string",
description: "Ethereum address",
pattern: "^0x[a-fA-F0-9]{40}$"
},
network: {
type: ["string", "number"],
description: "Network name or chain ID"
}
},
required: ["address"],
},
},
{
name: "get-logs",
description: "Get event logs filtered by address and/or topics with advanced filtering options",
inputSchema: {
type: "object",
properties: {
address: {
type: "string",
description: "Contract address to filter logs",
pattern: "^0x[a-fA-F0-9]{40}$"
},
fromBlock: {
type: "number",
description: "Start block number",
minimum: 0
},
toBlock: {
type: "number",
description: "End block number",
minimum: 0
},
topic0: {
type: "string",
description: "Event signature hash (first topic)"
},
topic1: {
type: "string",
description: "First indexed parameter"
},
topic2: {
type: "string",
description: "Second indexed parameter"
},
topic3: {
type: "string",
description: "Third indexed parameter"
},
network: {
type: ["string", "number"],
description: "Network name or chain ID"
}
},
},
},
{
name: "get-network-stats",
description: "Get network statistics including ETH supply, ETH2 staking, price, and market cap",
inputSchema: {
type: "object",
properties: {
network: {
type: ["string", "number"],
description: "Network name or chain ID"
}
},
},
},
{
name: "get-daily-stats",
description: "Get daily transaction count statistics for a date range",
inputSchema: {
type: "object",
properties: {
startDate: {
type: "string",
description: "Start date in YYYY-MM-DD format",
pattern: "^\\d{4}-\\d{2}-\\d{2}$"
},
endDate: {
type: "string",
description: "End date in YYYY-MM-DD format",
pattern: "^\\d{4}-\\d{2}-\\d{2}$"
},
sort: {
type: "string",
description: "Sort order",
enum: ["asc", "desc"]
},
network: {
type: ["string", "number"],
description: "Network name or chain ID"
}
},
required: ["startDate", "endDate"],
},
},
{
name: "list-networks",
description: "List all supported blockchain networks (70+) with their chain IDs and native currencies",
inputSchema: {
type: "object",
properties: {
filter: {
type: "string",
description: "Filter networks by type",
enum: ["all", "mainnet", "testnet", "l2"]
}
},
},
},
],
};
});
// Handle tool execution
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args = {} } = request.params;
// Create a new service instance if network is specified
const serviceForRequest = getServiceForRequest(args);
if (name === "check-balance") {
try {
const { address } = AddressSchema.parse(args);
const balance = await serviceForRequest.getAddressBalance(address);
const response = `Address: ${balance.address}\nBalance: ${balance.balanceInEth} ETH`;
return {
content: [{ type: "text", text: response }],
};
} catch (error) {
if (error instanceof z.ZodError) {
throw new Error(`Invalid input: ${error.errors.map(e => e.message).join(", ")}`);
}
throw error;
}
}
if (name === "get-transactions") {
try {
const { address, limit } = TransactionHistorySchema.parse(args);
const transactions = await serviceForRequest.getTransactionHistory(address, limit);
const formattedTransactions = transactions.map(tx => {
const date = new Date(tx.timestamp * 1000).toLocaleString();
return `Block ${tx.blockNumber} (${date}):\n` +
`Hash: ${tx.hash}\n` +
`From: ${tx.from}\n` +
`To: ${tx.to}\n` +
`Value: ${tx.value} ETH\n` +
`---`;
}).join('\n');
const response = transactions.length > 0
? `Recent transactions for ${address}:\n\n${formattedTransactions}`
: `No transactions found for ${address}`;
return {
content: [{ type: "text", text: response }],
};
} catch (error) {
if (error instanceof z.ZodError) {
throw new Error(`Invalid input: ${error.errors.map(e => e.message).join(", ")}`);
}
throw error;
}
}
if (name === "get-token-transfers") {
try {
const { address, limit } = TokenTransferSchema.parse(args);
const transfers = await serviceForRequest.getTokenTransfers(address, limit);
const formattedTransfers = transfers.map(tx => {
const date = new Date(tx.timestamp * 1000).toLocaleString();
return `Block ${tx.blockNumber} (${date}):\n` +
`Token: ${tx.tokenName} (${tx.tokenSymbol})\n` +
`From: ${tx.from}\n` +
`To: ${tx.to}\n` +
`Value: ${tx.value}\n` +
`Contract: ${tx.token}\n` +
`---`;
}).join('\n');
const response = transfers.length > 0
? `Recent token transfers for ${address}:\n\n${formattedTransfers}`
: `No token transfers found for ${address}`;
return {
content: [{ type: "text", text: response }],
};
} catch (error) {
if (error instanceof z.ZodError) {
throw new Error(`Invalid input: ${error.errors.map(e => e.message).join(", ")}`);
}
throw error;
}
}
if (name === "get-contract-abi") {
try {
const { address } = ContractSchema.parse(args);
const abi = await serviceForRequest.getContractABI(address);
return {
content: [{ type: "text", text: `Contract ABI for ${address}:\n\n${abi}` }],
};
} catch (error) {
if (error instanceof z.ZodError) {
throw new Error(`Invalid input: ${error.errors.map(e => e.message).join(", ")}`);
}
throw error;
}
}
if (name === "get-gas-prices") {
try {
const prices = await serviceForRequest.getGasOracle();
const response = `Current Gas Prices:\n` +
`Safe Low: ${prices.safeGwei} Gwei\n` +
`Standard: ${prices.proposeGwei} Gwei\n` +
`Fast: ${prices.fastGwei} Gwei`;
return {
content: [{ type: "text", text: response }],
};
} catch (error) {
throw error;
}
}
if (name === "get-ens-name") {
try {
const { address } = AddressSchema.parse(args);
// ENS is only available on mainnet
const mainnetService = new EtherscanService(apiKey, 'mainnet');
const ensName = await mainnetService.getENSName(address);
const response = ensName
? `ENS name for ${address}: ${ensName}`
: `No ENS name found for ${address}`;
return {
content: [{ type: "text", text: response }],
};
} catch (error) {
if (error instanceof z.ZodError) {
throw new Error(`Invalid input: ${error.errors.map(e => e.message).join(", ")}`);
}
throw error;
}
}
if (name === "get-mined-blocks") {
try {
const { address, blockType = 'blocks', page, offset, startBlock, endBlock } = MinedBlocksSchema.parse(args);
const blocks = await serviceForRequest.getMinedBlocks(address, blockType, page, offset, startBlock, endBlock);
const formattedBlocks = blocks.map(block => {
const date = new Date(block.timestamp * 1000).toLocaleString();
return `Block ${block.blockNumber} (${date}):\n` +
`Type: ${block.blockType}\n` +
`Miner: ${block.blockMiner}\n` +
`Reward: ${block.blockReward} ETH\n` +
`---`;
}).join('\n');
const response = blocks.length > 0
? `Mined blocks for ${address}:\n\n${formattedBlocks}`
: `No mined blocks found for ${address}`;
return {
content: [{ type: "text", text: response }],
};
} catch (error) {
if (error instanceof z.ZodError) {
throw new Error(`Invalid input: ${error.errors.map(e => e.message).join(", ")}`);
}
throw error;
}
}
if (name === "get-internal-transactions") {
try {
const { address, page, offset, startBlock, endBlock } = InternalTransactionsSchema.parse(args);
const transactions = await serviceForRequest.getInternalTransactions(address, page, offset, startBlock, endBlock);
const formattedTransactions = transactions.map(tx => {
const date = new Date(tx.timestamp * 1000).toLocaleString();
const status = tx.isError ? `Error: ${tx.errCode}` : 'Success';
return `Block ${tx.blockNumber} (${date}):\n` +
`Hash: ${tx.hash}\n` +
`From: ${tx.from}\n` +
`To: ${tx.to}\n` +
`Value: ${tx.value} ETH\n` +
`Type: ${tx.type}\n` +
`Status: ${status}\n` +
`Trace ID: ${tx.traceId}\n` +
`---`;
}).join('\n');
const response = transactions.length > 0
? `Internal transactions for ${address}:\n\n${formattedTransactions}`
: `No internal transactions found for ${address}`;
return {
content: [{ type: "text", text: response }],
};
} catch (error) {
if (error instanceof z.ZodError) {
throw new Error(`Invalid input: ${error.errors.map(e => e.message).join(", ")}`);
}
throw error;
}
}
if (name === "get-block-details") {
try {
const { blockNumber } = BlockSchema.parse(args);
const block = await serviceForRequest.getBlockDetails(blockNumber);
const date = new Date(block.timestamp * 1000).toLocaleString();
const response = `Block ${block.number} (${date}):\n` +
`Hash: ${block.hash}\n` +
`Parent Hash: ${block.parentHash}\n` +
`Miner: ${block.miner}\n` +
`Size: ${block.size} bytes\n` +
`Gas Used: ${parseInt(block.gasUsed, 16).toLocaleString()} \n`+
`Gas Limit: ${parseInt(block.gasLimit, 16).toLocaleString()}\n` +
`Transactions: ${block.transactions}\n` +
`Nonce: ${block.nonce}\n` +
`Difficulty: ${parseInt(block.difficulty, 16).toLocaleString()}\n` +
`Extra Data: ${block.extraData}`;
return {
content: [{ type: "text", text: response }],
};
} catch (error) {
if (error instanceof z.ZodError) {
throw new Error(`Invalid input: ${error.errors.map(e => e.message).join(", ")}`);
}
throw error;
}
}
if (name === "get-block-reward") {
try {
const { blockNumber } = BlockSchema.parse(args);
const reward = await serviceForRequest.getBlockReward(blockNumber);
const date = new Date(reward.timeStamp * 1000).toLocaleString();
const response = `Block ${reward.blockNumber} (${date}):\n` +
`Miner: ${reward.blockMiner}\n` +
`Block Reward: ${reward.blockReward} ETH\n` +
`Uncle Inclusion Reward: ${reward.uncleInclusionReward} ETH\n` +
`Uncles: ${reward.uncles.length}`;
return {
content: [{ type: "text", text: response }],
};
} catch (error) {
if (error instanceof z.ZodError) {
throw new Error(`Invalid input: ${error.errors.map(e => e.message).join(", ")}`);
}
throw error;
}
}
if (name === "get-contract-source") {
try {
const { address } = ContractSchema.parse(args);
const sourceCode = await serviceForRequest.getContractSourceCode(address);
const proxyInfo = sourceCode.proxy
? `\nProxy: Yes\nImplementation: ${sourceCode.implementation}`
: '\nProxy: No';
const response = `Contract Source Code for ${address}:\n\n` +
`Name: ${sourceCode.contractName}\n` +
`Compiler: ${sourceCode.compilerVersion}\n` +
`License: ${sourceCode.licenseType}\n` +
`Optimization: ${sourceCode.optimizationUsed ? `Yes (${sourceCode.runs} runs)` : 'No'}\n` +
`EVM Version: ${sourceCode.evmVersion}${proxyInfo}\n\n` +
`Source Code:\n${sourceCode.sourceCode}`;
return {
content: [{ type: "text", text: response }],
};
} catch (error) {
if (error instanceof z.ZodError) {
throw new Error(`Invalid input: ${error.errors.map(e => e.message).join(", ")}`);
}
throw error;
}
}
if (name === "get-contract-creation") {
try {
const { address } = ContractCreationSchema.parse(args);
const creationInfos = await serviceForRequest.getContractCreation(address);
if (creationInfos.length === 0) {
return {
content: [{ type: "text", text: `No creation info found for contract ${address}` }],
};
}
const creationInfo = creationInfos[0]; // Get first result
const date = new Date(parseInt(creationInfo.timestamp) * 1000).toLocaleString();
const response = `Contract Creation Info for ${creationInfo.contractAddress}:\n` +
`Creator: ${creationInfo.contractCreator}\n` +
`Creation Transaction: ${creationInfo.txHash}\n` +
`Block Number: ${creationInfo.blockNumber}\n` +
`Created: ${date}\n` +
`Factory: ${creationInfo.contractFactory || 'N/A'}\n`;
return {
content: [{ type: "text", text: response }],
};
} catch (error) {
if (error instanceof z.ZodError) {
throw new Error(`Invalid input: ${error.errors.map(e => e.message).join(", ")}`);
}
throw error;
}
}
if (name === "verify-contract") {
try {
const params = VerifyContractSchema.parse(args);
const guid = await serviceForRequest.verifyContract(params);
return {
content: [{
type: "text",
text: `Contract verification submitted. Use the check-verification command with GUID: ${guid} to check the status.`
}],
};
} catch (error) {
if (error instanceof z.ZodError) {
throw new Error(`Invalid input: ${error.errors.map(e => e.message).join(", ")}`);
}
throw error;
}
}
if (name === "check-verification") {
try {
const { guid } = CheckVerificationSchema.parse(args);
const status = await serviceForRequest.checkVerificationStatus(guid);
return {
content: [{
type: "text",
text: `Verification status: ${status.status}\nMessage: ${status.message}`
}],
};
} catch (error) {
if (error instanceof z.ZodError) {
throw new Error(`Invalid input: ${error.errors.map(e => e.message).join(", ")}`);
}
throw error;
}
}
if (name === "verify-proxy") {
try {
const params = VerifyProxySchema.parse(args);
const guid = await serviceForRequest.verifyProxyContract(params);
return {
content: [{
type: "text",
text: `Proxy contract verification submitted. Use the check-verification command with GUID: ${guid} to check the status.`
}],
};
} catch (error) {
if (error instanceof z.ZodError) {
throw new Error(`Invalid input: ${error.errors.map(e => e.message).join(", ")}`);
}
throw error;
}
}
if (name === "get-verified-contracts") {
try {
const { page, offset, sortBy } = VerifiedContractsSchema.parse(args);
const contracts = await serviceForRequest.getVerifiedContracts(page, offset, sortBy);
const formattedContracts = contracts.map(contract => {
const date = new Date(contract.timestamp * 1000).toLocaleString();
return `Contract: ${contract.name}\n` +
`Address: ${contract.address}\n` +
`Verified: ${date}\n` +
`Compiler: ${contract.compiler} v${contract.version}\n` +
`Balance: ${contract.balance} ETH\n` +
`Transactions: ${contract.txCount}\n` +
`License: ${contract.license}\n` +
`Optimization: ${contract.optimization ? `Yes (${contract.optimizationRuns} runs)` : 'No'}\n` +
`EVM Version: ${contract.evmVersion}\n` +
`Constructor Arguments: ${contract.constructorArguments || 'None'}\n` +
`Proxy: ${contract.proxy ? `Yes (Implementation: ${contract.implementation})` : 'No'}\n` +
`Swarm Source: ${contract.swarmSource || 'None'}\n` +
`---`;
}).join('\n');
const response = contracts.length > 0
? `Recently verified contracts:\n\n${formattedContracts}`
: `No verified contracts found`;
return {
content: [{ type: "text", text: response }],
};
} catch (error) {
if (error instanceof z.ZodError) {
throw new Error(`Invalid input: ${error.errors.map(e => e.message).join(", ")}`);
}
throw error;
}
}
if (name === "get-beacon-withdrawals") {
try {
const { address, page, offset, startBlock, endBlock } = args as {
address: string;
page?: number;
offset?: number;
startBlock?: number;
endBlock?: number;
};
const withdrawals = await serviceForRequest.getBeaconWithdrawals(address, {
page,
offset,
startBlock,
endBlock
});
const formattedWithdrawals = withdrawals.map(w => {
const date = new Date(w.timestamp * 1000).toLocaleString();
return `Withdrawal ${w.withdrawalIndex} (${date}):\n` +
`Validator Index: ${w.validatorIndex}\n` +
`Address: ${w.address}\n` +
`Amount: ${w.amount} ETH\n` +
`Block: ${w.blockNumber}\n` +
`---`;
}).join('\n');
const response = withdrawals.length > 0
? `Beacon withdrawals for ${address}:\n\n${formattedWithdrawals}`
: `No beacon withdrawals found for ${address}`;
return {
content: [{ type: "text", text: response }],
};
} catch (error) {
if (error instanceof z.ZodError) {
throw new Error(`Invalid input: ${error.errors.map(e => e.message).join(", ")}`);
}
throw error;
}
}
if (name === "get-token-info") {
try {
const { contractAddress } = args as { contractAddress: string };
const info = await serviceForRequest.getTokenInfo(contractAddress);
const response = `Token Information for ${contractAddress}:\n\n` +
`Name: ${info.tokenName}\n` +
`Symbol: ${info.symbol}\n` +
`Type: ${info.tokenType}\n` +
`Total Supply: ${info.totalSupply}\n` +
`Decimals: ${info.divisor}\n` +
`Verified: ${info.blueCheckmark ? 'Yes' : 'No'}\n` +
(info.description ? `\nDescription: ${info.description}\n` : '') +
(info.website ? `Website: ${info.website}\n` : '') +
(info.twitter ? `Twitter: ${info.twitter}\n` : '') +
(info.github ? `GitHub: ${info.github}\n` : '') +
(info.tokenPriceUSD ? `\nPrice: $${info.tokenPriceUSD}` : '');
return {
content: [{ type: "text", text: response }],
};
} catch (error) {
if (error instanceof z.ZodError) {
throw new Error(`Invalid input: ${error.errors.map(e => e.message).join(", ")}`);
}
throw error;
}
}
if (name === "get-token-holders") {
try {
const { contractAddress, page, offset } = args as {
contractAddress: string;
page?: number;
offset?: number;
};
const holders = await serviceForRequest.getTokenHolders(contractAddress, { page, offset });
const formattedHolders = holders.map((holder, index) => {
return `${index + 1}. ${holder.address}\n` +
` Balance: ${holder.balance}\n` +
` Share: ${holder.share.toFixed(4)}%\n` +
`---`;
}).join('\n');
const response = holders.length > 0
? `Top Token Holders for ${contractAddress}:\n\n${formattedHolders}`
: `No token holders found for ${contractAddress}`;
return {
content: [{ type: "text", text: response }],
};
} catch (error) {
if (error instanceof z.ZodError) {
throw new Error(`Invalid input: ${error.errors.map(e => e.message).join(", ")}`);
}
throw error;
}
}
if (name === "get-token-portfolio") {
try {
const { address } = args as { address: string };
const portfolio = await serviceForRequest.getTokenPortfolio(address);
const formattedPortfolio = portfolio.map(token => {
return `${token.tokenSymbol} (${token.tokenName}):\n` +
` Contract: ${token.tokenAddress}\n` +
` Balance: ${token.balance}\n` +
` Decimals: ${token.tokenDecimal}\n` +
`---`;
}).join('\n');
const response = portfolio.length > 0
? `Token Portfolio for ${address}:\n\n${formattedPortfolio}\n\nTotal Tokens: ${portfolio.length}`
: `No tokens found for ${address}`;
return {
content: [{ type: "text", text: response }],
};
} catch (error) {
if (error instanceof z.ZodError) {
throw new Error(`Invalid input: ${error.errors.map(e => e.message).join(", ")}`);
}
throw error;
}
}
if (name === "get-logs") {
try {
const params = args as {
address?: string;
fromBlock?: number;
toBlock?: number;
topic0?: string;
topic1?: string;
topic2?: string;
topic3?: string;
};
const logs = await serviceForRequest.getLogs(params);
const formattedLogs = logs.slice(0, 20).map(log => {
const date = new Date(log.timeStamp * 1000).toLocaleString();
return `Log ${log.logIndex} in Block ${log.blockNumber} (${date}):\n` +
`Address: ${log.address}\n` +
`Transaction: ${log.transactionHash}\n` +
`Topics: ${log.topics.length}\n` +
`Data: ${log.data.substring(0, 66)}${log.data.length > 66 ? '...' : ''}\n` +
`---`;
}).join('\n');
const response = logs.length > 0
? `Event Logs (showing first 20 of ${logs.length}):\n\n${formattedLogs}`
: `No logs found matching the criteria`;
return {
content: [{ type: "text", text: response }],
};
} catch (error) {
if (error instanceof z.ZodError) {
throw new Error(`Invalid input: ${error.errors.map(e => e.message).join(", ")}`);
}
throw error;
}
}
if (name === "get-network-stats") {
try {
const stats = await serviceForRequest.getNetworkStats();
const response = `Network Statistics:\n\n` +
`ETH Supply: ${stats.ethSupply}\n` +
(stats.eth2Staking ? `ETH2 Staking: ${stats.eth2Staking}\n` : '') +
(stats.burntFees ? `Burnt Fees: ${stats.burntFees}\n` : '') +
`ETH Price (USD): $${stats.ethPrice}\n` +
`ETH/BTC Price: ${stats.ethBtcPrice}\n` +
`Market Cap: $${stats.marketCap}`;
return {
content: [{ type: "text", text: response }],
};
} catch (error) {
throw error;
}
}
if (name === "get-daily-stats") {
try {
const { startDate, endDate, sort = 'desc' } = args as {
startDate: string;
endDate: string;
sort?: 'asc' | 'desc';
};
const dailyStats = await serviceForRequest.getDailyTxCount(startDate, endDate, sort);
const formattedStats = dailyStats.map(day => {
return `${day.date}: ${parseInt(day.value).toLocaleString()} transactions`;
}).join('\n');
const total = dailyStats.reduce((sum, day) => sum + parseInt(day.value), 0);
const avg = Math.round(total / dailyStats.length);
const response = `Daily Transaction Statistics (${startDate} to ${endDate}):\n\n` +
`${formattedStats}\n\n` +
`Total: ${total.toLocaleString()} transactions\n` +
`Average: ${avg.toLocaleString()} transactions/day`;
return {
content: [{ type: "text", text: response }],
};
} catch (error) {
if (error instanceof z.ZodError) {
throw new Error(`Invalid input: ${error.errors.map(e => e.message).join(", ")}`);
}
throw error;
}
}
if (name === "list-networks") {
try {
const { filter = 'all' } = args as { filter?: 'all' | 'mainnet' | 'testnet' | 'l2' };
let networks = getAllNetworks();
if (filter === 'mainnet') {
networks = networks.filter(n => !n.isTestnet);
} else if (filter === 'testnet') {
networks = networks.filter(n => n.isTestnet);
} else if (filter === 'l2') {
networks = networks.filter(n => n.isL2);
}
const formattedNetworks = networks.map(network => {
const tags = [];
if (network.isTestnet) tags.push('Testnet');
if (network.isL2) tags.push('L2');
const tagStr = tags.length > 0 ? ` [${tags.join(', ')}]` : '';
return `${network.name}${tagStr}\n` +
` Chain ID: ${network.chainId}\n` +
` Native Currency: ${network.nativeCurrency.symbol}\n` +
`---`;
}).join('\n');
const response = `Supported Networks (${networks.length} total):\n\n${formattedNetworks}`;
return {
content: [{ type: "text", text: response }],
};
} catch (error) {
throw error;
}
}
throw new Error(`Unknown tool: ${name}`);
});
// Start the server
export async function startServer() {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error(`Etherscan MCP Server running on stdio (Network: ${network})`);
}