Production-ready Model Context Protocol (MCP) server for the Kaia blockchain. Exposes 25 partner-safe tools (26 if KAIA_ALLOW_UNSAFE_WALLET=1), 5 resources, and 6 prompts for balance, transactions, blocks, tokens, NFTs, contracts, network info, and calldata encoding.
HTTP is a partner-style connector: OAuth 2.1 with PKCE (browser) or device flow (CLI), fail-closed bearer auth, and token scopes mapped to an allowed-tool registry. An in-process demo IdP runs with the HTTP server so tests and CI need no real credentials. See docs/AUTH.md.
npm install -g kaia-mcp
# or
npx kaia-mcpstdio (default) — run and connect via stdin/stdout (e.g. Claude Desktop, Cursor):
npx kaia-mcp
# or
npx kaia-mcp --transport stdioHTTP — Streamable HTTP on a port. Partner default (KAIA_AUTH_MODE=required) requires Authorization: Bearer on every MCP request. The same process serves the demo OAuth/OIDC endpoints.
npx kaia-mcp --transport http --port 3100
# GET http://127.0.0.1:3100/health
# GET http://127.0.0.1:3100/.well-known/openid-configuration| Tool | Description |
|---|---|
get_kaia_balance |
Get KAIA balance for an address |
get_account_info |
Get account summary: balance, tx count, nonce |
get_account_tokens |
List ERC-20 token holdings for an address |
get_account_nfts |
List NFT holdings for an address |
get_transaction |
Get transaction by hash |
get_transaction_receipt |
Get transaction receipt and status |
get_account_transactions |
List recent transactions for an address |
estimate_gas |
Estimate gas for a transaction |
get_block_number |
Current block number |
get_block |
Get block by number or tag |
get_block_rewards |
Block rewards for a block |
get_token_info |
Token metadata (name, symbol, supply) |
get_token_holders |
Top token holders |
get_token_transfers |
Token transfer history |
get_token_allowance |
ERC-20/KIP-7 allowance (owner/spender) for DeFi |
get_nft_info |
NFT collection info |
get_nft_item |
Single NFT item (owner, metadata) |
get_nft_transfers |
NFT transfer history |
read_contract |
Read contract view/pure function |
get_contract_abi |
Verified contract ABI from KaiaScan |
get_contract_source |
Verified contract source from KaiaScan |
get_gas_price |
Current gas price |
get_kaia_price |
KAIA price (USD, BTC, stats) from KaiaScan |
get_chain_info |
Chain id and name (mainnet/kairos) |
encode_function_data |
Encode contract call data from ABI and args (kaia:encode) |
generate_wallet |
Not in the default list. Unsafe local-dev only (KAIA_ALLOW_UNSAFE_WALLET=1 + kaia:wallet). Never returns private keys in partner mode. |
| URI | Description |
|---|---|
kaia://mainnet/status |
Mainnet status: block height, gas price, KAIA price |
kaia://kairos/status |
Kairos testnet status |
kaia://mainnet/tokens/popular |
Popular mainnet token addresses with name/symbol |
kaia://mainnet/top-accounts |
Top 100 KAIA holders |
kaia://docs/rpc-methods |
Static reference of Kaia RPC methods |
| Name | Args | Description |
|---|---|---|
analyze-wallet |
address, network? | Analyze wallet: balance, txs, tokens |
investigate-transaction |
txHash | Investigate tx: status, gas, transfers |
token-research |
contractAddress, network? | Research token: supply, holders, activity |
nft-lookup |
contractAddress, tokenId, network? | Look up NFT: owner, metadata, history |
gas-report |
network? | Gas report: price, history, recommendations |
smart-contract-audit |
contractAddress, network? | Review contract: ABI, source, type |
Environment variables (see .env.example):
| Variable | Description | Default |
|---|---|---|
KAIA_RPC_URL |
Kaia mainnet RPC endpoint | https://public-en.node.kaia.io |
KAIA_KAIROS_RPC_URL |
Kairos testnet RPC | https://public-en-kairos.node.kaia.io |
KAIASCAN_API_KEY |
KaiaScan API key (optional) | — |
KAIA_DEFAULT_NETWORK |
mainnet or kairos | mainnet |
LOG_LEVEL |
debug, info, warn, error | info |
RATE_LIMIT_RPC |
Max RPC requests per second | 10 |
RATE_LIMIT_KAIASCAN |
Max KaiaScan requests per second | 5 |
RPC_TIMEOUT_MS |
RPC request timeout (ms) | 30000 |
KAIASCAN_TIMEOUT_MS |
KaiaScan request timeout (ms) | 15000 |
KAIA_AUTH_MODE |
HTTP auth: required or off |
required |
KAIA_OAUTH_CLIENT_ID |
Demo public client id | kaia-mcp-demo |
KAIA_ACCESS_TOKEN_TTL_SECONDS |
Demo access-token TTL | 900 |
KAIA_PUBLIC_URL |
Public origin (https://host[:port], no path): OAuth issuer, canonical resource URI and token aud; also the default allowed browser Origin |
http://127.0.0.1:<port> |
KAIA_OAUTH_LEGACY_AUDIENCE |
Extra aud minted next to the canonical URI for gateways that pin a non-URI audience (never accepted alone). KAIA_OAUTH_AUDIENCE is a deprecated alias |
unset |
KAIA_OAUTH_REQUIRE_RESOURCE |
Reject authorize/device/token requests without an RFC 8707 resource |
off (missing = this server) |
KAIA_ALLOWED_ORIGINS |
Extra browser origins allowed on the MCP/OAuth endpoints (comma-separated); a foreign Origin gets 403, no Origin passes |
only the public origin |
KAIA_OAUTH_PREVIOUS_SIGNING_KEY_FILES |
Retired signing keys (comma-separated PEMs) still published in the JWKS and accepted for verification | unset |
KAIA_OAUTH_SIGNING_KEY_FILE |
Dev RS256 key path, created 0600 if missing; keep it gitignored (.kaia-dev/) |
unset (in-memory key per process) |
KAIA_OAUTH_REVOCATION_FILE |
Where revoked access-token jtis are persisted (atomic, durable writes, 0600). A corrupt, unreadable or group/world-writable file refuses startup. One file per server process: a file another running instance holds is refused at startup, so instances sharing a signing key must each set their own |
revoked-jti.json next to the signing key file; in memory when no key file is set |
KAIA_INTROSPECTION_CLIENT_ID |
Gateway client id for /oauth/introspect |
kaia-mcp-gateway |
KAIA_INTROSPECTION_CLIENT_SECRET |
Gateway secret (HTTP Basic). Unset: introspection is not offered | unset |
KAIA_ALLOW_UNSAFE_WALLET |
Enable generate_wallet private keys (local only) |
off |
Cursor — add to Cursor MCP settings (e.g. ~/.cursor/mcp.json or project MCP config):
{
"mcpServers": {
"kaia": {
"command": "npx",
"args": ["kaia-mcp"],
"env": {}
}
}
}Claude Desktop — in Claude Desktop config (e.g. ~/Library/Application Support/Claude/claude_desktop_config.json on macOS):
{
"mcpServers": {
"kaia": {
"command": "npx",
"args": ["kaia-mcp"],
"env": {}
}
}
}Override env as needed, e.g. "env": { "KAIA_RPC_URL": "https://your-rpc.io", "KAIASCAN_API_KEY": "your-key" }.
import { createKaiaMcpServer, runKaiaMcpServer } from "kaia-mcp";
// Get the MCP Server instance (e.g. for custom transport)
const server = createKaiaMcpServer();
// Run over stdio (CLI default)
await runKaiaMcpServer();For HTTP:
import { runKaiaMcpServerHttp } from "kaia-mcp";
await runKaiaMcpServerHttp(3100);docker build -t kaia-mcp .
docker run -p 3100:3100 -e KAIA_RPC_URL=https://public-en.node.kaia.io kaia-mcpServer listens on port 3100 (Streamable HTTP transport). Override env as needed.
The container runs the HTTP transport (not stdio), so clients must connect by URL.
Cursor — in MCP settings (e.g. ~/.cursor/mcp.json or project MCP config), add a server entry with url:
{
"mcpServers": {
"kaia": {
"url": "http://localhost:3100"
}
}
}If your client expects a path, use http://localhost:3100/mcp. Restart Cursor after changing MCP config.
Claude Desktop — if your Claude Desktop build supports remote MCP URLs, use the same url in its MCP config.
Test from the host (with the container running). Health is unauthenticated; MCP is not:
curl -s http://localhost:3100/health
# Obtain a demo token (see docs/AUTH.md), then:
curl -s -X POST http://localhost:3100 \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}' | jqYou should see a list of tools filtered by the token’s scopes. Replace localhost with your machine’s IP or hostname when connecting from another device.
This server is an MCP connector, not a hosted product partnership. Integrate over HTTP with OAuth; do not treat stdio as a partner path.
- Discover
/.well-known/oauth-protected-resourceand/.well-known/openid-configuration(demo) or your authorization server’s metadata. Access tokens are RS256 JWTs; fetch keys from the discoveryjwks_uriand pinissandaud(both the server's canonical URI,KAIA_PUBLIC_URL) instead of trusting whatever the token says. Sendresource=<canonical URI>on authorize and token requests (RFC 8707) and check theisson the authorization response (RFC 9207). - Register a public client. Require PKCE
S256for browser agents; use device flow for CLIs that cannot host a redirect. - Allowlist redirect URIs. The demo IdP accepts only
http://127.0.0.1/callback,http://localhost/callback, and/cbvariants. - Request least privilege:
kaia:readfor chain reads, addkaia:encodeonly if the agent must build calldata. Never requestkaia:walletin production. - Send
Authorization: Bearer <access_token>on every MCP HTTP request. The transport is stateless (MCP 2026-07-28 via SDK v2createMcpHandler): noMcp-Session-Id, modern clients may useserver/discoverinstead ofinitialize,GET/DELETEanswer 405. MatchMcp-Method/Mcp-Name/MCP-Protocol-Versionto the body on modern requests (-32020on mismatch).tools/listiscacheScope: private. - Handle fail-closed errors literally:
-32040unauthorized,-32041token_expired,-32043invalid_token (HTTP 401 with aWWW-Authenticatechallenge namingresource_metadata), and-32042insufficient_scope (HTTP 403,error="insufficient_scope"with the requiredscope). Retry only after a new token; do not retry a denied tool. - On disconnect or user logout,
POST /oauth/revokewith the access or refresh token. Revocation is byjtiand only the issuer knows about it: a gateway that verifies JWTs offline must also call/oauth/introspect(client-authenticated) if it needs to see revocations beforeexp. - Keep tokens out of logs, crash dumps, eval fixtures, and git. Prefer a token fingerprint if you must correlate requests.
- Leave
KAIA_ALLOW_UNSAFE_WALLETunset.generate_walletmust not appear in partner tool lists and must not return private keys. - Point production at your own OIDC issuer; the in-process demo IdP is for tests/CI/local bring-up only. No real user secrets or signing keys belong in this repo.
- If you keep your own copy of the tool → scope map (e.g. in a policy gateway), compare it against
GET /.well-known/kaia-mcp/tool-scopesat startup and refuse to run on drift.
- Scopes. A
kaia:readtoken must not callencode_function_data. Scope checks run before handlers; a deny has no chain or wallet side effect. - Secret handling. Access tokens are signed JWTs and are not stored; refresh tokens are hashed at rest. The logger redacts bearer values and bare JWTs. The signing key is generated at startup (or read from a gitignored dev path) and only the public JWK is published. Wallet private keys are not emitted unless the explicit unsafe dev flag is on.
- Offline verification vs revocation. A JWT stays cryptographically valid until
expeven after/oauth/revoke. kaia-mcp itself rejects revokedjtis; external verifiers need introspection or a short TTL. - Algorithm confusion. Only
RS256with the publishedkidis accepted;alg=none,HS*, and foreign keys fail asinvalid_token. - Sessions and DNS rebinding. There are no MCP sessions to steal; the bearer token is checked on every request. A browser page on a foreign origin is refused with 403 before auth (allow-list with
KAIA_ALLOWED_ORIGINS). - PKCE downgrade.
code_challenge_method=plainis rejected. - Open redirect. Unregistered
redirect_urivalues are rejected. - Fallbacks.
KAIA_AUTH_MODE=offand stdio skip OAuth. Those paths are local-only. Partners must not ship them as a “fallback” for failed authentication; failed auth is a deny. - Unsafe wallet flag. If an operator enables
KAIA_ALLOW_UNSAFE_WALLET=1, private keys can leave the process. Treat that as a break-glass local tool, not a partner feature.
Full protocol detail: docs/AUTH.md.
MIT