MCP server for blockchain interactions using viem, etherscan and hypersync.
TypeScript
0
83 commits
updated Oct 1, 2026
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.
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 ↓.
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 shows | What you get here |
|---|---|
Unknown contract, unknown call. |
|
Four things go into that panel:
| 🏷️ | Intent | Field labels straight from the protocol, via the ERC-7730 registry — 600+ contracts, 3,000+ selectors, bundled offline and refreshed weekly by a PR |
| 🔎 | Decoded call | Otherwise 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 amounts | Formatted with on-chain decimals and symbol. Unlimited approvals are called out |
| 🧪 | Simulation | eth_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 |
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 ↓
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.
| Tool | |
|---|---|
write_contract | Send a transaction calling a state-changing function, via your wallet. Pass large integers as strings |
send_native_token | Send ETH or a native token |
send_erc20_token | Send an ERC-20, with decimals read on-chain — a decimals you pass must agree |
sign_message | Sign a message — the same bytes on either signer |
check_signing_request | Collect a request still awaiting approval, by its requestId |
wallet_status | Every signer and whether it is ready — phone wallets and the browser page |
pair_phone_wallet | Pair a phone over WalletConnect, returns a QR |
disconnect_phone_wallet | Drop 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.
| Tool | |
|---|---|
read_contract | Read state via view/pure functions — batched (up to 25 calls), no wallet, no gas |
simulate_contract | Simulate without broadcasting, with gas |
simulate_bundle | Simulate several transactions in order, each on the state the last left — approve → swap. Needs ALCHEMY_API_KEY |
get_contract_abi | ABI, with proxy detection and verification status |
get_contract_source_code | Verified source, proxies included |
get_contract_source_file | One file out of the cached source |
is_contract | Contract or EOA |
encode_function_data | Calldata from an ABI and arguments |
get_function_signature | 4-byte selector |
get_event_signature | 32-byte topic0 |
get_error_signature | 4-byte error selector |
| Tool | |
|---|---|
get_balance | Native or ERC-20 balances — batched, up to 25 queries |
get_logs | Query 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_info | Block data |
get_storage_at | Storage slots, with type decoding |
get_gas_price | Current gas — legacy and EIP-1559 |
estimate_gas | Gas for any transaction |
trace_transaction | Call tree, prestate or state diff |
debug_call | Trace a call without broadcasting, through a node at ANVIL_RPC_URL |
get_portfolio | A 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_prices | USD prices from DefiLlama, now or at a past timestamp, up to 25 tokens; the zero address prices the native token |
get_yield_pools | DefiLlama 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.
| Tool | |
|---|---|
resolve_ens_name | Name → address |
reverse_resolve_ens | Address → name |
get_ens_text_record | Text records |
get_ens_avatar | The raw avatar record — URL, data URI or NFT reference, not fetched |
batch_resolve_ens_names | Up to 25 names at once |
ENS tools take mainnet (ENS), linea (Linea Names) or base (Basenames).
| Network | Chain ID | Hypersync |
|---|---|---|
| Ethereum | 1 | ✅ |
| Arbitrum | 42161 | ✅ |
| Avalanche | 43114 | ✅ |
| Base | 8453 | ✅ |
| BNB Chain | 56 | ✅ |
| Gnosis | 100 | ✅ |
| Optimism | 10 | ✅ |
| Polygon | 137 | ✅ |
| zkSync Era | 324 | ✅ |
| Linea | 59144 | ✅ |
| Unichain | 130 | ✅ |
| Monad | 143 | ✅ |
| Robinhood Chain | 4663 | ✅ |
| Arc | 5042 | ✅ |
| Plasma | 9745 | ✅ |
| Ink | 57073 | ✅ |
| Mantle | 5000 | ✅ |
| Celo | 42220 | ✅ |
| HyperEVM | 999 | ✅ |
| Localhost | 1337 | ❌ |
Adding one is a single entry in mcp/src/chains.ts.
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:
| Client | Config file | Or via the UI |
|---|---|---|
| Claude Desktop | macOS ~/Library/Application Support/Claude/claude_desktop_config.jsonWindows %APPDATA%\Claude\claude_desktop_config.jsonLinux ~/.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.jsonglobal 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.jsonnests 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 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>"
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.
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:
| Key | Free from | Without it |
|---|---|---|
WALLETCONNECT_PROJECT_ID | walletconnect | 📱 No phone signing at all. Browser wallet only, so a hosted instance has no way to sign |
ALCHEMY_API_KEYor CUSTOM_RPC | alchemy | No 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_KEY | etherscan | get_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_KEY | envio | get_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
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.
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.
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_TOKENis mandatory and the server refuses to start without it. Anyone who reaches/mcpwhile 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_PORT | Serve over HTTP on this port instead of stdio. Also switches on the hosted guards: no localhost chain, no loopback tracing node |
MCP_HTTP_HOST | Bind address, default 0.0.0.0 |
MCP_TOKEN | The one credential, as a bearer header or through OAuth. Changing it revokes every OAuth token issued against the old one |
MCP_PUBLIC_URL | The address clients reach you on — advertised in OAuth metadata and used for wallet links |
MCP_TRUST_PROXY | Reverse-proxy hops to trust for client IPs in rate limiting. 1 behind Fly or Render; off by default |
MCP_HOSTED | The hosted guards without MCP_HTTP_PORT, for a host assembling its own server from the library |
XDG_CONFIG_HOME | Where pairings, OAuth tokens and the relay token are stored, under web3-tools-mcp/ |
UPSTASH_REDIS_REST_URLUPSTASH_REDIS_REST_TOKEN | Keep sessions and tokens in Redis instead. Needs per-field hash expiry (HEXPIRE: Upstash, or Redis ≥ 7.4) |
WALLET_TOKEN | Relay secret. Unset, a hosted server mints one per boot |
WALLET_SERVER_URL | A separately hosted relay, below |
ANVIL_RPC_URLANVIL_ALLOW_RESET | Tracing node, and whether it may be re-forked — see Tools |
Two things decide whether the pairing survives:
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.
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.
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.
MIT
TypeScript
85.6%
JavaScript
9.5%
HTML
4.5%
MCP server for blockchain interactions using viem, etherscan and hypersync.
TypeScript
0
83 commits
updated Oct 1, 2026
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.
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 ↓.
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 shows | What you get here |
|---|---|
Unknown contract, unknown call. |
|
Four things go into that panel:
| 🏷️ | Intent | Field labels straight from the protocol, via the ERC-7730 registry — 600+ contracts, 3,000+ selectors, bundled offline and refreshed weekly by a PR |
| 🔎 | Decoded call | Otherwise 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 amounts | Formatted with on-chain decimals and symbol. Unlimited approvals are called out |
| 🧪 | Simulation | eth_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 |
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 ↓
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.
| Tool | |
|---|---|
write_contract | Send a transaction calling a state-changing function, via your wallet. Pass large integers as strings |
send_native_token | Send ETH or a native token |
send_erc20_token | Send an ERC-20, with decimals read on-chain — a decimals you pass must agree |
sign_message | Sign a message — the same bytes on either signer |
check_signing_request | Collect a request still awaiting approval, by its requestId |
wallet_status | Every signer and whether it is ready — phone wallets and the browser page |
pair_phone_wallet | Pair a phone over WalletConnect, returns a QR |
disconnect_phone_wallet | Drop 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.
| Tool | |
|---|---|
read_contract | Read state via view/pure functions — batched (up to 25 calls), no wallet, no gas |
simulate_contract | Simulate without broadcasting, with gas |
simulate_bundle | Simulate several transactions in order, each on the state the last left — approve → swap. Needs ALCHEMY_API_KEY |
get_contract_abi | ABI, with proxy detection and verification status |
get_contract_source_code | Verified source, proxies included |
get_contract_source_file | One file out of the cached source |
is_contract | Contract or EOA |
encode_function_data | Calldata from an ABI and arguments |
get_function_signature | 4-byte selector |
get_event_signature | 32-byte topic0 |
get_error_signature | 4-byte error selector |
| Tool | |
|---|---|
get_balance | Native or ERC-20 balances — batched, up to 25 queries |
get_logs | Query 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_info | Block data |
get_storage_at | Storage slots, with type decoding |
get_gas_price | Current gas — legacy and EIP-1559 |
estimate_gas | Gas for any transaction |
trace_transaction | Call tree, prestate or state diff |
debug_call | Trace a call without broadcasting, through a node at ANVIL_RPC_URL |
get_portfolio | A 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_prices | USD prices from DefiLlama, now or at a past timestamp, up to 25 tokens; the zero address prices the native token |
get_yield_pools | DefiLlama 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.
| Tool | |
|---|---|
resolve_ens_name | Name → address |
reverse_resolve_ens | Address → name |
get_ens_text_record | Text records |
get_ens_avatar | The raw avatar record — URL, data URI or NFT reference, not fetched |
batch_resolve_ens_names | Up to 25 names at once |
ENS tools take mainnet (ENS), linea (Linea Names) or base (Basenames).
| Network | Chain ID | Hypersync |
|---|---|---|
| Ethereum | 1 | ✅ |
| Arbitrum | 42161 | ✅ |
| Avalanche | 43114 | ✅ |
| Base | 8453 | ✅ |
| BNB Chain | 56 | ✅ |
| Gnosis | 100 | ✅ |
| Optimism | 10 | ✅ |
| Polygon | 137 | ✅ |
| zkSync Era | 324 | ✅ |
| Linea | 59144 | ✅ |
| Unichain | 130 | ✅ |
| Monad | 143 | ✅ |
| Robinhood Chain | 4663 | ✅ |
| Arc | 5042 | ✅ |
| Plasma | 9745 | ✅ |
| Ink | 57073 | ✅ |
| Mantle | 5000 | ✅ |
| Celo | 42220 | ✅ |
| HyperEVM | 999 | ✅ |
| Localhost | 1337 | ❌ |
Adding one is a single entry in mcp/src/chains.ts.
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:
| Client | Config file | Or via the UI |
|---|---|---|
| Claude Desktop | macOS ~/Library/Application Support/Claude/claude_desktop_config.jsonWindows %APPDATA%\Claude\claude_desktop_config.jsonLinux ~/.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.jsonglobal 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.jsonnests 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 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>"
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.
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:
| Key | Free from | Without it |
|---|---|---|
WALLETCONNECT_PROJECT_ID | walletconnect | 📱 No phone signing at all. Browser wallet only, so a hosted instance has no way to sign |
ALCHEMY_API_KEYor CUSTOM_RPC | alchemy | No 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_KEY | etherscan | get_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_KEY | envio | get_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
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.
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.
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_TOKENis mandatory and the server refuses to start without it. Anyone who reaches/mcpwhile 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_PORT | Serve over HTTP on this port instead of stdio. Also switches on the hosted guards: no localhost chain, no loopback tracing node |
MCP_HTTP_HOST | Bind address, default 0.0.0.0 |
MCP_TOKEN | The one credential, as a bearer header or through OAuth. Changing it revokes every OAuth token issued against the old one |
MCP_PUBLIC_URL | The address clients reach you on — advertised in OAuth metadata and used for wallet links |
MCP_TRUST_PROXY | Reverse-proxy hops to trust for client IPs in rate limiting. 1 behind Fly or Render; off by default |
MCP_HOSTED | The hosted guards without MCP_HTTP_PORT, for a host assembling its own server from the library |
XDG_CONFIG_HOME | Where pairings, OAuth tokens and the relay token are stored, under web3-tools-mcp/ |
UPSTASH_REDIS_REST_URLUPSTASH_REDIS_REST_TOKEN | Keep sessions and tokens in Redis instead. Needs per-field hash expiry (HEXPIRE: Upstash, or Redis ≥ 7.4) |
WALLET_TOKEN | Relay secret. Unset, a hosted server mints one per boot |
WALLET_SERVER_URL | A separately hosted relay, below |
ANVIL_RPC_URLANVIL_ALLOW_RESET | Tracing node, and whether it may be re-forked — see Tools |
Two things decide whether the pairing survives:
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.
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.
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.
MIT
TypeScript
85.6%
JavaScript
9.5%
HTML
4.5%