使用说明
Etherscan MCP
CLI + stdio MCP server,封装 Etherscan API V2(ABI / 源码 / 索引类查询)+ EVM JSON-RPC(eth_call / eth_getStorageAt / eth_getLogs / 区块 / 交易等只读调用),用于合约研判和链上数据抓取。不签名、不广播链上交易。
功能概览
- 合约详情:ABI、源码、编译器版本、验证状态。
- 创建信息:创建者、创建交易哈希、块高(Etherscan 优先,失败可回退 RPC 二分定位)。
- 代理检测:读取 EIP-1967 implementation/admin 槽,输出实现地址与证据。
- 交互数据:普通交易列表、ERC20/721/1155 代币转移列表、日志按 topic 查询。
- 状态读取:任意存储槽读取(
eth_getStorageAt)、只读函数调用(eth_call,支持本地 ABI 编码 + 返回值解码)。 - 区块 / 交易:
eth_getBlockByNumber、eth_getTransactionByHash+ receipt。 - 链清单:基于 Etherscan V2
/v2/chainlist动态拉取,支持别名(bsc/base/arb等)。 - 缓存:纯内存(进程级),合约详情与创建信息缓存;动态列表 / 日志 / 存储 /
eth_call默认不缓存。
环境要求
- Python 3.11(兼容 3.10+)。
ETHERSCAN_API_KEY(必填)。
安装
pip install -r src/etherscan-mcp/requirements.txt
CLI
CLI 是主要接口(TOOLING 决策 2026-07-05:能力经 CLI + skill 暴露),子命令与 MCP tools 一一对应、直调 ContractService 内核。数组类参数(--args / --topics)一律 JSON 数组字符串,topic 通配位用 null。
ETHERSCAN_API_KEY=<key> python -m app <subcommand> [options] # python -m app --help 看环境变量与子命令全表
# 合约
python -m app fetch --address <contract> [--network <chain>] [--inline-limit N|--force-inline]
python -m app get-source-file --address <contract> --filename <file> [--offset N --length M]
python -m app get-contract-creation --address <contract>
python -m app detect-proxy --address <contract>
# 索引类查询
python -m app list-transactions --address <addr> [--start-block N --end-block M --page P --offset O --sort asc|desc]
python -m app list-token-transfers --address <addr> [--token-type erc20|erc721|erc1155] [分页参数同上]
python -m app query-logs --address <contract> --topics '["0x..."]' [--from-block N --to-block latest --page P --offset O]
# 链上状态(需 RPC_URL / RPC_URL_<chainid>)
python -m app get-storage-at --address <contract> --slot <slot> [--block-tag latest|N|0x..]
python -m app call-function --address <contract> --function 'balanceOf(address)' --args '["0x..."]' [--decimals 6]
python -m app call-function-series --address <contract> --function 'totalSupply()' --from-block N --to-block M --stride K
# 交易 / 区块
python -m app get-transaction --tx-hash <0x..>
python -m app get-transaction-summary --tx-hash <0x..> [--compact --flow-scope user|user_router|all] [--no-decode-transfers --no-annotate-contracts]
python -m app get-block --block <latest|dec|0x> [--full-transactions] [--tx-hashes-only]
python -m app get-block-time --block <latest|dec|0x>
# 工具
python -m app encode-function-data --function 'transfer(address,uint256)' --args '["0x...", 1000]'
python -m app keccak --value 'Transfer(address,address,uint256)' [--input-type text|hex|bytes]
python -m app convert --value 0x1bc16d674ec80000 --from hex --to eth [--decimals 18]
python -m app list-chains [--include-degraded]
python -m app resolve-chain --network <name|alias|chainid>
源码超内联阈值(默认 20000 字符)且未强制时,source_files 仅返回摘要(filename/length/sha256/inline=false)并附 source_omitted/source_omitted_reason,需要原文用 get-source-file 分段拿。
MCP(能力保留,本机注册已退役)
2026-07-07 起本机不再注册本仓 MCP(TOOLING 决策:存量 MCP 逐仓退役,能力走 CLI + skill)。server 代码保留,其他环境如需注册可参考下例。
codex mcp add etherscan-mcp \
--env ETHERSCAN_API_KEY=<your-api-key> \
--env RPC_URL_1=<archive-rpc-https-endpoint> \
--env RPC_URL_56=<your-bsc-rpc-https-endpoint> \
--env RPC_URL_8453=<your-base-rpc-https-endpoint> \
-- bash -lc "cd `pwd`/src/etherscan-mcp && python -m app.mcp_server --transport stdio"
本机长驻 daemon 可用 streamable HTTP:
cd src/etherscan-mcp
python -m app.mcp_server --transport streamable-http --host 127.0.0.1 --port 8702 --streamable-http-path /mcp
RPC_URL_1 推荐配 archive 节点(Alchemy / Quicknode / drpc / Ankr / 自建 erigon),call_function / call_function_series / get_storage_at 才能按历史 block_tag 读链上 state;普通 full node 不带 archive 不行(详见 已知限制)。
工具列表变更后需在 Codex / Claude Code 侧重新连接 MCP server。常用主网测试地址:USDT 0xdAC17F958D2ee523a2206206994597C13D831ec7、USDC 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48。
模块结构
入口目录 src/etherscan-mcp/app/:
config.py—— 读取环境变量,封装为配置对象;保留少量静态NETWORK_CHAIN_ID_MAP(mainnet/bsc/sepolia 等)作为 chainlist 不可用时的兜底。chains.py—— 基于 Etherscan V2/v2/chainlist的链清单模块,进程内 TTL 缓存;提供list_chains()与resolve(network)(支持数字 chainid、链名模糊、别名arb/bsc/base)。capabilities.py—— 手维护的 per-chain caveat 矩阵(chainid → [{tool, status, reason, workaround}]),把 README「已知限制」结构化暴露出来。status枚举:requires_rpc_url/paid_tier_only/degraded/unsupported;service 层在输出时会附status_effective,requires_rpc_url在配了RPC_URL_<chainid>时降级为ok。etherscan_client.py—— requests 封装的 REST client,对源码 / 创建信息 / 交易 / 转移 / 日志 /module=proxy做有限重试与退避,识别限流文案(rate limit/Max calls per sec/Too Many Requests)。rpc_client.py—— JSON-RPC(HTTP POST)封装;eth_call/eth_getStorageAt/eth_getLogs/eth_getBlockByNumber/eth_getTransactionByHash/eth_getTransactionReceipt/eth_blockNumber等只读调用。cache.py—— 纯内存缓存(进程级,不落盘),按 address+chainid 键控;contract 详情与 creation 用不同命名空间。service.py—— 聚合层:地址校验、network/chainid 解析、ABI 解析、读链路由(已配 RPC 走 RPC,未配走module=proxy)、call_function 编码 / 解码、convert helper。cli.py/__main__.py—— CLI 入口。mcp_server.py—— FastMCP server,注册 tools。
MCP tools
| 类别 | tools |
|---|---|
| Contracts | fetch_contract、get_source_file、get_contract_creation、detect_proxy |
| Chains | list_chains、resolve_chain |
| Transactions / Transfers / Logs | list_transactions、list_token_transfers、query_logs |
| State / Calls | get_storage_at、call_function、call_function_series、encode_function_data、keccak |
| Blocks / Tx | get_block_by_number、get_block_time_by_number、get_transaction、get_transaction_summary |
| Helpers | convert |
参数与错误约定
容易踩坑的几条:
network入参:支持数字 chainid("42161",最稳定)、官方链名 / 模糊匹配、轻量别名(arb、arb-sepolia、bsc→56、base→8453)。歧义时强制要求 chainid。- 默认网络安全:未显式传
network且默认NETWORK无法解析、CHAIN_ID也未设置时,service 直接报错,避免误用主网。 - 数组形态参数:
call_function.args/call_function_series.args/encode_function_data.args/query_logs.topics必须是数组。MCP 入口层会把标量自动包成单元素数组;字符串 / bytes / 对象会直接报错并提示示例,避免被逐字符拆分。 - 块号输入兼容:
start_block/end_block/from_block/to_block接受整数、十进制字符串、0x十六进制字符串。非法输入报错提示"十进制或 0x 前缀"。 - 未验证合约:
getsourcecode返回典型未验证文案(如Contract source code not verified)时,明确报错"合约未验证导致 ABI 不可用",附 address/network/chain_id 与截断摘要。 call_function:基础校验 0x / 偶数字节 / 至少 4 字节 selector;ABI 命中时按 outputs 解码(含 tuple / 数组),数值类支持decimalshint 计算value_scaled;ABI 加载但 selector 缺失时软失败放行 raweth_call,decoded.warning提示;无参函数可省略括号(readTokens等价readTokens())。call_function_series:对同一个data或function+args从from_block开始、按stride递增采样,直到下一个点会超过to_block为止;例如from_block=10,to_block=15,stride=3采样10,13,不会强制补尾块15。返回series[] = {block_number, block_tag, data, decoded}。只走 JSON-RPC batch,不回退 Etherscan;必须配置对应链的 archiveRPC_URL_<chainid>。batch_size默认 25,用来控制单次 JSON-RPC batch 大小;单次最多 10000 个采样点,超出要加大stride或缩小 block range。- 代理感知:
fetch_contract解析 Etherscan Proxy/Implementation 元数据,规范化实现地址写入 proxy cache;call_functionABI 选择优先实现合约(来自元数据或 EIP-1967 detect_proxy);探测异常不缓存"非代理",避免假阴性;缺实现 ABI 不阻断调用,仅解码受限。 convert:from_unit/to_unit支持hex/dec/human/wei/gwei/eth,decimals默认 18;内部用整数 / Decimal 避免浮点丢精度;分数精度超限会报错。get_transaction:优先 RPC 的eth_getTransactionByHash+eth_getTransactionReceipt,未配 RPC 回退 Etherscan proxy;tx_hash需0x+ 64 hex。get_transaction_summary:一次性给出 tx meta + gas cost + 唯一 log address 列表(带 EtherscanContractName注解)+ ERC20Transfer解码(topic0=0xddf252ad...,3 topics 严格匹配,自动跳 ERC721 4-topic 变体),并 best-effort 拉每个 token 的symbol/decimals/name(标准 selector + 兼容bytes32symbol/name 的旧式 ERC20 如 MKR)。decode_transfers/annotate_contracts默认true,关掉跳过对应 lookup。注解 + token metadata 走线程池并发拉取(METADATA_FETCH_CONCURRENCY默认 5),并落ETHERSCAN_MCP_CACHE_DIR持久化,进程重启不重拉;瞬时 RPC 失败(节点限速 / 暂时不可用)不写 cache,下次自动重试,仅对真正解码失败的字段(合约不实现 ERC20 接口等)才缓存为None。协议特异识别("这是 Pendle market / PT / YT")默认不做,靠 Etherscan ContractName + 调用方在 pendle-mcp 等下游做交叉。get_transaction_summarycompact=true:跨协议套利结构视图。回答"这笔 tx 是什么结构、资金净流向是什么、成本多少",而不是"完整日志是什么"。返回gas(嵌套execution_fee/l1_fee/total_fee,OP stack 链直接读 receiptl1Fee/l1GasUsed/l1GasPrice;非 OP stackl1_fee_*为null)+protocols/contracts/tokens/net_token_flow_by_address(按(address, token)聚合的有符号 ERC20 净额,跳 0 项)+ 启发式route_hints(关键词匹配PendleRouter/PendleMarket/MetaAggregationRouter/Kyber/AggregationRouterV/UniswapV3/CLPool,token symbol 前缀PT-/YT-/SY-;规则在app/capabilities.py:ROUTE_HINT_RULES)+counts。route_hints是启发式标签,调研要交叉验证,不要拿来当结论。compact 模式不返回逐条erc20_transfers;要原始列表请用默认模式(compact=false)。get_transaction_summaryflow_scope(compact 模式专用):控制net_token_flow_by_address过滤粒度。user(默认)只保留tx.from净流,套利判断时一眼看用户最终拿了什么 / 丢了什么;user_router额外保留tx.to(router 自己截留 fee 的场景);all保留全部行(pool / zero address mint+burn / aggregator 中间地址都在)。tokens/contracts/protocols/route_hints不受影响 —— 它们描述 tx 结构,不是用户净额。counts.flow_rows_total/counts.flow_rows_after_scope暴露过滤前后行数。query_logs(RPC 路径):page/offset用"按 block range 分段累积后切片"的 best-effort 实现;RPC log 不含timeStamp,time_stamp字段为null。
错误处理:JSON-RPC error 对象统一抛 ValueError("RPC error: ...");Etherscan proxy 回退路径若返回非 hex result(往往是限流文案)会按错误处理而非成功;HTTP 429 / 5xx 走重试与退避。
已知限制
这些限制结构化进了
capabilities.py,跑任务前调一次resolve_chain --network <chain>就能拿到当前链的caveats+rpc_configured;list_chains输出带has_caveats标记。不必再二手转述这一节。
list_transactions/list_token_transfers在部分 free tier 链上返回空:对应 Etherscan 的txlist/tokentxindexed 端点,原生 JSON-RPC 没有等价能力(eth_*只能按 hash/block 拿,没法按 address 倒查历史)。Base 等链 free tier 直接返回空,目前没有 fallback。后续如有需要要走 BaseScan native key 或第三方索引服务(Covalent / Alchemy enhanced API),单独立项。status=paid_tier_only。get_contract_creation在 BSC 等链可能 NOTOK:建议配RPC_URL_<chainid>启用 RPC 二分回退;internal create 场景可能仅返回block_number/timestamp(complete=false),且需要 archive / full-history 节点。status=degraded。module=proxy在 Base / BSC 等链 free tier 受限:会报Free API access is not supported for this chain,配RPC_URL_<chainid>绕开。status=requires_rpc_url,配上 RPC 后status_effective自动降级为ok。call_function/call_function_series/get_storage_at历史 state 全链不支持(chain-agnostic):Etherscanmodule=proxy对非latest|earliest|pending的 block_tag 静默忽略,永远返回 latest state(debug 起来很坑),所有链都一样。当前代码当 RPC 未配且要读历史 state 时显式报错(不再静默 fallback)。修复方式:配 archive 节点的RPC_URL_<chainid>,普通 full node 不够要 archive(Alchemy / Quicknode / drpc / Ankr / 自建 erigon)。status=requires_rpc_url,登记在GLOBAL_CAVEATS,全链生效。status_effective在RPC_URL_<chainid>配上后会降级为ok,但 archive vs full node 没法从 URL 自动检测,配错节点会运行时报"historical state not available"。- 新链 / 未列入 caveat 矩阵的链(HyperEVM、Plasma 等):默认按"无 caveat"处理。先用
list_chains确认 Etherscan V2 是否覆盖(status=1 为正常),跑任务踩坑后回头补capabilities.py。
配置(环境变量)
| 变量 | 默认 | 说明 |
|---|---|---|
ETHERSCAN_API_KEY |
— | 必填 |
ETHERSCAN_BASE_URL |
https://api.etherscan.io/v2/api |
API base URL |
NETWORK |
mainnet |
默认链;可传数字 chainid 或链名 / 别名 |
CHAIN_ID |
— | 硬覆盖 network 推导出的 chainid |
ETHERSCAN_CHAINLIST_URL |
https://api.etherscan.io/v2/chainlist |
链清单端点 |
CHAINLIST_TTL_SECONDS |
3600 |
链清单缓存 TTL |
RPC_URL_<chainid> |
— | 指定链的 JSON-RPC HTTP 端点。BSC/Base 等绕 free-tier proxy 限制配普通 full node 即可;call_function / call_function_series / get_storage_at 走历史 state 必须配 archive 节点(Alchemy / Quicknode / drpc / Ankr / 自建 erigon)。常用 chain:RPC_URL_1 (mainnet)、RPC_URL_42161 (arbitrum)、RPC_URL_8453 (base)、RPC_URL_10 (optimism)、RPC_URL_137 (polygon)、RPC_URL_56 (bsc) |
RPC_<chainid> |
— | RPC_URL_<chainid> 的兼容别名 |
RPC_URL |
— | 默认链的 JSON-RPC 端点(仅未显式传 network 时生效;显式传 network 推荐用 RPC_URL_<chainid> 避免误绑定) |
REQUEST_TIMEOUT |
10 |
单次请求超时(秒) |
REQUEST_RETRIES |
3 |
重试次数 |
REQUEST_BACKOFF_SECONDS |
0.5 |
退避基数 |
ETHERSCAN_MCP_CACHE_DIR |
~/.cache/etherscan-mcp |
持久化 token metadata + contract name 的目录;落 token_metadata.json 与 contract_names.json,按 (chainid, address) 键。进程重启后避免重新拉同一批 token / 同一批合约名,批量扫地址收益最明显。设空字符串完全禁用持久化。 |
METADATA_FETCH_CONCURRENCY |
5 |
get_transaction_summary 拉 token metadata(symbol/decimals/name)+ contract name 时的线程池并发数。冷启动一笔 tx 涉及 9 个新 token + 17 个未注解地址时,从串行 ~40s 降到 ~6-8s。设大触发更多 429 / rate limit;1 退化回串行。 |
读链类工具(call_function / call_function_series / get_storage_at / detect_proxy / query_logs / get_block_by_number / get_block_time_by_number / get_transaction)在配了对应 RPC_URL_<chainid> 时优先走 RPC;未配则保持原行为,回退 Etherscan module=proxy。例外:call_function_series 永远只走 RPC,因为它的语义就是历史区块序列采样。
例外:call_function / get_storage_at 在传了历史 block_tag(hex / decimal block number)但 RPC 未配时,显式报错而不再回退 module=proxy;call_function_series 是历史序列工具,始终要求 RPC —— Etherscan proxy 对历史 tag 静默忽略并返回 latest state,回退会让历史读看起来"成功了"实际上是 latest,比报错更坑。要走历史 block_tag 必须配 archive 节点(详见 已知限制)。
命名约定
- MCP tools 对外参数用
snake_case。 - 透传给 Etherscan / RPC 的 query 字段保持官方命名(
chainid、startblock、fromBlock等)。