Kaia MCP Server
by naveed949
README.md
# Kaia MCP Server
Production-ready [Model Context Protocol](https://modelcontextprotocol.io) (MCP) server for the [Kaia](https://kaia.io) 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](docs/AUTH.md).
## Installation
```bash
npm install -g kaia-mcp
# or
npx kaia-mcp
```
## Quick start
**stdio (default)** — run and connect via stdin/stdout (e.g. Claude Desktop, Cursor):
```bash
npx kaia-mcp
# or
npx kaia-mcp --transport stdio
```
**HTTP** — 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.
```bash
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
```
## Tools (25 partner-safe)
| 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. |
## Resources (5)
| 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 |
## Prompts (6)
| 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 |
## Configuration
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 `jti`s 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 |
## MCP client setup
**Cursor** — add to Cursor MCP settings (e.g. `~/.cursor/mcp.json` or project MCP config):
```json
{
"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):
```json
{
"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" }`.
## Links
- [Kaia docs](https://docs.kaia.io)
- [KaiaScan API](https://docs.kaiascan.io)
## Programmatic use
```ts
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:
```ts
import { runKaiaMcpServerHttp } from "kaia-mcp";
await runKaiaMcpServerHttp(3100);
```
## Docker
```bash
docker build -t kaia-mcp .
docker run -p 3100:3100 -e KAIA_RPC_URL=https://public-en.node.kaia.io kaia-mcp
```
Server listens on port 3100 (Streamable HTTP transport). Override env as needed.
### Connecting to the Docker server
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`:
```json
{
"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:
```bash
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":{}}' | jq
```
You 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.
## Partner integration
This server is an MCP connector, not a hosted product partnership. Integrate over HTTP with OAuth; do not treat stdio as a partner path.
### Integration checklist
1. Discover `/.well-known/oauth-protected-resource` and `/.well-known/openid-configuration` (demo) or your authorization server’s metadata. Access tokens are RS256 JWTs; fetch keys from the discovery `jwks_uri` and pin `iss` and `aud` (both the server's canonical URI, `KAIA_PUBLIC_URL`) instead of trusting whatever the token says. Send `resource=<canonical URI>` on authorize and token requests (RFC 8707) and check the `iss` on the authorization response (RFC 9207).
2. Register a **public** client. Require PKCE `S256` for browser agents; use device flow for CLIs that cannot host a redirect.
3. Allowlist redirect URIs. The demo IdP accepts only `http://127.0.0.1/callback`, `http://localhost/callback`, and `/cb` variants.
4. Request least privilege: `kaia:read` for chain reads, add `kaia:encode` only if the agent must build calldata. Never request `kaia:wallet` in production.
5. Send `Authorization: Bearer <access_token>` on **every** MCP HTTP request. The transport is stateless (MCP 2026-07-28 via SDK v2 `createMcpHandler`): no `Mcp-Session-Id`, modern clients may use `server/discover` instead of `initialize`, `GET`/`DELETE` answer 405. Match `Mcp-Method`/`Mcp-Name`/`MCP-Protocol-Version` to the body on modern requests (`-32020` on mismatch). `tools/list` is `cacheScope: private`.
6. Handle fail-closed errors literally: `-32040` unauthorized, `-32041` token_expired, `-32043` invalid_token (HTTP 401 with a `WWW-Authenticate` challenge naming `resource_metadata`), and `-32042` insufficient_scope (HTTP 403, `error="insufficient_scope"` with the required `scope`). Retry only after a new token; do not retry a denied tool.
7. On disconnect or user logout, `POST /oauth/revoke` with the access or refresh token. Revocation is by `jti` and 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 before `exp`.
8. Keep tokens out of logs, crash dumps, eval fixtures, and git. Prefer a token fingerprint if you must correlate requests.
9. Leave `KAIA_ALLOW_UNSAFE_WALLET` unset. `generate_wallet` must not appear in partner tool lists and must not return private keys.
10. 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.
11. 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-scopes` at startup and refuse to run on drift.
### Threat notes
- **Scopes.** A `kaia:read` token must not call `encode_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 `exp` even after `/oauth/revoke`. kaia-mcp itself rejects revoked `jti`s; external verifiers need introspection or a short TTL.
- **Algorithm confusion.** Only `RS256` with the published `kid` is accepted; `alg=none`, `HS*`, and foreign keys fail as `invalid_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=plain` is rejected.
- **Open redirect.** Unregistered `redirect_uri` values are rejected.
- **Fallbacks.** `KAIA_AUTH_MODE=off` and 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](docs/AUTH.md).
## License
MIT
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessResponsive