hajnalben/web3-tools-mcp

MCP server for blockchain interactions using viem, etherscan and hypersync.

TypeScript

0

83 commits

updated Oct 1, 2026

See the code

See what people are saying

SourceMessageScoreDate

Web3 Tools MCP (r/mcp)

I built and host this, so this is self-promotion. **Web3 Tools MCP** is a hosted MCP server that lets your agent work onchain: check balances, value a wallet across chains, look up token prices and yields, and send transactions. It covers 19 EVM chains (Ethereum, Base, Arbitrum, Optimism, Polygon…

3

Oct 2, 2026

README

Web3 Tools logo

Web3 Tools MCP

Everything onchain your agent needs — read, price, simulate, sign — with you holding the keys. 🔐

35 MCP tools across 19 EVM networks, signing in your own wallet on your phone or in the browser. Every transaction is decoded, named and simulated before it reaches your wallet.

npm downloads node license

Claude Desktop, Cursor, VS Code, Windsurf and Claude Code launch it on your machine. One entry in that client's MCP config:

{ "mcpServers": { "web3-tools": { "command": "npx", "args": ["-y", "web3-tools-mcp"] } } }

Each app keeps that config somewhere different — the file paths are below ↓. You need Node.js ≥ 20.19; npx fetches the rest.

ChatGPT, Grok and claude.ai cannot launch anything locally — they only reach a URL. Host it once ↓ and add it as a connector, and there is nothing running on your laptop at all.

It runs with no API keys at all — reads on recent state work on public RPCs, and signing previews still decode. Keys widen what it can reach; see what each unlocks ↓.

🔍 No blind signing

Your agent proposes. You see what it actually does, then approve it in your own wallet. The server never holds a private key.

What a wallet usually showsWhat you get here
To    0x87870Bca…B4fA4E2
Data  0x617ba037000000000…
Value 0

Unknown contract, unknown call.

Supply · Aave V3
Amount to supply   250 USDC
On behalf of       you

−250 USDC  →  +250 aUSDC
184,207 gas · will succeed

Four things go into that panel:

🏷️IntentField labels straight from the protocol, via the ERC-7730 registry — 600+ contracts, 3,000+ selectors, bundled offline and refreshed weekly by a PR
🔎Decoded callOtherwise the function and named arguments, from the verified ABI (Sourcify / Etherscan) — or recovered straight from bytecode with WhatsABI when a contract is unverified. Proxies resolve to their implementation
💰Real amountsFormatted with on-chain decimals and symbol. Unlimited approvals are called out
🧪Simulationeth_simulateV1 reports the gas and every ERC-20 movement the transaction would cause, marked in or out. A transaction that would revert says so before you can sign it

📱 Sign on your phone

Your agent runs wherever it likes. Your keys stay in your pocket.

A phone can't host a local signing page — so it doesn't have to. Point the server at a WalletConnect project id and it talks to your wallet app directly, over WalletConnect's own end-to-end encrypted relay. Nothing to load, nothing to host, no browser anywhere in the path.

npx web3-tools-mcp --walletconnect-project-id YOUR_PROJECT_ID
🌍Works hosted. A deployed server can sign on your phone with nothing running beside you — and now in your browser too, from the page it serves itself
📷Pair once. Ask the agent to run pair_phone_wallet. You get a QR to scan — or, when the agent is already on your phone, a tap-through link straight into your wallet app
💾It sticks. The session is stored on disk (or in Redis when hosted), so it survives restarts and redeploys. Pair once, not once a day
✍️You choose each time. Every signing tool takes a required signWith, so the agent asks whether this one goes to your phone or the browser. A paired phone never quietly claims a request
🧾You still see the decode. The full preview comes back in the tool response — your wallet shows its own summary, so read ours before you approve theirs
🔌Drop it any time. disconnect_phone_wallet drops the wallet you name by account, or every session and pairing when you name none

Works with MetaMask · Rabby · Trust · Coinbase Wallet — anything speaking WalletConnect v2.

☁️ + 📱 The combination that unlocks everything

Because signing no longer needs anything next to you, the server can live in the cloud and still have your phone sign. Host it once and every client you own — laptop, browser, the Claude phone app — drives the same instance, with approvals landing on your phone wherever you are. Hosting takes about a minute ↓

💻 On a laptop? Sign in the wallet extension you already run — zero configuration

Nothing to install and nothing to set up: the server serves a local page, and that page talks to whatever wallet extension your browser already has. The first transaction opens it, you connect MetaMask, Rabby or Coinbase once, and every later request lands in that same tab.

One relay per machine — all your editor sessions share the page, so you connect once and a transaction from any session shows up there. The page and the server are both clients of a local relay on 127.0.0.1:3456 (next free port up to 3460), authenticated with a pairing token carried in the URL fragment (#t=…) of the link the server prints. The token lives in ~/.config/web3-tools-mcp/relay-token (mode 0600) — delete it to rotate.

Works with MetaMask, Rabby, Coinbase Wallet, and any EIP-1193 browser wallet. wallet_status returns the page URL any time you want to open it yourself.

🧰 Tools

35 tools — reads, writes, ENS, logs, tracing

✍️ Transactions & signing

Tool
write_contractSend a transaction calling a state-changing function, via your wallet. Pass large integers as strings
send_native_tokenSend ETH or a native token
send_erc20_tokenSend an ERC-20, with decimals read on-chain — a decimals you pass must agree
sign_messageSign a message — the same bytes on either signer
check_signing_requestCollect a request still awaiting approval, by its requestId
wallet_statusEvery signer and whether it is ready — phone wallets and the browser page
pair_phone_walletPair a phone over WalletConnect, returns a QR
disconnect_phone_walletDrop one paired wallet by account, or every session and pairing

The four signing tools take a required signWith of phone or browser. There is no default on purpose — the agent has to ask you, so a request never lands on a device you are not holding. On localhost only the browser can sign; a phone has no route to your node. With several phones paired, the optional account picks one by address; omitted, the most recently paired is used.

A request not approved within the call comes back with a requestId instead of hanging, and check_signing_request collects it — asking again would put a second prompt in front of your wallet.

📜 Contracts

Tool
read_contractRead state via view/pure functions — batched (up to 25 calls), no wallet, no gas
simulate_contractSimulate without broadcasting, with gas
simulate_bundleSimulate several transactions in order, each on the state the last left — approve → swap. Needs ALCHEMY_API_KEY
get_contract_abiABI, with proxy detection and verification status
get_contract_source_codeVerified source, proxies included
get_contract_source_fileOne file out of the cached source
is_contractContract or EOA
encode_function_dataCalldata from an ABI and arguments
get_function_signature4-byte selector
get_event_signature32-byte topic0
get_error_signature4-byte error selector

⛓️ Chain data

Tool
get_balanceNative or ERC-20 balances — batched, up to 25 queries
get_logsQuery and decode events; Hypersync when the RPC refuses the range. At most 1,000 logs — past that it returns truncated and the nextBlock to continue from. A list in eventArgs matches any of its values
get_block_infoBlock data
get_storage_atStorage slots, with type decoding
get_gas_priceCurrent gas — legacy and EIP-1559
estimate_gasGas for any transaction
trace_transactionCall tree, prestate or state diff
debug_callTrace a call without broadcasting, through a node at ANVIL_RPC_URL
get_portfolioA wallet's native and ERC-20 holdings across chains, valued in USD; tokens DefiLlama cannot price — spam, mostly — are left out. Needs ALCHEMY_API_KEY
get_token_pricesUSD prices from DefiLlama, now or at a past timestamp, up to 25 tokens; the zero address prices the native token
get_yield_poolsDefiLlama yield pools by chain, token and protocol, highest APY first

Tracing prefers an RPC that exposes debug_*, and otherwise uses a node you already run at ANVIL_RPC_URL (default 127.0.0.1:8545, e.g. anvil --fork-url …) — nothing is started for you. Replaying a transaction older than the fork re-forks that node, clearing its state, so it happens only with ANVIL_ALLOW_RESET=1. A hosted server has no default and traces only through an explicit ANVIL_RPC_URL.

🏷️ ENS

Tool
resolve_ens_nameName → address
reverse_resolve_ensAddress → name
get_ens_text_recordText records
get_ens_avatarThe raw avatar record — URL, data URI or NFT reference, not fetched
batch_resolve_ens_namesUp to 25 names at once

ENS tools take mainnet (ENS), linea (Linea Names) or base (Basenames).

🌐 20 networks
NetworkChain IDHypersync
Ethereum1✅
Arbitrum42161✅
Avalanche43114✅
Base8453✅
BNB Chain56✅
Gnosis100✅
Optimism10✅
Polygon137✅
zkSync Era324✅
Linea59144✅
Unichain130✅
Monad143✅
Robinhood Chain4663✅
Arc5042✅
Plasma9745✅
Ink57073✅
Mantle5000✅
Celo42220✅
HyperEVM999✅
Localhost1337❌

Adding one is a single entry in mcp/src/chains.ts.

🔧 Setup

🖥️ Local clients — where the config file lives

Add the entry below to your client's MCP config, then restart the app. Every one of these also has a UI route that creates the file for you, which is usually quicker than finding it:

ClientConfig fileOr via the UI
Claude DesktopmacOS ~/Library/Application Support/Claude/claude_desktop_config.json
Windows %APPDATA%\Claude\claude_desktop_config.json
Linux ~/.config/Claude/claude_desktop_config.json
Settings → Developer → Edit Config
Cursor~/.cursor/mcp.json (all projects)
.cursor/mcp.json (this project)
Settings → Tools & MCP → Add new MCP server
VS Code.vscode/mcp.json (workspace)
or your user profile
Command Palette → MCP: Add Server
Windsurf~/.codeium/windsurf/mcp_config.json
global only, no per-project
Cascade panel → MCP icon
{
  "mcpServers": {
    "web3-tools": {
      "command": "npx",
      "args": ["-y", "web3-tools-mcp"],
      "env": {
        "WALLETCONNECT_PROJECT_ID": "<your project id>",
        "ETHERSCAN_API_KEY": "<your key>"
      }
    }
  }
}

Drop the whole env block if you have no keys yet: the server runs without them, with less reach. 🔑 API keys below says exactly what each one turns on.

⚠️ VS Code is the exception. Its mcp.json nests servers under "servers", not "mcpServers" — paste the block above unchanged and it is silently ignored:

{ "servers": { "web3-tools": { "command": "npx", "args": ["-y", "web3-tools-mcp"] } } }
⌨️ Claude Code
claude mcp add --scope user --transport stdio web3-tools -- npx -y web3-tools-mcp

Or, against a hosted instance:

claude mcp add --transport http web3-tools https://your-host/mcp \
  --header "Authorization: Bearer <MCP_TOKEN>"
🤖 ChatGPT, 🦾 Grok and 🌐 claude.ai — remote only

None of these can launch a local process, so they need a hosted instance: deploy one ↓, then point the client at https://your-host/mcp.

ChatGPT — Settings → Apps & Connectors → Advanced → enable Developer mode, then Create. Give it a name and the server URL. The URL has to include the /mcp path; that is the usual mistake. Pick OAuth and it walks you through the login page the server serves, or pick token and paste MCP_TOKEN. Needs a paid plan.

Grok — grok.com/connectors → New Connector → Custom, then the same URL and authentication. Needs a paid plan.

claude.ai — Settings → Connectors → Add custom connector, then the URL. It uses OAuth, which the server implements against MCP_TOKEN: it shows a page asking for the token and issues one once you paste it.

Once connected, pair your phone and approvals arrive there — no laptop in the loop at all.

🔑 API keys — none required, but they decide how much works

Nothing here is required to start, and the server will not refuse to run without any of it. What you lose is reach, not stability — so here is the honest version:

KeyFree fromWithout it
WALLETCONNECT_PROJECT_IDwalletconnect📱 No phone signing at all. Browser wallet only, so a hosted instance has no way to sign
ALCHEMY_API_KEY
or CUSTOM_RPC
alchemyNo historical state. Public endpoints answer 403 Archive requests require a personal token, which takes out balances, storage and calls at a past block, event ranges, and trace_transaction
ETHERSCAN_API_KEYetherscanget_contract_abi, get_contract_source_code and get_contract_source_file refuse. Signing previews still decode — they try Sourcify first, then recover the ABI from bytecode
HYPERSYNC_API_KEYenvioget_logs asks the RPC first and falls back to Hypersync when the RPC refuses the range. Without a token that fallback is gone, so wide or past ranges depend on your provider's limits

So with nothing configured you get current balances, contract reads, gas, ENS, simulation and browser signing. Add WALLETCONNECT_PROJECT_ID for your phone and one provider key for history, and everything above lights up.

Every key has a matching flag (--etherscan-api-key, …), RPC selection falls back Alchemy → public, and CUSTOM_RPC overrides both, chain by chain. npx web3-tools-mcp --help lists them; .env.example documents each one.

npx web3-tools-mcp --etherscan-api-key KEY --walletconnect-project-id ID

🚀 Hosting

With WalletConnect nothing has to run next to you, so the server can live in the cloud and still have your phone sign. Run your own rather than sharing one: your wallet, your keys, your quota, and no multi-tenant server in the middle that could push someone else's transaction at your phone.

Deploy to Render   Deploy on Fly.io

Render reads render.yaml, generates MCP_TOKEN and asks for WALLETCONNECT_PROJECT_ID — one click, on a paid starter instance with a 1 GB disk so it stays awake and keeps the pairing. MCP_PUBLIC_URL defaults to the service's onrender.com address; set it only for a custom domain.

Fly ships no deploy button, so it is four commands. Worth it for a personal instance: volumes come with any plan, so the pairing survives redeploys without paying for a disk.

fly launch --no-deploy --copy-config   # then set MCP_PUBLIC_URL in fly.toml to https://<app>.fly.dev
fly volumes create data --size 1
fly secrets set MCP_TOKEN=$(openssl rand -hex 24) WALLETCONNECT_PROJECT_ID=<id>
fly deploy

Without the right MCP_PUBLIC_URL the OAuth metadata and the wallet links point somewhere clients cannot reach. Both fly.toml and render.yaml set MCP_TRUST_PROXY=1, so rate limiting sees client addresses rather than the platform's proxy.

🐳 Docker, and connecting a client

There is a plain Dockerfile too, so Railway, Cloud Run or your own box work the same way:

docker build -t web3-tools-mcp .
docker run -p 8080:8080 \
  -e MCP_TOKEN=<long random string> \
  -e MCP_PUBLIC_URL=https://your-host \
  -e WALLETCONNECT_PROJECT_ID=<id> \
  web3-tools-mcp

Then point a client at it:

claude mcp add --transport http web3-tools https://your-host/mcp \
  --header "Authorization: Bearer <MCP_TOKEN>"

Clients that cannot send a header — claude.ai connectors, and the browser and phone apps — use OAuth, which the server implements against the same credential: it shows a page asking for MCP_TOKEN and issues a token once you paste it. No identity provider, no accounts.

🔐 MCP_TOKEN is mandatory and the server refuses to start without it. Anyone who reaches /mcp while your phone is paired can push signing prompts at it. You would still approve each one, but that is a phishing surface, not a feature.

Variable
MCP_HTTP_PORTServe over HTTP on this port instead of stdio. Also switches on the hosted guards: no localhost chain, no loopback tracing node
MCP_HTTP_HOSTBind address, default 0.0.0.0
MCP_TOKENThe one credential, as a bearer header or through OAuth. Changing it revokes every OAuth token issued against the old one
MCP_PUBLIC_URLThe address clients reach you on — advertised in OAuth metadata and used for wallet links
MCP_TRUST_PROXYReverse-proxy hops to trust for client IPs in rate limiting. 1 behind Fly or Render; off by default
MCP_HOSTEDThe hosted guards without MCP_HTTP_PORT, for a host assembling its own server from the library
XDG_CONFIG_HOMEWhere pairings, OAuth tokens and the relay token are stored, under web3-tools-mcp/
UPSTASH_REDIS_REST_URL
UPSTASH_REDIS_REST_TOKEN
Keep sessions and tokens in Redis instead. Needs per-field hash expiry (HEXPIRE: Upstash, or Redis ≥ 7.4)
WALLET_TOKENRelay secret. Unset, a hosted server mints one per boot
WALLET_SERVER_URLA separately hosted relay, below
ANVIL_RPC_URL
ANVIL_ALLOW_RESET
Tracing node, and whether it may be re-forked — see Tools

Two things decide whether the pairing survives:

  • ⏰ Keep the instance awake. Sleeping drops the WalletConnect socket, so the first transaction after an idle period waits for the host to wake — about a minute on a free tier.
  • 💾 Give the session somewhere durable. Most hosts have an ephemeral filesystem, so a plain file is lost on redeploy and you rescan the QR. Attach a volume and point XDG_CONFIG_HOME at it (what render.yaml and fly.toml do), or set UPSTASH_REDIS_REST_URL and UPSTASH_REDIS_REST_TOKEN (Upstash has a free plan).

Render's free Key Value instance is not an option here — no persistence, so the pairing disappears at its next maintenance.

Both signers work on a hosted instance. The signing page is served from the same port as /mcp, so https://your-host/sign/#t=<relay token> is your wallet page — no second service to deploy. The root is left free for a page of your own. Ask the agent for wallet_status to get the link; it is behind MCP_TOKEN and never written to the logs. Unless you set WALLET_TOKEN, the relay secret is minted fresh each boot rather than stored, so the URL changes when you redeploy.

🖇️ Host the signing page separately

The relay is its own package, web3-wallet-relay, and runs anywhere that keeps a Node process alive and supports WebSockets. Its README covers hosting it; run it at this server's version, then point the server at it with WALLET_SERVER_URL and the same WALLET_TOKEN.

Locally nothing changes: the server embeds the same relay.

🔄 How signing works

  1. 🤖 A transaction tool runs. The server decodes and simulates the transaction first.
  2. 📨 It goes to the signer you picked — your phone, or the browser page.
  3. 👀 You read the summary and approve or reject in your wallet.
  4. 🔗 The result, with an explorer link, comes back to the agent.

🛠️ Development

npm install
npm run lint       # Biome; npm run format fixes what it can
npm run typecheck
npm test

Two workspaces: mcp/ is this server, wallet-relay/ is the signing page. CONTRIBUTING.md covers running a checkout against a client, the layout, and how to add a chain or a tool. Releases: CHANGELOG.md.

Requires Node.js ≥ 20.19.

📄 License

MIT

hajnalben/web3-tools-mcp

MCP server for blockchain interactions using viem, etherscan and hypersync.

TypeScript

0

83 commits

updated Oct 1, 2026

See the code

See what people are saying

SourceMessageScoreDate

Web3 Tools MCP (r/mcp)

I built and host this, so this is self-promotion. **Web3 Tools MCP** is a hosted MCP server that lets your agent work onchain: check balances, value a wallet across chains, look up token prices and yields, and send transactions. It covers 19 EVM chains (Ethereum, Base, Arbitrum, Optimism, Polygon…

3

Oct 2, 2026

README

Web3 Tools logo

Web3 Tools MCP

Everything onchain your agent needs — read, price, simulate, sign — with you holding the keys. 🔐

35 MCP tools across 19 EVM networks, signing in your own wallet on your phone or in the browser. Every transaction is decoded, named and simulated before it reaches your wallet.

npm downloads node license

Claude Desktop, Cursor, VS Code, Windsurf and Claude Code launch it on your machine. One entry in that client's MCP config:

{ "mcpServers": { "web3-tools": { "command": "npx", "args": ["-y", "web3-tools-mcp"] } } }

Each app keeps that config somewhere different — the file paths are below ↓. You need Node.js ≥ 20.19; npx fetches the rest.

ChatGPT, Grok and claude.ai cannot launch anything locally — they only reach a URL. Host it once ↓ and add it as a connector, and there is nothing running on your laptop at all.

It runs with no API keys at all — reads on recent state work on public RPCs, and signing previews still decode. Keys widen what it can reach; see what each unlocks ↓.

🔍 No blind signing

Your agent proposes. You see what it actually does, then approve it in your own wallet. The server never holds a private key.

What a wallet usually showsWhat you get here
To    0x87870Bca…B4fA4E2
Data  0x617ba037000000000…
Value 0

Unknown contract, unknown call.

Supply · Aave V3
Amount to supply   250 USDC
On behalf of       you

−250 USDC  →  +250 aUSDC
184,207 gas · will succeed

Four things go into that panel:

🏷️IntentField labels straight from the protocol, via the ERC-7730 registry — 600+ contracts, 3,000+ selectors, bundled offline and refreshed weekly by a PR
🔎Decoded callOtherwise the function and named arguments, from the verified ABI (Sourcify / Etherscan) — or recovered straight from bytecode with WhatsABI when a contract is unverified. Proxies resolve to their implementation
💰Real amountsFormatted with on-chain decimals and symbol. Unlimited approvals are called out
🧪Simulationeth_simulateV1 reports the gas and every ERC-20 movement the transaction would cause, marked in or out. A transaction that would revert says so before you can sign it

📱 Sign on your phone

Your agent runs wherever it likes. Your keys stay in your pocket.

A phone can't host a local signing page — so it doesn't have to. Point the server at a WalletConnect project id and it talks to your wallet app directly, over WalletConnect's own end-to-end encrypted relay. Nothing to load, nothing to host, no browser anywhere in the path.

npx web3-tools-mcp --walletconnect-project-id YOUR_PROJECT_ID
🌍Works hosted. A deployed server can sign on your phone with nothing running beside you — and now in your browser too, from the page it serves itself
📷Pair once. Ask the agent to run pair_phone_wallet. You get a QR to scan — or, when the agent is already on your phone, a tap-through link straight into your wallet app
💾It sticks. The session is stored on disk (or in Redis when hosted), so it survives restarts and redeploys. Pair once, not once a day
✍️You choose each time. Every signing tool takes a required signWith, so the agent asks whether this one goes to your phone or the browser. A paired phone never quietly claims a request
🧾You still see the decode. The full preview comes back in the tool response — your wallet shows its own summary, so read ours before you approve theirs
🔌Drop it any time. disconnect_phone_wallet drops the wallet you name by account, or every session and pairing when you name none

Works with MetaMask · Rabby · Trust · Coinbase Wallet — anything speaking WalletConnect v2.

☁️ + 📱 The combination that unlocks everything

Because signing no longer needs anything next to you, the server can live in the cloud and still have your phone sign. Host it once and every client you own — laptop, browser, the Claude phone app — drives the same instance, with approvals landing on your phone wherever you are. Hosting takes about a minute ↓

💻 On a laptop? Sign in the wallet extension you already run — zero configuration

Nothing to install and nothing to set up: the server serves a local page, and that page talks to whatever wallet extension your browser already has. The first transaction opens it, you connect MetaMask, Rabby or Coinbase once, and every later request lands in that same tab.

One relay per machine — all your editor sessions share the page, so you connect once and a transaction from any session shows up there. The page and the server are both clients of a local relay on 127.0.0.1:3456 (next free port up to 3460), authenticated with a pairing token carried in the URL fragment (#t=…) of the link the server prints. The token lives in ~/.config/web3-tools-mcp/relay-token (mode 0600) — delete it to rotate.

Works with MetaMask, Rabby, Coinbase Wallet, and any EIP-1193 browser wallet. wallet_status returns the page URL any time you want to open it yourself.

🧰 Tools

35 tools — reads, writes, ENS, logs, tracing

✍️ Transactions & signing

Tool
write_contractSend a transaction calling a state-changing function, via your wallet. Pass large integers as strings
send_native_tokenSend ETH or a native token
send_erc20_tokenSend an ERC-20, with decimals read on-chain — a decimals you pass must agree
sign_messageSign a message — the same bytes on either signer
check_signing_requestCollect a request still awaiting approval, by its requestId
wallet_statusEvery signer and whether it is ready — phone wallets and the browser page
pair_phone_walletPair a phone over WalletConnect, returns a QR
disconnect_phone_walletDrop one paired wallet by account, or every session and pairing

The four signing tools take a required signWith of phone or browser. There is no default on purpose — the agent has to ask you, so a request never lands on a device you are not holding. On localhost only the browser can sign; a phone has no route to your node. With several phones paired, the optional account picks one by address; omitted, the most recently paired is used.

A request not approved within the call comes back with a requestId instead of hanging, and check_signing_request collects it — asking again would put a second prompt in front of your wallet.

📜 Contracts

Tool
read_contractRead state via view/pure functions — batched (up to 25 calls), no wallet, no gas
simulate_contractSimulate without broadcasting, with gas
simulate_bundleSimulate several transactions in order, each on the state the last left — approve → swap. Needs ALCHEMY_API_KEY
get_contract_abiABI, with proxy detection and verification status
get_contract_source_codeVerified source, proxies included
get_contract_source_fileOne file out of the cached source
is_contractContract or EOA
encode_function_dataCalldata from an ABI and arguments
get_function_signature4-byte selector
get_event_signature32-byte topic0
get_error_signature4-byte error selector

⛓️ Chain data

Tool
get_balanceNative or ERC-20 balances — batched, up to 25 queries
get_logsQuery and decode events; Hypersync when the RPC refuses the range. At most 1,000 logs — past that it returns truncated and the nextBlock to continue from. A list in eventArgs matches any of its values
get_block_infoBlock data
get_storage_atStorage slots, with type decoding
get_gas_priceCurrent gas — legacy and EIP-1559
estimate_gasGas for any transaction
trace_transactionCall tree, prestate or state diff
debug_callTrace a call without broadcasting, through a node at ANVIL_RPC_URL
get_portfolioA wallet's native and ERC-20 holdings across chains, valued in USD; tokens DefiLlama cannot price — spam, mostly — are left out. Needs ALCHEMY_API_KEY
get_token_pricesUSD prices from DefiLlama, now or at a past timestamp, up to 25 tokens; the zero address prices the native token
get_yield_poolsDefiLlama yield pools by chain, token and protocol, highest APY first

Tracing prefers an RPC that exposes debug_*, and otherwise uses a node you already run at ANVIL_RPC_URL (default 127.0.0.1:8545, e.g. anvil --fork-url …) — nothing is started for you. Replaying a transaction older than the fork re-forks that node, clearing its state, so it happens only with ANVIL_ALLOW_RESET=1. A hosted server has no default and traces only through an explicit ANVIL_RPC_URL.

🏷️ ENS

Tool
resolve_ens_nameName → address
reverse_resolve_ensAddress → name
get_ens_text_recordText records
get_ens_avatarThe raw avatar record — URL, data URI or NFT reference, not fetched
batch_resolve_ens_namesUp to 25 names at once

ENS tools take mainnet (ENS), linea (Linea Names) or base (Basenames).

🌐 20 networks
NetworkChain IDHypersync
Ethereum1✅
Arbitrum42161✅
Avalanche43114✅
Base8453✅
BNB Chain56✅
Gnosis100✅
Optimism10✅
Polygon137✅
zkSync Era324✅
Linea59144✅
Unichain130✅
Monad143✅
Robinhood Chain4663✅
Arc5042✅
Plasma9745✅
Ink57073✅
Mantle5000✅
Celo42220✅
HyperEVM999✅
Localhost1337❌

Adding one is a single entry in mcp/src/chains.ts.

🔧 Setup

🖥️ Local clients — where the config file lives

Add the entry below to your client's MCP config, then restart the app. Every one of these also has a UI route that creates the file for you, which is usually quicker than finding it:

ClientConfig fileOr via the UI
Claude DesktopmacOS ~/Library/Application Support/Claude/claude_desktop_config.json
Windows %APPDATA%\Claude\claude_desktop_config.json
Linux ~/.config/Claude/claude_desktop_config.json
Settings → Developer → Edit Config
Cursor~/.cursor/mcp.json (all projects)
.cursor/mcp.json (this project)
Settings → Tools & MCP → Add new MCP server
VS Code.vscode/mcp.json (workspace)
or your user profile
Command Palette → MCP: Add Server
Windsurf~/.codeium/windsurf/mcp_config.json
global only, no per-project
Cascade panel → MCP icon
{
  "mcpServers": {
    "web3-tools": {
      "command": "npx",
      "args": ["-y", "web3-tools-mcp"],
      "env": {
        "WALLETCONNECT_PROJECT_ID": "<your project id>",
        "ETHERSCAN_API_KEY": "<your key>"
      }
    }
  }
}

Drop the whole env block if you have no keys yet: the server runs without them, with less reach. 🔑 API keys below says exactly what each one turns on.

⚠️ VS Code is the exception. Its mcp.json nests servers under "servers", not "mcpServers" — paste the block above unchanged and it is silently ignored:

{ "servers": { "web3-tools": { "command": "npx", "args": ["-y", "web3-tools-mcp"] } } }
⌨️ Claude Code
claude mcp add --scope user --transport stdio web3-tools -- npx -y web3-tools-mcp

Or, against a hosted instance:

claude mcp add --transport http web3-tools https://your-host/mcp \
  --header "Authorization: Bearer <MCP_TOKEN>"
🤖 ChatGPT, 🦾 Grok and 🌐 claude.ai — remote only

None of these can launch a local process, so they need a hosted instance: deploy one ↓, then point the client at https://your-host/mcp.

ChatGPT — Settings → Apps & Connectors → Advanced → enable Developer mode, then Create. Give it a name and the server URL. The URL has to include the /mcp path; that is the usual mistake. Pick OAuth and it walks you through the login page the server serves, or pick token and paste MCP_TOKEN. Needs a paid plan.

Grok — grok.com/connectors → New Connector → Custom, then the same URL and authentication. Needs a paid plan.

claude.ai — Settings → Connectors → Add custom connector, then the URL. It uses OAuth, which the server implements against MCP_TOKEN: it shows a page asking for the token and issues one once you paste it.

Once connected, pair your phone and approvals arrive there — no laptop in the loop at all.

🔑 API keys — none required, but they decide how much works

Nothing here is required to start, and the server will not refuse to run without any of it. What you lose is reach, not stability — so here is the honest version:

KeyFree fromWithout it
WALLETCONNECT_PROJECT_IDwalletconnect📱 No phone signing at all. Browser wallet only, so a hosted instance has no way to sign
ALCHEMY_API_KEY
or CUSTOM_RPC
alchemyNo historical state. Public endpoints answer 403 Archive requests require a personal token, which takes out balances, storage and calls at a past block, event ranges, and trace_transaction
ETHERSCAN_API_KEYetherscanget_contract_abi, get_contract_source_code and get_contract_source_file refuse. Signing previews still decode — they try Sourcify first, then recover the ABI from bytecode
HYPERSYNC_API_KEYenvioget_logs asks the RPC first and falls back to Hypersync when the RPC refuses the range. Without a token that fallback is gone, so wide or past ranges depend on your provider's limits

So with nothing configured you get current balances, contract reads, gas, ENS, simulation and browser signing. Add WALLETCONNECT_PROJECT_ID for your phone and one provider key for history, and everything above lights up.

Every key has a matching flag (--etherscan-api-key, …), RPC selection falls back Alchemy → public, and CUSTOM_RPC overrides both, chain by chain. npx web3-tools-mcp --help lists them; .env.example documents each one.

npx web3-tools-mcp --etherscan-api-key KEY --walletconnect-project-id ID

🚀 Hosting

With WalletConnect nothing has to run next to you, so the server can live in the cloud and still have your phone sign. Run your own rather than sharing one: your wallet, your keys, your quota, and no multi-tenant server in the middle that could push someone else's transaction at your phone.

Deploy to Render   Deploy on Fly.io

Render reads render.yaml, generates MCP_TOKEN and asks for WALLETCONNECT_PROJECT_ID — one click, on a paid starter instance with a 1 GB disk so it stays awake and keeps the pairing. MCP_PUBLIC_URL defaults to the service's onrender.com address; set it only for a custom domain.

Fly ships no deploy button, so it is four commands. Worth it for a personal instance: volumes come with any plan, so the pairing survives redeploys without paying for a disk.

fly launch --no-deploy --copy-config   # then set MCP_PUBLIC_URL in fly.toml to https://<app>.fly.dev
fly volumes create data --size 1
fly secrets set MCP_TOKEN=$(openssl rand -hex 24) WALLETCONNECT_PROJECT_ID=<id>
fly deploy

Without the right MCP_PUBLIC_URL the OAuth metadata and the wallet links point somewhere clients cannot reach. Both fly.toml and render.yaml set MCP_TRUST_PROXY=1, so rate limiting sees client addresses rather than the platform's proxy.

🐳 Docker, and connecting a client

There is a plain Dockerfile too, so Railway, Cloud Run or your own box work the same way:

docker build -t web3-tools-mcp .
docker run -p 8080:8080 \
  -e MCP_TOKEN=<long random string> \
  -e MCP_PUBLIC_URL=https://your-host \
  -e WALLETCONNECT_PROJECT_ID=<id> \
  web3-tools-mcp

Then point a client at it:

claude mcp add --transport http web3-tools https://your-host/mcp \
  --header "Authorization: Bearer <MCP_TOKEN>"

Clients that cannot send a header — claude.ai connectors, and the browser and phone apps — use OAuth, which the server implements against the same credential: it shows a page asking for MCP_TOKEN and issues a token once you paste it. No identity provider, no accounts.

🔐 MCP_TOKEN is mandatory and the server refuses to start without it. Anyone who reaches /mcp while your phone is paired can push signing prompts at it. You would still approve each one, but that is a phishing surface, not a feature.

Variable
MCP_HTTP_PORTServe over HTTP on this port instead of stdio. Also switches on the hosted guards: no localhost chain, no loopback tracing node
MCP_HTTP_HOSTBind address, default 0.0.0.0
MCP_TOKENThe one credential, as a bearer header or through OAuth. Changing it revokes every OAuth token issued against the old one
MCP_PUBLIC_URLThe address clients reach you on — advertised in OAuth metadata and used for wallet links
MCP_TRUST_PROXYReverse-proxy hops to trust for client IPs in rate limiting. 1 behind Fly or Render; off by default
MCP_HOSTEDThe hosted guards without MCP_HTTP_PORT, for a host assembling its own server from the library
XDG_CONFIG_HOMEWhere pairings, OAuth tokens and the relay token are stored, under web3-tools-mcp/
UPSTASH_REDIS_REST_URL
UPSTASH_REDIS_REST_TOKEN
Keep sessions and tokens in Redis instead. Needs per-field hash expiry (HEXPIRE: Upstash, or Redis ≥ 7.4)
WALLET_TOKENRelay secret. Unset, a hosted server mints one per boot
WALLET_SERVER_URLA separately hosted relay, below
ANVIL_RPC_URL
ANVIL_ALLOW_RESET
Tracing node, and whether it may be re-forked — see Tools

Two things decide whether the pairing survives:

  • ⏰ Keep the instance awake. Sleeping drops the WalletConnect socket, so the first transaction after an idle period waits for the host to wake — about a minute on a free tier.
  • 💾 Give the session somewhere durable. Most hosts have an ephemeral filesystem, so a plain file is lost on redeploy and you rescan the QR. Attach a volume and point XDG_CONFIG_HOME at it (what render.yaml and fly.toml do), or set UPSTASH_REDIS_REST_URL and UPSTASH_REDIS_REST_TOKEN (Upstash has a free plan).

Render's free Key Value instance is not an option here — no persistence, so the pairing disappears at its next maintenance.

Both signers work on a hosted instance. The signing page is served from the same port as /mcp, so https://your-host/sign/#t=<relay token> is your wallet page — no second service to deploy. The root is left free for a page of your own. Ask the agent for wallet_status to get the link; it is behind MCP_TOKEN and never written to the logs. Unless you set WALLET_TOKEN, the relay secret is minted fresh each boot rather than stored, so the URL changes when you redeploy.

🖇️ Host the signing page separately

The relay is its own package, web3-wallet-relay, and runs anywhere that keeps a Node process alive and supports WebSockets. Its README covers hosting it; run it at this server's version, then point the server at it with WALLET_SERVER_URL and the same WALLET_TOKEN.

Locally nothing changes: the server embeds the same relay.

🔄 How signing works

  1. 🤖 A transaction tool runs. The server decodes and simulates the transaction first.
  2. 📨 It goes to the signer you picked — your phone, or the browser page.
  3. 👀 You read the summary and approve or reject in your wallet.
  4. 🔗 The result, with an explorer link, comes back to the agent.

🛠️ Development

npm install
npm run lint       # Biome; npm run format fixes what it can
npm run typecheck
npm test

Two workspaces: mcp/ is this server, wallet-relay/ is the signing page. CONTRIBUTING.md covers running a checkout against a client, the layout, and how to add a chain or a tool. Releases: CHANGELOG.md.

Requires Node.js ≥ 20.19.

📄 License

MIT

Languages

TypeScript

85.6%

JavaScript

9.5%

HTML

4.5%