symbol-mcp-server
by inotakeh
README.md
# symbol-mcp-server
[](https://www.npmjs.com/package/symbol-mcp-server)
> **Symbol only.** This server talks to [Symbol](https://docs.symbol.dev/) (catapult) nodes. It does not
> support NEM NIS1 (XEM), which is a separate chain with a different API.
> **Unofficial.** This is an independent project with no affiliation to the NEM or Symbol core teams.
[日本語版 README](README.ja.md)
Read-only [MCP](https://modelcontextprotocol.io/) server that turns the Symbol REST API into 20
task-level tools. Instead of mirroring REST endpoints one-to-one, each tool answers a question a
person actually asks:
- **Account holders:** balances with alias names and decimals applied, transaction history and
details with decoded messages, mosaic and namespace lookups, fee estimates, address validation,
height/epoch/time conversion.
- **Node operators:** node health and sync state, delegated-harvesting status, comparison against
reference nodes, and above all **voting-key expiry**: remaining epochs, blocks and days, the
estimated expiry date and a recommended renewal window.
Every tool returns `structuredContent` (validated against a published `outputSchema`) plus the same
JSON as text, with a one-to-three-line `summary` first. Amounts are returned both with divisibility
applied and as the raw integer; timestamps are ISO 8601 UTC, with a local time added when
`SYMBOL_TIMEZONE` is set.
## Requirements
- Node.js 20 or newer.
- A Symbol REST node reachable over `https://` (port 3001 on most public nodes). Public nodes are
listed at https://nodewatch.symbol.tools/.
## Install
**From npm** (recommended):
```sh
npx -y symbol-mcp-server --help
```
Also listed in the [MCP Registry](https://registry.modelcontextprotocol.io) as `io.github.inotakeh/symbol`.
**From source:**
```sh
git clone https://github.com/inotakeh/symbol-mcp-server.git
cd symbol-mcp-server
npm ci
npm run build
SYMBOL_NODE_URL=https://<node-host>:3001 node dist/index.js
```
`node dist/index.js --help` prints the environment variables to stderr and exits;
`--version` prints the version. The binary takes no other flags: everything is configured through
the environment, so a model can never point it at another host.
## Configure your MCP host
The server speaks MCP over stdio. On start-up it fetches `/node/info`, detects mainnet or testnet
from the generation hash seed, and logs one line to stderr:
```
symbol-mcp-server 0.1.0: mainnet via <node-host>:3001, timezone Asia/Tokyo
```
### Claude Desktop
Add to `claude_desktop_config.json`. With the npm package:
```json
{
"mcpServers": {
"symbol": {
"command": "npx",
"args": ["-y", "symbol-mcp-server"],
"env": {
"SYMBOL_NODE_URL": "https://<node-host>:3001",
"SYMBOL_TIMEZONE": "Asia/Tokyo"
}
}
}
}
```
From a source checkout:
```json
{
"mcpServers": {
"symbol": {
"command": "node",
"args": ["/path/to/symbol-mcp-server/dist/index.js"],
"env": {
"SYMBOL_NODE_URL": "https://<node-host>:3001"
}
}
}
}
```
### Claude Code
```sh
claude mcp add symbol -s user -e SYMBOL_NODE_URL=https://<node-host>:3001 -e SYMBOL_TIMEZONE=Asia/Tokyo -- npx -y symbol-mcp-server
# or, from a source checkout:
claude mcp add symbol -s user -e SYMBOL_NODE_URL=https://<node-host>:3001 -- node /path/to/symbol-mcp-server/dist/index.js
```
Or commit a project-level `.mcp.json`:
```json
{
"mcpServers": {
"symbol": {
"command": "npx",
"args": ["-y", "symbol-mcp-server"],
"env": { "SYMBOL_NODE_URL": "https://<node-host>:3001" }
}
}
}
```
## Environment variables
| Variable | Required | Meaning |
|---|---|---|
| `SYMBOL_NODE_URL` | yes | REST URL of the node to query, e.g. `https://<node-host>:3001`. `https://` is required (`http://` only for `localhost` / `127.0.0.1`). The port is used exactly as given. |
| `SYMBOL_NETWORK` | no | `mainnet` or `testnet`. When set, start-up fails if the node reports a different network. |
| `SYMBOL_TIMEZONE` | no | IANA zone such as `Asia/Tokyo`. Adds a local time next to every UTC timestamp. |
| `SYMBOL_REFERENCE_NODES` | no | Comma-separated `https://` node URLs that `symbol_network_compare` and `symbol_version_drift` check against. No other host is ever contacted. |
| `SYMBOL_REQUEST_TIMEOUT_MS` | no | Per-request timeout, 100 to 600000. Default `10000`. |
| `SYMBOL_STATE_DIR` | no | Absolute directory where `symbol_harvester_watch` keeps one snapshot file per node (unlocked harvester public keys, heights and times; no secrets). Created on first save with mode 0700. Unset: the tool reports the current list without a comparison. |
## Tools
All 20 tools are read-only (`readOnlyHint: true`) and are listed in a fixed order. Arguments are
identifiers only, never URLs. Every `account` argument (and the `address` of
`symbol_transaction_search`) takes a base32 address, a hex public key, or a namespace name such as
`alice` or `alice.pay` that carries an address alias; the resolution is reported in
`accountResolution` and at the start of the summary.
| Tool | Arguments | Answers |
|---|---|---|
| `symbol_network_info` | none | Network name/identifier and generation hash seed, current and finalized height, finalization epoch, block target time, voting set grouping, epoch adjustment, XYM mosaic id/alias/divisibility, current fee multipliers. |
| `symbol_node_status` | none | Friendly name, host, roles (Peer/API/Voting), decoded version, health of API node and database, heights, peer count, and a sync check (latest block older than 5 minutes means `synced: false`). |
| `symbol_account_get` | `account` (address, public key or namespace name), `format` | Address in base32 and hex, public key, every mosaic balance with alias and decimals, importance, linked/VRF/node/voting keys, whether delegated harvesting is set up, multisig settings. |
| `symbol_voting_key_status` | `account` | Every voting key with status (expired/active/future), remaining epochs/blocks/days, estimated expiry date, recommended renewal window (7 to 3 days before), slot usage including expired keys, voter eligibility versus `minVoterBalance`, warnings. |
| `symbol_transaction_get` | `transactionHash` | Looks in confirmed, unconfirmed and partial groups and reports the status; type name, signer and recipient, mosaics with aliases, decoded plain message or "encrypted" marker, fee, height and time, inner transactions of aggregates. |
| `symbol_transaction_search` | `address`, `type`, `pageSize`, `pageNumber`, `order`, `format` | Confirmed transactions involving an account, newest first by default, optional type filter by name (`transfer`) or code (`16724`), 10 to 100 per page. |
| `symbol_mosaic_get` | `mosaic` (hex id or alias such as `symbol.xym`) | Supply, divisibility, flags (supply mutable, transferable, restrictable, revokable), owner, start height, duration and estimated expiry. |
| `symbol_namespace_get` | `namespace` (name or hex id) | Owner, root or sub, level names, alias target (address or mosaic), start and end height, estimated expiry date. |
| `symbol_fee_estimate` | `transactionSizeBytes` (optional) | Slow/average/median/fast fee tiers in XYM computed from the node's current multipliers. Nothing is signed or sent. |
| `symbol_address_parse` | `value` (address, public key or namespace name) | Offline validation: checksum, network byte, base32/hex/dashed forms, and the addresses derived from a public key. A namespace name is resolved through the node to its address alias. |
| `symbol_time_convert` | one of `height`, `epoch`, `timestamp` | Height, finalization epoch, network timestamp and wall-clock time. Exact for the past, estimated (and flagged) for the future. |
| `symbol_harvesting_status` | `account` (optional) | Unlocked delegated harvesters on the node, harvesting limits and beneficiary percentage, and whether the given account's linked key is unlocked here. |
| `symbol_network_compare` | none | Height and finalization of the node versus `SYMBOL_REFERENCE_NODES`, blocks behind the best, `lagging` flags. Explains what to do when no reference nodes are configured. |
| `symbol_harvesting_income` | `account`, `fromDate` + `toDate` or `fromHeight` + `toHeight`, `granularity`, `format` | Harvest rewards received in the period: receipt count and exact XYM total (summed on the server as integers), harvester / beneficiary / unknown split, per-day buckets in `SYMBOL_TIMEZONE` or UTC, or a list of receipts. Dates are resolved to heights from block timestamps. `granularity: monthly` gives one row per calendar month (yearly questions); `output: csv` returns the rows as CSV text for a spreadsheet while the JSON stays available. |
| `symbol_transaction_status` | `transactionHashes` (array, 1 to 20) | Where each transaction stands right now: confirmed (with height), unconfirmed, partial (waiting for cosignatures), failed (with the node's code and its meaning) or not_found. One request for the whole batch. |
| `symbol_finality_participation` | `account`, `epoch` (optional, default latest finalized), `epochs` (1 to 20, default 1), `format` | Whether the account's voting key actually signed the finalization proof of each epoch: participated (both prevote and precommit), missed (which stage was not signed), no_active_key or unavailable, with the signature count per stage and a warning when no key covers the current epoch or the current epoch was missed (historical epochs never warn). |
| `symbol_delegation_diagnose` | `account`, `recentDays` (1 to 30, default 7), `format` | Is delegated harvesting active, and if not, where does it stop: account exists, balance within the harvesting limits, importance above zero (or blocks until the next recalculation), linked/VRF/node keys, node key equal to the configured node's `nodePublicKey`, remote key unlocked on that node, account type, harvested blocks in the last N days, and the persistent delegation request transfer to the node. Verdict `active`, `not_active` or `cannot_verify` (delegation to another node cannot be checked from here). |
| `symbol_node_health` | `format` | Is the configured node running healthily right now: API node and database status (a 503 `/node/health` answer is read, not treated as a failure), database block count versus chain height, node clock versus this machine's clock, finalization lag in blocks and minutes, and roles. Six checks in a fixed order, each ok/warn/fail/unknown with a hint; verdict `healthy`, `degraded` (a warning or a check that could not be made) or `unhealthy`. Thresholds derive from the network properties. Complements `symbol_node_status`. |
| `symbol_version_drift` | `format` | Is the node's software version behind the network majority: versions of the peers the node knows plus the reference nodes, as a distribution with the majority version and the share running something newer. Verdict `ok`, `behind` (older than the majority, or newer versions hold at least half the sample), `far_behind` (75% or more newer: peers may refuse connections) or `unknown` (no peers). Peer hosts and keys are never reported. |
| `symbol_harvester_watch` | `mode` (`compare`, `compare_and_save`, `save_only`), `format` | Did the delegated harvesters unlocked on the node increase or decrease since the last call: added and removed remote keys, count delta, and min / max / average over the snapshots of the last 30 days. Snapshots are kept in one file per node under `SYMBOL_STATE_DIR`; without it the current list is reported and no comparison is possible. `compare` reads only, `compare_and_save` (default) also stores the current list, `save_only` stores without comparing. |
### Example questions
**"When does the voting key of NCV5HRBSFEGTPNBIUPBVAGWXWXZ43C4TNOQUYUY expire, and when should I renew it?"**
→ `symbol_voting_key_status { "account": "NCV5HRBSFEGTPNBIUPBVAGWXWXZ43C4TNOQUYUY" }`
Returns each key's `startEpoch`/`endEpoch`, the expiry height `(endEpoch - 1) × votingSetGrouping`,
remaining epochs, blocks and days, an estimated expiry date based on the measured average block
time, the renewal window, free slots (expired keys still occupy slots) and whether the balance
meets `minVoterBalance`.
**"Show me alice's account."**
→ `symbol_account_get { "account": "alice" }`
The namespace `alice` is resolved through the node to its address alias (a missing, expired,
mosaic-aliased or alias-less namespace is an error with a hint); the answer starts with
`alice → NCV5…` and carries the resolution in `accountResolution`. Works for every account argument.
**"How much XYM does NCV5HRBSFEGTPNBIUPBVAGWXWXZ43C4TNOQUYUY hold?"**
→ `symbol_account_get { "account": "NCV5HRBSFEGTPNBIUPBVAGWXWXZ43C4TNOQUYUY" }`
Returns every mosaic with `alias` (`symbol.xym`), `amount` (decimals applied) and `rawAmount`.
**"Show the last 20 transfers involving that account, then the details of the newest one."**
→ `symbol_transaction_search { "address": "NCV5HR…", "type": "transfer", "pageSize": 20 }`
→ `symbol_transaction_get { "transactionHash": "<hash from the list>" }`
The list gives hashes, dates, counterparties and message previews; the second call adds fees,
full decoded messages and inner transactions.
**"Is my node behind?"**
→ `symbol_node_status {}` checks the age of the latest block on the configured node;
→ `symbol_network_compare {}` reports how many blocks it trails `SYMBOL_REFERENCE_NODES`.
**"How much did NCV5HRBSFEGTPNBIUPBVAGWXWXZ43C4TNOQUYUY earn from harvesting in August 2026?"**
→ `symbol_harvesting_income { "account": "NCV5HR…", "fromDate": "2026-08-01", "toDate": "2026-08-31" }`
Resolves the dates to block heights, reads every HarvestFee receipt addressed to the account and
sums them as exact integers: total XYM, harvester versus beneficiary share, and one row per day.
Nothing is left for the model to add up.
**"I just announced my voting key link. Did transaction FAEEB042… go through?"**
→ `symbol_transaction_status { "transactionHashes": ["FAEEB042…"] }`
Answers confirmed (with the height), unconfirmed, partial (aggregate bonded waiting for
cosignatures), failed (with the node's code such as `Failure_Core_Insufficient_Balance` and its
meaning) or not_found. Always an array, up to 20 hashes per call.
**"Was my voting node NCV5HRBSFEGTPNBIUPBVAGWXWXZ43C4TNOQUYUY actually able to vote last week?"**
→ `symbol_finality_participation { "account": "NCV5HR…", "epochs": 14 }`
Reads the finalization proof of the latest finalized epoch and the 13 before it (an epoch is
`votingSetGrouping` blocks, about 12 hours on mainnet) and reports per epoch whether one of the
account's voting keys is among the signers of both stages, how many voters signed, and a warning
if the current epoch was missed or no key covers it.
**"I think my delegated harvesting is not working. Have a look at NCV5HRBSFEGTPNBIUPBVAGWXWXZ43C4TNOQUYUY."**
→ `symbol_delegation_diagnose { "account": "NCV5HR…" }`
Runs eleven checks in a fixed order (existence, balance limits, importance, the three key links, node
key versus the configured node, unlocked on that node, account type, recent harvested blocks, the
delegation request transfer) and answers `active`, `not_active` (with the failing step and a hint) or
`cannot_verify` (the account delegates to a node other than `SYMBOL_NODE_URL`, so the node side cannot
be checked).
**"Is my node healthy, and is its version behind?"**
→ `symbol_node_health {}` checks the API node, database, storage, clock and finalization lag of the
configured node and answers healthy / degraded / unhealthy with the failing checks;
→ `symbol_version_drift {}` compares the node version with its peers and the reference nodes and
answers ok / behind / far_behind. Both are the first things to look at after a node OS migration.
**"Have my delegators come back after the migration?"**
→ `symbol_harvester_watch {}` compares the harvesters unlocked on the node right now with the last
stored snapshot (added and removed keys, count delta, 30-day min / max / average) and stores today's
list for the next check. Needs `SYMBOL_STATE_DIR`; without it the tool reports the current count and
says no comparison is possible.
More cases, with the exact arguments expected for each, are in [`evals/cases.json`](evals/cases.json).
## Prompts
Two MCP prompts (`prompts/list`) bundle the tool calls a node operator repeats. Both take one
argument, `account`: the 39-character base32 address of the voting / harvesting account. The
prompt text contains no addresses, hosts, keys or dates of its own.
| Prompt | What it walks through |
|---|---|
| `voting_key_renewal_checklist` | `symbol_voting_key_status` (expiry, renewal window, free slots), `symbol_node_status` (stop if not synced), `symbol_network_compare`, then, after the operator has announced the VotingKeyLink outside this server, `symbol_transaction_status` on the hash, a second `symbol_voting_key_status` to confirm the new key, and `symbol_finality_participation` once the new key's start epoch is finalized. Ends with a four-line summary. |
| `monthly_health_check` | `symbol_node_status`, `symbol_node_health` (unhealthy goes first), `symbol_version_drift` (behind or far_behind goes first), `symbol_network_compare`, `symbol_harvester_watch` (delta against the previous snapshot; `symbol_harvesting_status` only on request), `symbol_voting_key_status` (warning first if a key expires within 30 days), `symbol_account_get` (balance versus `minVoterBalance`) and `symbol_harvesting_income` for the previous calendar month. Reports on one screen as Action required / Attention / Normal. |
The server also sends short `instructions` at initialize time (read-only, account formats, which
tool answers harvest-income and voting-key questions, use the returned numbers as they are).
## Security
- **Read-only.** No tool signs, builds or announces transactions. No argument accepts a private key,
mnemonic or token. Nothing is stored between calls, except that `symbol_harvester_watch` keeps its
per-node snapshot of unlocked harvester public keys, heights and times under `SYMBOL_STATE_DIR` when
that variable is set (no secrets; delete the file to start over).
- **Fixed destinations.** The server contacts only `SYMBOL_NODE_URL` and, for
`symbol_network_compare` and `symbol_version_drift`, the hosts listed in `SYMBOL_REFERENCE_NODES`. Tools never take a URL as
an argument, so a model cannot redirect requests. There is no telemetry.
- **Untrusted chain data.** Transfer messages, node friendly names, host names and alias names are
written by third parties. They are exposed under names that make this obvious (`messageText`),
control and bidi characters are stripped, and length is capped. Treat them as data, not
instructions.
- **Fail loudly.** A network mismatch (`SYMBOL_NETWORK` versus the node), an unreachable node or an
unexpected response shape is an error with a recovery hint, never a silent fallback to another
network. Stack traces and raw HTTP bodies are never returned to the model.
- **Request hygiene.** Per-request timeout, `User-Agent`, a 5 MB response cap, at most 4 concurrent
requests, and schema validation of every response.
Vulnerability reports: see [`SECURITY.md`](SECURITY.md).
## Supported networks
| Network | Identifier | Detected by generation hash seed | Example node |
|---|---|---|---|
| Symbol mainnet | 104 | `57F7DA20…72B2D6` | `https://sym-main-01.opening-line.jp:3001` |
| Symbol testnet (sai) | 152 | `49D6E1CE…FC665A4` | `https://sym-test-01.opening-line.jp:3001` |
The network is detected from the node at start-up. Any other generation hash seed (private
networks, NEM NIS1) is rejected. Node availability changes over time; pick a current one from
https://nodewatch.symbol.tools/.
## Limitations
- **Node history.** Results come from the configured node. Nodes that prune transaction history
return only what they still hold, so `symbol_transaction_search` may miss old transactions on
such nodes.
- **Future dates are estimates.** Expiry dates for voting keys, namespaces and mosaics, and any
future height or epoch, are projected from the measured average block time over the last 10,000
blocks (about 30 s on mainnet) and are flagged as estimates.
- **Encrypted messages are not decrypted**; they are reported as encrypted.
- **Page size is 10 to 100**, because catapult-rest coerces smaller pages to 10.
- **Confirmed transactions only** in search. Unconfirmed and partial transactions are visible
through `symbol_transaction_get` by hash.
- **Harvesting status covers the configured node** (`/node/unlockedaccount`), not the whole network.
- **Harvester history is local.** `symbol_harvester_watch` compares against snapshots it wrote itself
under `SYMBOL_STATE_DIR`; another machine, a deleted file or a changed node key (a new node.key.pem
after a migration) starts a new baseline. Repeated calls on the same day add repeated snapshots;
only the newest 60 are kept.
- **Harvest income reads at most 20,000 statements per call** (200 pages of 100). A longer period
comes back `truncated`; split it with `fromHeight`/`toHeight`. Rewards are summed from HarvestFee
receipts, so a node that prunes receipts reports less than the chain holds.
- **Finality participation reads the proofs the node holds.** `unavailable` means the node has no
proof for that epoch (not finalized yet, or outside the history it keeps), not that the account
did not vote. The server does not know how many voters are registered, so `signatureCount` can
only be compared with an external list such as nodewatch.
- **Version drift is sampled, not surveyed.** `symbol_version_drift` sees the peers the configured
node currently knows plus the reference nodes, not the whole network; the full picture is on
nodewatch. Clock skew in `symbol_node_health` is measured against the clock of the machine running
this server, which may itself be off.
- **Mainnet and testnet only.** No transaction building, signing or announcing, by design.
- **The URL is used as given.** The server does not switch ports or schemes on its own; if a node
only serves port 3000 over http, it cannot be used unless it is on localhost.
## Development
```sh
npm ci
npm run lint && npm run typecheck && npm test
npm run build
SYMBOL_NODE_URL=https://<node-host>:3001 node dist/index.js
npx @modelcontextprotocol/inspector node dist/index.js
SYMBOL_INTEGRATION=1 SYMBOL_NODE_URL=https://sym-test-01.opening-line.jp:3001 npm test # live-node tests
SYMBOL_INTEGRATION=1 SYMBOL_NODE_URL=https://<node-host>:3001 SYMBOL_INTEGRATION_ACCOUNT=<address> npm test # account tools against a specific account
node scripts/capture-fixtures.mjs https://<node-host>:3001 # refresh test/fixtures/<network>/ from a node
```
Design notes: [`docs/DESIGN-BRIEF.md`](docs/DESIGN-BRIEF.md). Changes: [`CHANGELOG.md`](CHANGELOG.md).
## License
[MIT](LICENSE)
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues