Skip to main content
Glama

Hedera MCP Server

A Model Context Protocol (MCP) server that lets AI agents work with the Hedera network. It speaks MCP over Streamable HTTP and exposes the Hedera Agent Kit toolset, so an LLM client (Claude, Cursor, VS Code, Codex, …) can query accounts, tokens and topics, and prepare transactions on testnet or mainnet.

The server is non-custodial: it never holds a Hedera private key. It runs in one of two modes:

  • Dev mode (default, no setup). Write tools return unsigned transaction bytes for the caller to sign and submit. The account is taken from an x-hedera-account-id request header. No database, no OAuth.

  • Wallet signing (optional). The server becomes an OAuth 2.1 authorization server whose login is the user's wallet. Each write tool sends the transaction to that wallet over WalletConnect (HIP-820) for the user to approve; the wallet signs and broadcasts. Needs Postgres and a WalletConnect project ID. See Wallet Signing.

TIP

Just want a plain local Hedera MCP server, without wallet sign-in? The Hedera Agent Kit ships a runnable example with the same toolset: examples/modelcontextprotocol. Its HTTP server needs no keys; its stdio server takes your account's private key (HEDERA_OPERATOR_KEY) and signs transactions itself, so use a testnet account. To build your own server, use the library @hashgraph/hedera-agent-kit-mcp.

What it does

Out of the box the server registers 44 Hedera Agent Kit tools:

Area

Examples

Accounts & HBAR

create_account_tool, update_account_tool, transfer_hbar_tool, HBAR allowances

Hedera Token Service

create / mint / airdrop / transfer fungible and non-fungible tokens, associate / dissociate, token and NFT allowances

Consensus Service

create_topic_tool, submit_topic_message_tool, update_topic_tool, delete_topic_tool

EVM

create_erc20_tool, transfer_erc20_tool, create_erc721_tool, mint_erc721_tool, transfer_erc721_tool

Scheduled transactions

sign_schedule_transaction_tool, schedule_delete_tool

Queries (mirror node)

HBAR and token balances, account, token, topic and contract info, topic messages, pending airdrops, transaction records, exchange rate

Use --tools to expose a subset. In wallet mode, three tools are added (wallet_status, check_signing_status, sign_with_wallet).

Related MCP server: BananaCrystal MCP Server

Run it locally

Prerequisites

  • Node.js 24.22.1 or later 24.x (see .nvmrc; earlier 24.x builds are vulnerable to CVE-2026-111497)

  • A Hedera account ID to build transactions for. A free testnet account is available from the Hedera Portal.

Quick start (dev mode)

git clone https://github.com/blockydevs/hedera-mcp-server.git
cd hedera-mcp-server
nvm use            # optional, picks up .nvmrc
npm install
npm run build
npm start          # = node dist/main.js --agent-mode=returnBytes (testnet)

The server listens on http://localhost:3001/mcp. Check it with curl http://localhost:3001/healthz → {"status":"ok"}.

Connect an MCP client

Point any MCP client that supports Streamable HTTP at the server and pass your account ID in the x-hedera-account-id header. For example, with Claude Code:

claude mcp add --transport http hedera http://localhost:3001/mcp \
  --header "x-hedera-account-id: 0.0.YOUR_ACCOUNT_ID"

or in a client's JSON config:

{
  "mcpServers": {
    "hedera": {
      "url": "http://localhost:3001/mcp",
      "headers": {
        "x-hedera-account-id": "0.0.YOUR_ACCOUNT_ID"
      }
    }
  }
}

To poke at the tools without an LLM, run the MCP Inspector (npx @modelcontextprotocol/inspector), choose Streamable HTTP, enter the URL above and add the header.

Run with Docker

docker build -t hedera-mcp .
docker run --rm -p 3001:3001 \
  -e HEDERA_HTTP_HOST=0.0.0.0 -e ALLOW_INSECURE=true \
  hedera-mcp                      # add --ledger-id=mainnet to override the testnet default

Inside a container the server must bind 0.0.0.0 to be reachable through the published port, and an unauthenticated non-loopback bind is refused unless ALLOW_INSECURE=true. That is fine on your own machine; never deploy it that way (see Security notes).

The image's entrypoint is node dist/main.js --agent-mode=returnBytes with a default CMD ["--ledger-id=testnet"].

Quick start — wallet signing (non-custodial)

The full path to a running wallet-signing server, where every transaction is approved in the user's own wallet. See Wallet Signing below for how it works.

  1. Configure. Copy the sample env and fill it in (dist/main.js auto-loads .env):

    cp .env.example .env

    Set at least these in .env:

    WALLETCONNECT_PROJECT_ID="<your id from https://cloud.reown.com>"
    PUBLIC_URL="https://mcp.example.com"          # public HTTPS origin of THIS server
    WC_ENCRYPTION_KEY="<output of: openssl rand -base64 32>"  # encrypts WC session keys at rest
    DATABASE_URL="postgresql://hedera_mcp:hedera_mcp@localhost:5433/hedera_mcp"  # matches docker compose

    .env values are not shell-expanded: run openssl rand -base64 32 and paste its output. Setting exactly one of WALLETCONNECT_PROJECT_ID / PUBLIC_URL fails at startup — set both (wallet mode) or neither (dev mode). WC_ENCRYPTION_KEY is required in wallet mode (min 32 chars); without it the server refuses to start, unless WC_ALLOW_PLAINTEXT=true explicitly accepts unencrypted session-key storage (non-production only).

  2. Start Postgres (local dev):

    docker compose up -d
  3. Install, build, and run migrations:

    npm install
    npm run build
    npm run migration:run
  4. Run the server:

    node dist/main.js --agent-mode=returnBytes --ledger-id=testnet

    To expose it externally, set HEDERA_HTTP_HOST=0.0.0.0 and put it behind a TLS-terminating reverse proxy — a non-loopback bind without authentication is refused at startup.

  5. Connect a client. Point your MCP client at PUBLIC_URL + /mcp. On the first call the server returns 401, and the client runs the OAuth 2.1 + wallet sign-in flow automatically (browser-extension button or WalletConnect QR). No x-hedera-account-id header is used — the account comes from the wallet-verified token.

Health checks

Two unauthenticated probes for orchestrators (mounted ahead of rate-limiting and auth, so they are never gated):

  • GET /healthz — liveness; returns 200 {"status":"ok"} whenever the process is up.

  • GET /readyz — readiness; round-trips the database (200 {"status":"ready"}, or 503 {"status":"unavailable"} if it can't). In dev mode (no database) it collapses to liveness.

Configuration

The server can be configured using environment variables:

  • HEDERA_HTTP_HOST (default: localhost)

  • HEDERA_HTTP_PORT (default: 3001)

  • HEDERA_HTTP_ALLOWED_HOSTS (optional, comma-separated allowlist for Host header)

  • ORIGIN_AUTH_VERIFICATION_ENABLED (default: false, enables HMAC origin verification)

  • ORIGIN_AUTH_SECRET_FILE_PATH (path to a file containing the shared HMAC secret)

  • LOG_LEVEL (default: info; silent disables logging) and LOG_PRETTY (default: false, human-readable instead of one-JSON-per-line)

  • DAPP_NAME, DAPP_DESCRIPTION, DAPP_ICON_URL - WalletConnect dApp metadata shown to the wallet

  • TTL tuning knobs (ms): SIGN_REQUEST_TTL_MS (~180s), SIGNIN_NONCE_TTL_MS, CEREMONY_TTL_MS, ACCESS_TOKEN_TTL_MS, REFRESH_TOKEN_TTL_MS

See .env.example for the complete, annotated list of environment variables (wallet signing, encryption at rest, resource governance, database).

Optional Origin Verification (HMAC)

When ORIGIN_AUTH_VERIFICATION_ENABLED=true, the server verifies that each request was signed by a trusted edge (e.g. Cloudflare Worker) using HMAC-SHA256. Required headers:

  • X-Origin-Timestamp — millisecond timestamp (must be within 30 seconds of server time)

  • X-Origin-Nonce — unique value per request (replay protection)

  • X-Origin-Signature — hex-encoded HMAC-SHA256 of the canonical string:

{method}\n{path}\n{timestamp}\n{nonce}\n{sha256Hex(body)}

The shared secret is loaded from the file at ORIGIN_AUTH_SECRET_FILE_PATH.

  • Missing or invalid signature returns 401 Unauthorized

  • When ORIGIN_AUTH_VERIFICATION_ENABLED=false, requests are allowed without this check

The gate covers the whole request surface: /mcp, the OAuth endpoints (/authorize, /token, /register, /.well-known/*), the /connect sign-in page, and any unmatched path. The only exemptions are /healthz and /readyz, which orchestrators call against the container directly rather than through the edge, so they cannot carry a signature.

In production, the edge proxy generates these headers. For local testing you must compute the HMAC yourself.

Example setup:

echo "my-super-secret" > /tmp/origin-auth-secret.txt
export ORIGIN_AUTH_VERIFICATION_ENABLED=true
export ORIGIN_AUTH_SECRET_FILE_PATH=/tmp/origin-auth-secret.txt

You can also override the port and ledger via CLI:

node dist/main.js --agent-mode=returnBytes --port=8080 --ledger-id=mainnet

Supported Arguments

  • --agent-mode: returnBytes only, and the default when the flag is omitted. This server is non-custodial and holds no operator key, so executing modes (the agent kit's autonomous, and its customExecuteTx) are refused at startup rather than accepted and failing later.

  • --ledger-id: testnet (default) or mainnet.

  • --host: Interface to bind (default: localhost; also settable via HEDERA_HTTP_HOST). A non-loopback host is refused at startup unless the server is authenticated or ALLOW_INSECURE=true.

  • --port: Port to listen on (default: 3001).

  • --tools: Comma-separated list of tools to enable (default: all).

  • --account-id, --public-key: dev-mode account binding (ignored in wallet mode, where the account comes from the bearer token).

Agent Mode: RETURN_BYTES

In this mode, the MCP server returns the unsigned transaction bytes instead of executing them. This ensures the server remains stateless and does not require operator credentials.

IMPORTANT

Requesters must provide the account context by passing thex-hedera-account-id header in the HTTP request. This identifies which account the transactions are being built for.

NOTE

Whenwallet signing is enabled, the account comes from the verified access token instead and the x-hedera-account-id header is ignored — a client must not be able to name an account it has not proven it controls.

Wallet Signing (non-custodial)

An optional mode in which the user approves and signs every transaction in their own wallet over WalletConnect v2 / HIP-820. The server holds no Hedera keys of any kind, never submits a transaction, and never sees a user signature — the wallet signs and broadcasts.

It is off unless both WALLETCONNECT_PROJECT_ID and PUBLIC_URL are set. Without them the server behaves exactly as documented above.

In wallet mode, each write tool auto-delivers. The tool definitions are untouched, but a wrapper runs the underlying Hedera tool to build the transaction and then delivers it to the wallet for approval, returning a requestId — one call, no bytes handled by the model. (In dev mode, with no wallet bound, the tools return unsigned bytes exactly as before.)

How it works

1. transfer_hbar_tool({ to, amount })  ->  { requestId, summary, walletVerifiable, ... }
                                            (built AND delivered to the wallet in one call;
                                            also walletKind, approveUrl?, hint)
2. check_signing_status({ requestId }) ->  awaiting_approval | confirming | executed
                                            | rejected | failed | expired | needs_reauth

The write tool never blocks: it returns a requestId at once and the wallet prompt resolves in the background (MCP clients cap tool calls near 60s, well under human approval time). After the wallet broadcasts, the server confirms the transaction on the mirror node before reporting executed — so a stale-window approval that never lands is reported honestly, not as success.

Added tools

Tool

Args

Returns

wallet_status

none

accountId, network, sessionExpiry (the WC session expiry), walletKind, walletName?

check_signing_status

requestId, waitMs? (long-poll, default 45s, max 55s; 0 = non-blocking)

status, transactionId?, hashscanUrl?, nextStep

sign_with_wallet

bytes

requestId, … — raw-bytes escape hatch; normally unnecessary since write tools auto-deliver

Status values: awaiting_approval → confirming (mirror-node check) → executed, or rejected / failed (reached the network but failed on-chain) / expired / needs_reauth.

Sign-in

The server is an OAuth 2.1 authorization server whose login form is a wallet. MCP clients start this flow on their own when /mcp returns 401:

  1. Client runs OAuth 2.1 authorization-code + PKCE and opens /authorize.

  2. That redirects to /connect, which offers a browser-extension button (for detected HIP-820 wallet extensions) and a WalletConnect QR code for mobile wallets.

  3. The user pairs their wallet, then approves a sign-in message — a single-use nonce bound to this server's domain and an issued-at time. Signing it moves no funds.

  4. The signature is verified against the account's key from the mirror node. Only then is an authorization code issued.

  5. The token's subject is that proven Hedera account.

There are no pairing tools: an authenticated request always has a wallet behind it. If the wallet session ends, tools report needs_reauth and the client re-runs the flow.

Security notes

  • The payer is never a parameter. It comes from the token, and sign_with_wallet re-checks the decoded transaction's payer against it before anything reaches a wallet. Those bytes travel through the model's context, so they are not trusted.

  • walletVerifiable: false marks transaction types a wallet cannot fully decode (contract calls, file operations). The agent must relay that warning: for those types the wallet's display is not an independent check.

  • Access tokens never outlive their wallet session, and a wallet-side disconnect purges the pairing and its grants immediately.

  • Run behind a TLS-terminating reverse proxy. The server speaks plaintext HTTP; bearer tokens and the wc: pairing URI must never traverse an untrusted network in the clear. Binding a non-loopback host with no authentication is refused at startup (ALLOW_INSECURE=true overrides — never in production). Set TRUST_PROXY_HOPS to your proxy depth.

  • Resource governance. MAX_SESSIONS, RATE_LIMIT_PER_MINUTE, and MAX_CONCURRENT_APPROVALS bound session growth, request rate, and per-account wallet prompts.

  • The Postgres store is secret-adjacent. Grants and auth codes are stored hashed. The wc_kv table holds WalletConnect session symmetric keys, envelope-encrypted at rest (AES-256-GCM) with WC_ENCRYPTION_KEY — required in wallet mode; the server refuses to start without it unless WC_ALLOW_PLAINTEXT=true opts into cleartext storage (non-production only). Even in the clear, a leaked database allows prompt initiation, never signing. Restrict database access; sourcing the key from a KMS is a planned follow-up.

  • Single instance only. The database layer requires exactly one server process (see docs/DATABASE.md); a second one against the same Postgres refuses to boot via an advisory lock. Do not autoscale horizontally, deploy stop-then-start (K8s strategy: Recreate), and point DATABASE_URL at a direct connection rather than a transaction-pooling proxy.

Not implemented yet

WARNING

This mode has been exercised end-to-end ontestnet (sign-in, delivery, mirror-confirm). WalletConnect sessions persist across a server restart (Postgres-backed store), and the concurrent-approval cap and at-rest encryption are in place. Not built: spend policies, an append-only audit log, and HA failover (the server runs as a single instance). See docs/SPEC.md §11 for the known limitations.

Status of the hardening items:

Area

Status

Prompt-spam quotas

Implemented — concurrent-approval cap (MAX_CONCURRENT_APPROVALS) + request rate limit (RATE_LIMIT_PER_MINUTE); per-account cooldown not yet

Spend policies (per-tx / per-day caps, recipient allowlists)

Not implemented

Append-only audit log

Not implemented

HA / active-standby failover

Not implemented — single stateful instance

Server-side re-issue on the ~180s validity window

Not implemented — an expired request reports expired and the agent re-runs the write tool

Threshold / key-list accounts at sign-in

Not supported — single-key accounts only

Mainnet

Untested — testnet only

At-rest encryption of session keys

Implemented — AES-256-GCM envelope via WC_ENCRYPTION_KEY; sourcing the key from a KMS is the remaining follow-up

Database migrations

The OAuth/session schema is owned by TypeORM migrations, not auto-sync (DB_SYNCHRONIZE defaults off — a forgotten flag in production can't silently drop columns).

docker compose up -d                       # local Postgres
npm run migration:run                       # apply pending migrations (builds first)
npm run migration:generate --name=AddThing  # after an entity change, generate the diff
npm run migration:revert                    # roll back the last migration

Deploy: run npm run migration:run as a discrete step (a k8s Job / initContainer), or set DB_MIGRATIONS_RUN=true to apply pending migrations at startup on a single instance. Set DB_SYNCHRONIZE=true only for throwaway local dev. Tests use in-memory sqlite and don't need migrations.

Development

npm test            # vitest (unit + integration; in-memory sqlite, no Postgres needed)
npm run typecheck   # tsc over src and test
npm run lint        # eslint

The tests cover the logic that does not need a live wallet: nonce lifecycle, payer verification, the signing status machine, the session store, and the HTTP surface. The WalletConnect pairing handshake and the hedera_signAndExecuteTransaction round trip need a real wallet and are tested manually on testnet.

Further reading:

  • docs/SPEC.md — architecture, trust model, tool surface and known limitations

  • docs/DATABASE.md — the Postgres schema and the single-instance requirement

  • SECURITY.md — reporting vulnerabilities and deployment requirements

Contributing

Issues and pull requests are welcome. Before opening a PR, please make sure npm test, npm run typecheck and npm run lint pass, and keep changes focused. Report security issues privately as described in SECURITY.md, not in public issues.

License

Copyright 2026 Hedera Hashgraph, LLC. Licensed under the Apache License 2.0; see NOTICE for attributions.

The sign-in page bundles the Montserrat typeface under the SIL Open Font License 1.1. The Hedera name and logo shown on the sign-in page are trademarks of Hedera Hashgraph, LLC; they are not covered by the Apache license.

Installing this server also installs WalletConnect and Reown packages, which are source-available rather than open source and are not covered by the Apache license. They are licensed under the WalletConnect Community License and the Reown Community License, and those terms apply to anyone who installs or runs the server. Portions © 2025 Reown, Inc. All Rights Reserved.

If you run it as a service, read those licences first. Among other things they require a commercial licence from Reown above their usage thresholds (§3), require connecting through the Reown relay network (§2b), and require the Reown copyright notice above in the service's notices or the about section of its website (§2a).

Related MCP Connectors

Related MCP Servers

  • A
    license
    A
    quality
    B
    maintenance
    Production MCP server giving AI agents metered access to live Hedera blockchain data. Query token prices, screen identities, monitor governance, write tamper-evident HCS compliance records, and analyze smart contracts — all paid in HBAR micropayments per call.
    20
    47 npm
    Academic Free v1.1
  • A
    license
    B
    quality
    D
    maintenance
    Comprehensive MCP server for Hedera (Hashgraph) covering 73 tools across all core services, building unsigned transactions for AI agents to propose. Never holds keys; reads via public Mirror Node.
    73
    69 npm
    MIT