quai-mcp-server
# quai-mcp-server
An MCP (Model Context Protocol) server that exposes Quai Network chain data and read-only interaction tools to AI clients like Claude Desktop and Claude Code. Built with the official [`@modelcontextprotocol/sdk`](https://github.com/modelcontextprotocol/typescript-sdk) and [`quais`](https://github.com/dominant-strategies/quais.js), Quai's ethers-like SDK.
## What is Quai Network, in plain terms
Quai is a proof-of-work, EVM-compatible Layer 1 that scales by **sharding**: instead of one chain doing all the work, it splits into many chains arranged in a hierarchy.
```
Prime chain (1)
/ | \
Region Region Region <- "Cyprus", "Paxos", "Hydra"
/ | \ / | \ / | \
Zone Zone Zone ... 9 Zone chains total
```
- **Prime** is the single top-level chain. Every miner mines Prime; it settles state across the whole network but doesn't process user transactions directly.
- **Region** chains (currently Cyprus, Paxos, Hydra) sit beneath Prime, aggregating their Zones.
- **Zone** chains (Cyprus1/2/3, Paxos1/2/3, Hydra1/2/3 — 9 today, more can be added as the network grows) are where the actual EVM lives: user transactions, contracts, balances, everything.
Unlike sharding designs that split security along with data, Quai keeps security unified across the whole hierarchy while only the data/throughput is split — Prime and Region chains merge-mine with the Zones beneath them.
The part that matters most for tooling: **every Quai address is location-aware**. The address's own bytes encode which single Zone it lives in (and whether it's on the QUAI ledger, account-based like Ethereum, or the Qi ledger, UTXO-based like Bitcoin). An address on Cyprus1 only exists on Cyprus1 — you can't ask Paxos2 about it. That's why several tools below either resolve the zone for you automatically, or ask you to specify one explicitly.
## Tools
### Read-only
| Tool | What it does |
|---|---|
| `get_balance` | QUAI balance for an address. Zone is resolved automatically from the address. |
| `get_block` | Block details by number/hash/tag. Requires a shard/zone, since block numbers aren't globally unique across chains. |
| `get_transaction` | Transaction + receipt by hash, including which zone it landed on. |
| `resolve_zone` | Given an address, report its zone, region, and ledger (Quai vs Qi) — no network call. |
| `call_contract` | Read-only `eth_call`-style contract call (address + ABI fragment + method + args). Zone resolved from the contract address. |
| `search_docs` | Search a small curated offline index of Quai docs and get back snippets + links. |
| `get_conversion_rate` | Quote a conversion between QUAI and Qi, Quai's own two native ledgers — this is Quai's built-in "swap," not a third-party DEX (none is known to be confirmed on Quai). |
None of these can move funds, sign anything, or change on-chain state.
### Wallets (custodial: encrypted, named, password-protected)
| Tool | What it does |
|---|---|
| `create_wallet` | Generates a new QUAI-ledger private key + address, ground to land in a chosen zone (default `cyprus1`), and stores it **encrypted** under a name and password. By default (`pairQiWallet: true`) also creates a matching Qi wallet under the same name/password/zone, so a QUAI→Qi conversion always has somewhere real to land — set `pairQiWallet: false` for a QUAI-only wallet. |
| `import_wallet` | Same encrypted storage, for a QUAI-ledger private key you already have. |
| `create_qi_wallet` | Generates a new **Qi**-ledger (UTXO-based) wallet — an HD wallet with a mnemonic, since Qi needs address derivation and UTXO scanning, not a single keypair. Encrypted the same way. |
| `import_qi_wallet` | Same encrypted storage, for a Qi mnemonic phrase you already have. |
| `list_wallets` | Lists stored wallets of both kinds (name, ledger, address, zone). No password needed — only spending or checking a Qi balance needs it. |
| `send_transaction` | Signs and sends QUAI from a stored QUAI wallet. Two-step confirmation (see below). Sender/recipient can be in different zones — that's an external transaction (ETX), handled automatically by the network. If the recipient is a Qi address, this doubles as the QUAI→Qi conversion path (see below). |
| `get_qi_balance` | Total and spendable Qi balance for a Qi wallet. Needs the password — see "Why Qi needs the password" below. |
| `convert_qi_to_quai` | Converts Qi held in a Qi wallet into QUAI, sent to a QUAI address. Two-step confirmation, same pattern as `send_transaction`. |
| `get_qi_payment_code` | Gets a Qi wallet's reusable BIP-47 payment code — what you hand someone so they can `send_qi` to *you*. Needs the password (purely local, no network call). |
| `send_qi` | Sends Qi from a Qi wallet to a recipient's payment code (not a plain address) — see "Qi → Qi sending" below. Two-step confirmation, same pattern as the other write tools. |
This server holds keys on your behalf once you create or import a wallet — it's custodial in that narrow, local sense, the same way a `geth` keystore or MetaMask's local vault is. It does **not** operate as a hosted service for other people's funds; everything lives in a directory on the machine running the server, encrypted with a password only you know.
**How the encryption works**: each wallet is a private key in the standard [Web3 Secret Storage (V3 keystore)](https://github.com/ethereum/wiki/wiki/Web3-Secret-Storage-Definition) format — the same format `geth` and MetaMask use — via `quais`' `encryptKeystoreJson`. Concretely: the password is stretched with **scrypt** (`N=2^17, r=8, p=1`, the standard "expensive" cost parameters — this deliberately makes each password guess slow), the private key is encrypted with **AES-128-CTR**, and a **MAC** over the ciphertext detects a wrong password (or a tampered file) before any key material is derived from it. This is a well-reviewed, widely-deployed scheme; nothing here is custom cryptography.
**Where wallets live**: `~/.quai-mcp-server/wallets/` by default (override with `QUAI_WALLET_DIR`) — QUAI wallets as `<name>.json`, Qi wallets as `<name>.qi.json`. The directory is created `0700` and each keystore file `0600` (owner-read/write only, best-effort on non-POSIX platforms) — enforced explicitly after creation, not just left to the process umask. The address is stored in the clear in both cases (it's public information; that's how `list_wallets` and QUAI-side previews work without a password), but the private key (or mnemonic, for Qi) is never written, logged, or returned in plaintext by any tool.
**Naming**: a name identifies at most one QUAI wallet *and* at most one Qi wallet — they're independent keystores (different files, different secrets, entirely unrelated key material) that happen to share a label. You can't create two QUAI wallets (or two Qi wallets) with the same name, but reusing a QUAI wallet's name for a Qi wallet is exactly how `create_wallet`'s pairing works, and `create_qi_wallet`/`import_qi_wallet` allow it deliberately for the same reason.
**Qi wallets are HD wallets under the hood, but this server only ever stores the mnemonic** — never the derived address tree or any UTXO/scan state. `create_qi_wallet`/`import_qi_wallet` encrypt `{address, privateKey, mnemonic}` via the exact same `encryptKeystoreJson` call as the QUAI side (the `address`/`privateKey` fields there are just the wallet's first derived address, present so the file is a normal, valid V3 keystore); the meaningful secret is the mnemonic. Every later operation (`get_qi_balance`, `convert_qi_to_quai`) reconstructs a fresh `QiHDWallet` from that mnemonic and re-derives the same receiving address on demand -- deterministically, since HD derivation for a fixed account/zone always produces the same address. This was verified directly: exporting a wallet's mnemonic and re-importing it under a different name reproduced the identical address. The tradeoff is that every Qi operation re-derives from scratch rather than reading a cache, which is simpler to reason about and can't drift from what the mnemonic actually implies, at the cost of needing the password more often than the QUAI side does (see below).
**Why Qi needs the password more often**: QUAI's `get_balance` reads a public account balance straight from the chain -- no secret needed. Qi has no such thing: a "balance" is the sum of unspent transaction outputs (UTXOs) that belong to addresses only the wallet's mnemonic can derive, so computing it at all means reconstructing the wallet first. That's why `get_qi_balance` takes a password (QUAI's `get_balance` doesn't), and why `convert_qi_to_quai`'s preview step can quote a conversion rate but can't confirm you actually have enough Qi to spend -- that check only happens once the password arrives at the confirm step.
**Password rules**: minimum 8 characters, checked before anything is encrypted. There's no separate rate-limiting on wrong-password attempts — scrypt's cost parameters already make each guess computationally expensive, which is the standard defense for this kind of local keystore.
**Confirmation flow for `send_transaction`, `convert_qi_to_quai`, and `send_qi`**: all three always require two calls, and only the second one needs the password.
1. Call with the destination and amount (`walletName`/`to`/`amount` for `send_transaction`; `walletName`/`recipientPaymentCode`/`amount`/`destinationZone` for `send_qi`; the `to`-shaped version for `convert_qi_to_quai`) — no password required yet. Nothing is broadcast. You get back a preview — resolved zones, an estimate where one exists (gas for a send, converted amount for a conversion; `send_qi` has none, since it's a 1:1 transfer), and a `confirmationToken` valid for 2 minutes.
2. Call again with the *same* parameters, plus `confirm: true`, that `confirmationToken`, and the wallet's `password`. Only then is the key/mnemonic decrypted and the transaction actually signed and sent.
A token is single-use and tied to the exact parameters previewed — if anything changes, the token expired, or it was already used, step 2 fails with a clear error and you preview again. This works the same way regardless of whether the MCP client itself has a tool-approval UI, so it's a real gate rather than relying on the client to provide one. A wrong password fails cleanly (`Incorrect password for wallet "..."`) without leaking whether the token/params were otherwise valid.
There is intentionally no `export_wallet`/"show private key or mnemonic" tool — once a secret is in the store, the only way out through this server is to sign with it.
**QUAI ↔ Qi conversion ("swap")**: Quai has a native, protocol-level conversion between its two ledgers — QUAI (account-based) and Qi (UTXO-based, like Bitcoin) — with an on-chain exchange rate, not a third-party DEX. `get_conversion_rate` quotes either direction, with no wallet needed. Both execution directions are now implemented:
- **QUAI → Qi**: just a normal `send_transaction` to a Qi-ledger address (e.g. one from `create_qi_wallet`). The tool detects this automatically (`isConversion: true` in the preview) and shows the estimated Qi received alongside the usual gas/balance info.
- **Qi → QUAI**: `convert_qi_to_quai`, using `quais`' `QiHDWallet.convertToQuai` under the hood, following the same preview/confirm/password pattern as `send_transaction`.
**Qi → Qi sending**: Qi wallets don't send to each other's addresses directly. Instead, each Qi wallet has a reusable **BIP-47 payment code** (`get_qi_payment_code`) — share that the way you'd share an address, but a fresh one-time address is derived from it for every payment, for privacy. To send, the sender "opens a channel" with the recipient's payment code (`send_qi` does this automatically) — this is pure local ECDH between the two payment codes, deterministic and reproducible, no on-chain action or persisted state involved. The catch is on the *receiving* end: those pairwise-derived addresses aren't part of the wallet's normal deterministic address sequence, so nothing will find funds sent that way unless you tell it to look. Concretely: after someone pays your Qi wallet via payment code, pass **their** payment code into `get_qi_balance`'s `counterpartyPaymentCodes` — it opens that same channel and includes it in the balance. There's no notification mechanism (on-chain or otherwise) that tells the receiver a payment-code payment arrived; the two sides have to already know about each other out of band, the same way you'd need to know an address before checking its balance. `send_qi` also supports cross-zone sends (a `destinationZone` separate from the sender's own zone), the same way `send_transaction`'s ETX and `QiHDWallet`'s own zone model do.
One known rough edge: `send_qi`'s preview step doesn't validate the payment code's format up front (there's no exported validator to check it against), so a malformed code will preview fine and only fail once you confirm — safely (nothing is sent, no funds are at risk), just later than ideal.
Not yet implemented: `deploy_contract`, `request_faucet`.
**Honesty about what's tested here, updated**: the full `send_qi` / payment-code loop was verified live against mainnet with two real wallets — a real, correctly-formatted BIP-47 payment code (`PM8T...`) was generated and confirmed deterministic across calls, a preview correctly detected cross-zone vs. same-zone, a confirm against an empty wallet failed with a genuine SDK error (`No Qi available in zone`) rather than crashing, and `get_qi_balance` correctly isolated an invalid counterparty payment code into `rejectedPaymentCodes` without failing the whole call. What's still unverified, for the same reason as everywhere else in this doc: an actual payment-code send completing between two funded wallets, since that needs real Qi and wasn't done without being asked.
**Honesty about what's tested here**: everything above was exercised against live mainnet, including a determinism check (exporting a Qi wallet's mnemonic and reimporting it under a different name reproduced the identical address) and real error paths (wrong password, insufficient QUAI gas, and a real `QiHDWallet` error -- `No Qi available in zone` -- when attempting to convert from an empty Qi wallet). What has **not** been exercised is a `convert_qi_to_quai` or QUAI→Qi conversion actually completing against a wallet that holds real funds, since that requires spending real money and wasn't done without being asked to.
## Install
```bash
npm install
npm run build
```
Or run directly without installing, once published:
```bash
npx quai-mcp-server
```
### Requirements
- Node.js 18+
### Configuration (environment variables)
All optional — sensible defaults point at Quai mainnet.
| Variable | Default | Purpose |
|---|---|---|
| `QUAI_MAINNET_RPC_URL` | `https://rpc.quai.network` | Mainnet RPC gateway used by tools when `network: "mainnet"` (the default). |
| `QUAI_TESTNET_RPC_URL` | `https://orchard.rpc.quai.network` | Orchard testnet RPC gateway used when `network: "testnet"`. |
| `QUAI_WALLET_DIR` | `~/.quai-mcp-server/wallets` | Where encrypted wallet keystore files are stored. |
Every tool also accepts a `network` argument (`"mainnet"` or `"testnet"`) per call, so a client can query either network without restarting the server.
**On keys**: see "Wallets" above. Keys only exist as plaintext in memory for the duration of a `create_wallet`/`import_wallet`/`send_transaction` call that needs them — never on disk, never logged. Treat `QUAI_WALLET_DIR` (and whatever machine runs this server) as you would any other local secret store: anyone with filesystem access to that directory and enough compute to brute-force a weak password can eventually decrypt a wallet, same as a local `geth` keystore or MetaMask vault.
## Register with Claude Desktop
Add this to your Claude Desktop MCP config (`claude_desktop_config.json` — on macOS: `~/Library/Application Support/Claude/claude_desktop_config.json`):
```json
{
"mcpServers": {
"quai": {
"command": "npx",
"args": ["quai-mcp-server"]
}
}
}
```
Or, if you've cloned and built this repo locally instead of using a published package:
```json
{
"mcpServers": {
"quai": {
"command": "node",
"args": ["/absolute/path/to/quai-mcp-server/dist/index.js"]
}
}
}
```
To point it at testnet by default, add an `env` block:
```json
{
"mcpServers": {
"quai": {
"command": "npx",
"args": ["quai-mcp-server"],
"env": {
"QUAI_TESTNET_RPC_URL": "https://orchard.rpc.quai.network"
}
}
}
}
```
(then pass `"network": "testnet"` in individual tool calls — the env vars set the endpoint, not the default network per call).
## Register with Claude Code
```bash
claude mcp add quai -- npx quai-mcp-server
```
or, for a local build:
```bash
claude mcp add quai -- node /absolute/path/to/quai-mcp-server/dist/index.js
```
## Development
```bash
npm run dev # tsc --watch
npm run build # one-shot build to dist/
npm start # run the built server directly (stdio) -- mainly useful for manual smoke tests
```
The server speaks MCP over stdio only in v1; there is no HTTP transport.
## Design notes
- **quais over raw RPC**: every tool goes through the `quais` SDK's `JsonRpcProvider`, `Contract`, and address utilities rather than hand-rolled `eth_`/`quai_` JSON-RPC calls, so zone resolution, response formatting, and error shapes stay consistent with the rest of the Quai ecosystem.
- **One provider, many zones**: a single `JsonRpcProvider` pointed at a base gateway URL (e.g. `https://rpc.quai.network`) auto-discovers active zones from the Prime chain and routes each call to the right one — most tools never construct a per-zone URL.
- **Custody, done with standard tools, not custom crypto**: wallets are stored using `quais`' implementation of the Ethereum V3 keystore format (scrypt + AES-128-CTR + MAC) — the same well-reviewed scheme `geth` and MetaMask use — rather than anything hand-rolled. See "Wallets" above for the full model.
- **Errors are text, not stack traces**: RPC/contract errors are caught and rewritten into short, specific messages (e.g. "Contract call reverted: ...", "Insufficient funds: ...", "Incorrect password for wallet...", "not a validly checksummed Quai address") instead of leaking raw exception objects to the model.
- **Confirmation is a real gate, not just a client hint**: write tools are annotated `readOnlyHint: false` (and `destructiveHint: true` for sending) so MCP clients with their own approval UI show one, but `send_transaction` additionally enforces its own preview → token → password handshake server-side (`src/confirmations.ts` for the token, `src/walletStore.ts` + `decryptKeystoreJson` for the password), so it's still safe to call from a client with no approval UI at all.
- **Password only needed once, at the last moment**: previewing a send resolves the wallet's address straight from the unencrypted part of its keystore file and uses a `VoidSigner` (a quais signer that can estimate gas but not sign) to estimate cost — no decryption, no password. Only the final `confirm: true` call decrypts the key, and only for the duration of that one call.
- **ETX is not a separate code path**: sending to an address in a different zone uses the exact same `send_transaction` call as a same-zone send — Quai's network handles the cross-zone routing (as an external transaction) transparently once the signed transaction reaches the sender's zone. The tool just detects and reports the zones involved so the caller knows what to expect.
- **Qi wallets are stateless between calls, on purpose**: `create_qi_wallet`/`import_qi_wallet` only ever encrypt a mnemonic. `get_qi_balance` and `convert_qi_to_quai` reconstruct the `QiHDWallet` from scratch each call and re-derive its address (`src/qiWallet.ts`) rather than reading any cached address/UTXO state -- there isn't any to read. This traded a little performance (every Qi operation re-derives and re-queries rather than hitting a cache) for a simpler, harder-to-get-wrong security story: the only thing ever at rest is the one secret that matters.
TDQS
Scored across 17 tools
Each tool targets a distinct resource (blocks, transactions, balances, wallets, conversions), and the QUAI vs Qi split is clearly delineated. The only minor overlap is send_transaction also covering QUAI→Qi conversion, which send_qi and convert_qi_to_quai handle separately, but descriptions clarify the paths.
All tools use a predictable verb_first snake_case pattern (get_, create_, import_, send_, resolve_, call_, search_, list_, convert_). Ledger-specific variants are consistently prefixed with qi rather than mixed into inconsistent conventions.
Seventeen tools is slightly beyond the 3–15 sweet spot, but the domain is genuinely large: two ledgers, sharded chains, wallet lifecycle, and conversion flows. Each tool addresses a real operation, so the count feels dense rather than bloated.
The surface covers querying, wallet lifecycle (create/import/list), QUAI and Qi transfers, and the native conversion both ways, which is solid for the domain. Minor gaps remain: no wallet deletion, no Qi transaction/UTXO detail lookup, and no contract state-changing sends (though send_transaction exists despite call_contract's stale description).