Back to Directory/Developer Tools

io.github.inotakeh/symbol

Read-only MCP server for the Symbol blockchain (XYM): node health, harvesting, voting keys.

Developer ToolsTypeScriptv0.9.3

symbol-mcp-server

npm version OpenSSF Scorecard OpenSSF Best Practices

Symbol only. This server talks to Symbol (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

Read-only MCP server that turns the Symbol REST API into 22 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 (only symbol_harvesting_income with output: "csv" puts CSV in the text instead), with a short summary first: its first line answers the question, and further lines add details such as each month or each check that is not ok. 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.

What it looks like

Two calls and excerpts of what they return. The values come from the test fixtures (a synthetic node host and account), not from a live node.

"Is my node healthy, and is it in sync?" → symbol_node_status {}

{
  "summary": "node.test:3001 (friendlyName \"fixture-node\", host \"mainnet-node.example\") on mainnet is healthy and synced.\nSymbol 1.0.3.9, roles Peer/API/Voting; height 5,763,675, finalized 5,763,656 (epoch 4004); 6 peers; latest block 201 s old (not synced above 300 s).",
  "verdict": "healthy",
  "sync": {
    "synced": true,
    "latestBlockTime": { "utc": "2026-09-10T03:01:38.808Z" },
    "ageSeconds": 201,
    "thresholdSeconds": 300,
    "checkedAt": { "utc": "2026-09-10T03:05:00.000Z" }
  },
  "checks": [
    { "id": "api_node", "status": "ok", "detail": "API node service is up.", "hint": null },
    { "id": "db", "status": "ok", "detail": "Database service is up.", "hint": null },
    {
      "id": "clock_skew",
      "status": "ok",
      "detail": "Node clock is 1,000 ms behind this machine's clock (warn at 15,000 ms, fail at 30,000 ms).",
      "hint": null
    },
    {
      "id": "chain_tip_age",
      "status": "ok",
      "detail": "Latest block (height 5,763,675) is 201 s old (about 3.4 min; warn above 300 s, fail above 900 s).",
      "hint": null
    }
    // … and storage_consistent, finalization_lag and roles
  ]
  // … then node, network, chain, storage, peers, time and notes
}

verdict folds the seven checks into one word, and sync.synced says whether the node follows the chain (null when that could not be judged). A check that is not ok carries a hint with the next step, and the summary lists it on its own line. A request that fails makes its check unknown and its fields null; the call still answers.

"How much did I earn from harvesting, month by month?" → symbol_harvesting_income { "account": "NCV5HR…", "fromDate": "2026-09-01", "toDate": "2026-09-11", "granularity": "monthly" }

NCV5HRBSFEGTPNBIUPBVAGWXWXZ43C4TNOQUYUY on mainnet, 2026-09-01 to 2026-09-11 (Asia/Tokyo; heights 5,736,305-5,767,984, 31,680 blocks): 20 harvest receipts totalling 662.574177 symbol.xym from 11 blocks (harvester share 461.240390 in 9 receipts, beneficiary share 201.333787 in 11 receipts).
Blocks: 9 harvested by this account, 2 harvested by others that paid it only the beneficiary share (typically delegators on its node). 9 of the 11 beneficiary receipts come from blocks it harvested itself, as its own node's beneficiary.
2026-09: 20 receipts, 662.574177 symbol.xym; 11 blocks: 9 harvested by this account, 2 by others (receipts 9 harvester / 11 beneficiary)

That is the summary; the same numbers are in totals and monthly[], each amount both as a decimal string ("662.574177") and as the raw integer ("662574177"), summed by the server. Receipts are shares of a block's reward, so an operator that is its own node's beneficiary gets two for each block it harvests; blocksHarvested and blocksBeneficiaryOnly count the blocks.

Requirements

  • Node.js 22 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

Claude Desktop: one-click bundle (.mcpb)

  1. Download the latest symbol-mcp-server-<version>.mcpb from Releases.
  2. Double-click it, or open Claude Desktop's Settings → Extensions and install it there.
  3. In the settings form, enter the Symbol node URL, for example https://<node-host>:3001 (your own node is best, see Choosing a node). It is required: the extension does not start without it. Optionally set the time zone and a state directory, where symbol_harvesting_status keeps its snapshots; the other fields can stay empty.
  4. Enable the extension.

After changing these settings later, try them in a new conversation.

The bundle holds the server built from the published npm package with its production dependencies and runs on the Node.js that ships with Claude Desktop. Use either the bundle or the npx configuration below, not both, or every tool appears twice. Claude Desktop may show the extension as unverified because the bundle is not signed with mcpb sign; its origin can be checked with the build provenance the release workflow attaches:

gh attestation verify symbol-mcp-server-<version>.mcpb --repo inotakeh/symbol-mcp-server

npm and source

From npm (recommended for every other MCP host):

npx -y symbol-mcp-server --help

Also listed in the MCP Registry as io.github.inotakeh/symbol.

From source:

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. Apart from those two and the check subcommand (see CLI: monitoring from cron), the binary takes no arguments: 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 <version>: mainnet via <node-host>:3001, timezone Asia/Tokyo

Claude Desktop

Add to claude_desktop_config.json. With the npm package:

{
  "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:

{
  "mcpServers": {
    "symbol": {
      "command": "node",
      "args": ["/path/to/symbol-mcp-server/dist/index.js"],
      "env": {
        "SYMBOL_NODE_URL": "https://<node-host>:3001"
      }
    }
  }
}

Claude Code

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:

{
  "mcpServers": {
    "symbol": {
      "command": "npx",
      "args": ["-y", "symbol-mcp-server"],
      "env": { "SYMBOL_NODE_URL": "https://<node-host>:3001" }
    }
  }
}

Other clients

The same npx command works in any MCP host that starts stdio servers. File locations and formats below follow each client's own documentation.

Cursor: ~/.cursor/mcp.json for all projects, or .cursor/mcp.json in one project.

{
  "mcpServers": {
    "symbol": {
      "command": "npx",
      "args": ["-y", "symbol-mcp-server"],
      "env": { "SYMBOL_NODE_URL": "https://<node-host>:3001" }
    }
  }
}

VS Code: .vscode/mcp.json in the workspace, or the user-level file opened with the command MCP: Open User Configuration. The top-level key is servers, not mcpServers.

{
  "servers": {
    "symbol": {
      "type": "stdio",
      "command": "npx",
      "args": ["-y", "symbol-mcp-server"],
      "env": { "SYMBOL_NODE_URL": "https://<node-host>:3001" }
    }
  }
}

Cline: in the Cline panel, open MCP Servers → Configure → Configure MCP Servers and add the server under mcpServers (the Cline CLI reads the same format from ~/.cline/mcp.json).

{
  "mcpServers": {
    "symbol": {
      "command": "npx",
      "args": ["-y", "symbol-mcp-server"],
      "env": { "SYMBOL_NODE_URL": "https://<node-host>:3001" },
      "disabled": false,
      "autoApprove": []
    }
  }
}

On Windows, a client that cannot start npx may need "command": "npx.cmd".

Environment variables

VariableRequiredMeaning
SYMBOL_NODE_URLyesREST URL of the node to query, e.g. https://<node-host>:3001. https:// is required (http:// only for localhost / 127.0.0.1 / [::1]). The port is used exactly as given.
SYMBOL_NETWORKnomainnet or testnet. When set, start-up fails if the node reports a different network.
SYMBOL_TIMEZONEnoIANA zone such as Asia/Tokyo. Adds a local time next to every UTC timestamp.
SYMBOL_REFERENCE_NODESnoComma-separated https:// node URLs that symbol_network_compare and symbol_version_drift check against. No other host is ever contacted.
SYMBOL_REQUEST_TIMEOUT_MSnoPer-request timeout, 100 to 600000. Default 10000.
SYMBOL_STATE_DIRnoAbsolute directory where symbol_harvesting_status keeps one snapshot file per node (unlocked harvester public keys, heights and times; no secrets) when it is asked to compare or save. Created on first save with mode 0700. Unset: the tool reports the current list without a comparison.

Choosing a node

  • Your own node is the best choice. Every call sends the addresses, public keys, hashes and namespace names you ask about to SYMBOL_NODE_URL. The reference nodes only receive /node/info and /chain/info requests, never your identifiers.
  • A public node works, but its operator can see what you look up. Its access logs can show which accounts, transactions and namespaces were queried, when, and from which IP address. Use a node you trust, or your own node for anything you would rather keep private.
  • Finding one: https://nodewatch.symbol.tools/ lists mainnet and testnet nodes with their height and version. Pick an API node that is at the current height, runs the majority version and answers over https:// (usually port 3001). Set SYMBOL_NETWORK to make start-up fail if the node turns out to be on the other network, and try it with SYMBOL_NODE_URL=https://<node-host>:3001 npx -y symbol-mcp-server check.

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 48-character hex address, a hex public key, or a namespace name such as alice or alice.pay that carries an address alias; the resolution of a name is reported in accountResolution and at the start of the summary.

ToolArgumentsAnswers
symbol_network_infononeNetwork 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_statusformatIs the configured node in sync and are its services healthy right now, and what is it. One verdict, healthy, degraded (a warning or a check that could not be made) or unhealthy, from seven checks in a fixed order, each ok/warn/fail/unknown with a hint: 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, roles, and the age of the latest block versus this machine's clock, which catches a node that has stopped following the chain (warn beyond 10 target block times, fail beyond 30). sync.synced is true while the latest block is at most 10 target block times old (5 minutes on mainnet), false beyond that, and null when it could not be judged. Also friendly name, host, roles (Peer/API/Voting), decoded version, network, heights, peer count, database counts and the node clock. A request that fails makes its check unknown and its fields null instead of failing the call. Thresholds derive from the network properties.
symbol_account_getaccount (address, public key or namespace name), formatAddress 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 (as a multisig account or as a cosignatory).
symbol_voting_key_statusaccountEvery 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_gettransactionHashLooks 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. A transaction the node rejected is reported as failed, with the node's code and its meaning; a hash the node does not know as not_found.
symbol_transaction_searchaddress, type, pageSize, pageNumber, order, formatConfirmed transactions involving an account, newest first by default, optional type filter by name (transfer) or code (16724), 10 to 100 per page.
symbol_mosaic_getmosaic (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_getnamespace (name or hex id)Owner, root or sub, level names, alias target (address or mosaic), start and end height, estimated expiry date.
symbol_fee_estimatetransactionSizeBytes (optional)Slow/average/median/fast fee tiers in XYM computed from the node's current multipliers, for the size given or else a transfer with 1 mosaic and a 20-character ASCII message (197 bytes). A transfer counts 160 bytes, 16 per mosaic and a plain message as its UTF-8 bytes plus 1 type byte (usually 3 bytes per Japanese character; an encrypted message is larger); this count is not for aggregate transactions. Nothing is signed or sent.
symbol_address_parsevalue (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_convertone of height, epoch, timestampHeight, finalization epoch, network timestamp and wall-clock time. Exact for the past, estimated (and flagged) for the future.
symbol_harvesting_statusmode (current, compare, compare_and_save, save_only), formatThe delegated harvesters unlocked on the node: how many there are now, and the harvesting limits and beneficiary percentage. current (default) reads the node only; the keys themselves come with format: detailed. The other modes answer whether the harvesters increased or decreased since the last stored snapshot: 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 also stores the current list, save_only stores without comparing. For one account (is its linked key unlocked here, is its balance within the limits), use symbol_delegation_diagnose.
symbol_network_comparenoneHeight 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, and says so when none of them could be compared.
symbol_harvesting_incomeaccount, fromDate + toDate or fromHeight + toHeight, granularity, format, outputHarvest rewards received in the period: receipt count and exact XYM total (summed on the server as integers), harvester / beneficiary / unknown split, block counts (blocksHarvested: blocks the account harvested; blocksBeneficiaryOnly: blocks others harvested that paid it only the beneficiary share), 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. A year or more in one call is fine: the range is read in chunks of about 90 days (fetch reports chunks, retries and pages).
symbol_transaction_statustransactionHashes (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_participationaccount, epoch (optional, default latest finalized), epochs (1 to 20, default 1), formatWhether 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 (a stage that the proof splits into several message groups counts as one stage; a signature in any of its groups counts) and a warning when no key covers the current epoch or the current epoch was missed (historical epochs never warn).
symbol_delegation_diagnoseaccount, recentDays (1 to 30, default 7), formatIs 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_version_driftformatIs 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 usable peers, or the node reports no version of its own). Peers that report no version yet (0.0.0.0) are counted apart (sample.unknownVersion), not as a version. Peer hosts and keys are never reported.
symbol_account_rankaccount (optional), mosaic (optional; hex id or alias, default XYM), top (1 to 100, default 20), maxRank (100 to 5000, default 1000), formatWhere an account ranks among the holders of a mosaic and who the top holders are, like an explorer rich list: the account's balance, share of supply (4 decimals, integer arithmetic) and rank, the top N holders with balances and shares, and the combined top-N share. Holders are read from GET /accounts?orderBy=balance 100 per request, one request at a time, until the account is found or maxRank is reached (rankBeyond then says so). Omit account for the top list only. Ties are ordered by the node; no labels (exchange, foundation) are attached.
symbol_holdings_valueaccount, unitPrice (decimal string, e.g. "12.34"), currency (3 to 6 upper-case letters), priceSource (optional), priceAsOf (optional), mosaic (optional, default XYM), decimals (optional, 0 to 12), formatWhat the account's balance of a mosaic is worth at a unit price the caller supplies: the balance, the normalised price, the exact product and the product rounded half up, all in integer arithmetic. The rounding keeps the digits Intl (Unicode CLDR) gives the currency (JPY 0, USD 2, KWD 3, CLF 4); CLDR differs from ISO 4217 for a few codes (HUF, IDR, IQD and IRR have 0 digits in CLDR; ISO 4217 gives IQD 3 and the others 2), and the digits come from the Node.js that runs the server, so pass decimals to fix them. A code Intl does not know (BTC, USDT) is not rounded, and neither is a non-zero value that would round to 0; value.decimalsSource says which rule applied. The server never fetches or checks prices; priceSource and priceAsOf are echoed so the answer states where the number came from. Not a tax computation: no fees, spread or taxes.

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 <hash> go through?" → symbol_transaction_status { "transactionHashes": ["<hash>"] } 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_status {} checks the API node, database, storage, clock, finalization lag and the age of the latest block of the configured node, answers healthy / degraded / unhealthy with the failing checks, and says whether the node is synced; → 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_harvesting_status { "mode": "compare_and_save" } 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; "mode": "compare" looks without storing. Needs SYMBOL_STATE_DIR; without it the tool reports the current count and says no comparison is possible. Without a mode the tool answers "how many delegators right now?" and touches no snapshot.

"Where does NCV5HRBSFEGTPNBIUPBVAGWXWXZ43C4TNOQUYUY rank by XYM holdings? Who are the top 10?" → symbol_account_rank { "account": "NCV5HRBSFEGTPNBIUPBVAGWXWXZ43C4TNOQUYUY", "top": 10 } Reads the holder list ordered by balance 100 accounts at a time until the account turns up (or maxRank, default 1000, is reached), and returns its balance, share of supply and rank together with the top 10 holders and their combined share. All shares are computed by the server in integer arithmetic; the top of the list is usually exchanges and the foundation, and the tool labels nobody.

"If XYM is 12.34 yen, how much are my holdings worth? NCV5HRBSFEGTPNBIUPBVAGWXWXZ43C4TNOQUYUY" → symbol_holdings_value { "account": "NCV5HR…", "unitPrice": "12.34", "currency": "JPY" } Reads the balance and multiplies it by the price in integer arithmetic: 53,321,140 JPY for 4,321,000 XYM, with the exact product alongside. The price comes from the caller. This server never contacts a price API (it talks to SYMBOL_NODE_URL only), so when you ask "what are my holdings worth right now?" the flow in Claude Desktop is: the model looks the price up first (a web search, another MCP server that serves prices, or you type it in), then calls this tool with unitPrice, currency and, ideally, priceSource / priceAsOf so the answer says where and when the price was observed. The model is told not to multiply balance by price itself.

More cases, with the exact arguments expected for each, are in 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.

PromptWhat it walks through
voting_key_renewal_checklistsymbol_voting_key_status (expiry, renewal window, free slots and its warnings as given), symbol_node_status (stop if not synced, or if the sync could not be judged; its verdict and the checks that are not ok go under open items), symbol_network_compare (sync reported as not confirmed when it could not 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_checksymbol_node_status (verdict, sync, version and peer count; unhealthy goes first), symbol_version_drift (behind or far_behind goes first), symbol_network_compare (sync reported as not confirmed when it could not compare), symbol_harvesting_status in mode compare_and_save (delta against the previous snapshot), symbol_voting_key_status (remaining days and expiry; its warnings go first, as given), symbol_account_get (balance versus minVoterBalance) and symbol_harvesting_income for the previous calendar month (the income, then the blocks the account harvested itself and the blocks of delegators or others as separate items). 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 the questions that are easy to mix up: harvest income, voting keys, node health and sync versus version, whether a transaction went through; use the returned numbers as they are). Tools that answer neighbouring questions point to each other in their descriptions.

CLI: monitoring from cron

The same binary has a one-shot check subcommand that needs no MCP client. It judges the node with the tools above, prints one report and exits non-zero when something is wrong:

symbol-mcp-server check [--account <address|publicKey|namespace>] [--warn-days <n>]
                        [--cert <path>]... [--cert-warn-days <n>]
                        [--format text|json] [--quiet]

It reads the same environment variables as the server (SYMBOL_NODE_URL is required; SYMBOL_TIMEZONE, SYMBOL_REFERENCE_NODES and SYMBOL_STATE_DIR are optional) and needs Node.js 22 or newer. Started without arguments the binary is still the MCP server, unchanged.

#Itemok / warn / fail
1node_healthThe verdict of symbol_node_status: healthy / degraded / unhealthy. A node whose latest block is older than 10 target block times (5 minutes on mainnet) is degraded, older than 30 unhealthy
2version_driftsymbol_version_drift: ok / behind or unknown / far_behind
3harvester_watchsymbol_harvesting_status in mode compare_and_save: warn when fewer harvesters are unlocked than at the previous run, or when the snapshot could not be saved. Skipped without SYMBOL_STATE_DIR
4voting_key_statusWith --account: warn when the active voting key expires within --warn-days (default 14, 1 to 120), fail within 3 days or without an active key; ok when a successor key is already registered without a gap. Fail also when the balance is below minVoterBalance (the account cannot vote), successor or not. Full key slots change no status, but the hint of a warn or fail then adds the tool's slot warning, unless a successor key is already registered. Skipped without --account
5finality_participationWith --account, latest finalized epoch: participated / missed or no proof on the node / no key covers the epoch. Skipped without --account
6certificateWith --cert <path>, once for each copy of the node certificate: fail when a certificate has expired or has fewer than 7 days left, warn with fewer than --cert-warn-days (default 30) or when the files are not copies of one certificate. A file that cannot be read, or is not a certificate, fails with the reason. Skipped without --cert. See Node certificate files

For items 1 to 5 the judgments are the tools' own; the check only reads their output, and the hint printed under a warn or fail line is the tool's text. A tool that fails (for example an HTTP error) fails its item and the others still run. Item 6 is the exception: no REST endpoint shows a node's certificate files, so the check reads the files itself and has no MCP tool behind it.

Exit codeMeaning
0every item is ok or skipped
1at least one warning, no failure
2at least one failure
3the check could not run: configuration error, node unreachable, or bad arguments (one or two lines on stderr say why)

Text output (the default; illustrative values):

symbol check: WARN (node.example:3001, mainnet, 2026-01-15T07:00:03+09:00)
[ok] node_health: healthy (finalization lag 12 blocks)
[ok] version_drift: ok. node.example:3001 runs 1.0.3.9; majority of 24 sampled nodes runs 1.0.3.9; 0% run something newer.
[ok] harvester_watch: 18 unlocked harvesters on node.example:3001, unchanged since 2026-01-14T07:00:02+09:00 (2026-01-13T22:00:02.000Z). Snapshot saved (31 stored).
[warn] voting_key_status: active key 0A1B2C3D… expires in about 12.4 days (epoch 4321, estimated 2026-01-27T16:40:00+09:00 (2026-01-27T07:40:00.000Z))
  hint: Active voting key 0A1B2C3D… expires at epoch 4321 in about 12.4 days (...) and no successor key is registered.
[ok] finality_participation: epoch 4290: participated (signed prevote and precommit)
[skip] certificate: no --cert given; pass --cert <path> for each copy of the node certificate to check its expiry

--format json prints the same report as one JSON document: { verdict, exitCode, node: { host, network }, checkedAt, checks: [{ id, status, detail, hint }], warnDays, account }, with verdict one of ok, warn, fail, error (exit code 3) and account the resolved address. --quiet prints nothing when the exit code is 0, so cron only mails when there is something to read:

MAILTO=you@example.com
0 7 * * * SYMBOL_NODE_URL=https://node.example:3001 SYMBOL_STATE_DIR=/var/lib/symbol-mcp-server \
  npx --yes symbol-mcp-server check --account NXXX... --warn-days 14 --quiet \
  --cert /path/to/target/nodes/node/cert/node.crt.pem \
  --cert /path/to/target/gateways/rest-gateway/api-node-config/cert/node.crt.pem

The entry is broken into lines here only to fit the page: cron has no line continuation, so write it on one line in the crontab.

  • The check sends no notification. It writes to stdout and stderr and sets the exit code; mail is cron's job (MAILTO). It contacts SYMBOL_NODE_URL and the SYMBOL_REFERENCE_NODES, nothing else, and is as read-only as the server. With SYMBOL_STATE_DIR set, every run appends one snapshot to the file symbol_harvesting_status uses (the newest 60 are kept). With --cert it reads the named files on the machine it runs on; nothing of them is sent anywhere.
  • The whole run is limited to 120 seconds. At the limit the remaining items are skipped, the reason goes to stderr, and the result is WARN at best, printed even with --quiet.
  • A typo in the subcommand name is an unknown argument of the server and exits with 2, as before.

Node certificate files (--cert)

A node set up with symbol-bootstrap keeps its certificate in two places: the node itself uses target/nodes/node/cert/node.crt.pem, and the REST gateway has its own copy at target/gateways/rest-gateway/api-node-config/cert/node.crt.pem. symbol-bootstrap renewCertificates renews the first and does not touch the second. When only the REST gateway's copy expires, the gateway can no longer talk to the node and /node/health reports apiNode: down. Pass both files, and check tells you before that happens:

symbol-mcp-server check --cert target/nodes/node/cert/node.crt.pem \
  --cert target/gateways/rest-gateway/api-node-config/cert/node.crt.pem
[warn] certificate: warn (copies differ: target/nodes/node/cert/node.crt.pem vs target/gateways/rest-gateway/api-node-config/cert/node.crt.pem)
  target/nodes/node/cert/node.crt.pem: ok, expires 2027-01-20T02:11:09.000Z, 370 days left, sha256 0A:1B:2C:…
  target/gateways/rest-gateway/api-node-config/cert/node.crt.pem: ok, expires 2026-03-01T02:11:09.000Z, 45 days left, sha256 3D:4E:5F:…
  • Each file: fail when the certificate has expired or has fewer than 7 days left (fixed, not an option), warn with fewer than --cert-warn-days days left (default 30, a whole number of 1 or more; independent of --warn-days), ok otherwise. Days left are whole days from now to the certificate's notAfter, rounded down, so the last day counts as 0. A file that holds several certificates (node.full.crt.pem) is judged by its first one.
  • Copies: with two or more files, their SHA-256 fingerprints must all be the same; if not, the item is at least warn and names the files that differ. A renewal that replaced one copy and not the other shows up here on the same day, long before the old copy expires. Giving the same path twice is refused as a usage error, since it would compare the file with itself (paths are compared as written, after resolving . and ..; a link to the same file, or another spelling of it on a file system that ignores case, is not noticed).
  • The item's verdict is the worst of the files and the comparison. A file that cannot be read or is not a certificate fails with the reason (cannot be read (ENOENT), not a certificate …), and the other files and the other items are still checked.
  • Run it on the node's server. The files are read from the local disk; no MCP tool can do this, because the REST API does not show them. A relative path is resolved against the current directory; cron starts commands in the home directory, so give absolute paths there.
  • Never pass a key. A file whose content contains PRIVATE KEY (the label of every PEM private key) is not parsed and fails with a private key was passed; pass the certificate (.crt.pem). Any other file that is not a certificate fails as not a certificate. Either way, the report holds only the path you gave, the verdict, notAfter (ISO 8601, UTC), the days left, the SHA-256 fingerprint and, in the JSON, the subject's common name; no other content of a file is printed.
  • --format json adds files: [{ path, status, notAfter, daysLeft, fingerprint256, commonName, reason }] to the certificate item when it ran (reason says why a file could not be used and is null otherwise; daysLeft is negative once expired).
  • When the node stops answering during the run and every node-backed item fails for that reason, the exit code stays 3 whatever the files say; the certificate lines are still printed. When /node/info cannot be read at start-up (the node does not answer, or answers with an error), nothing is checked, certificate included: the exit code is 3 and only the lines on stderr are printed. The item is there to warn ahead of time; once the node is in that state, look at the files by hand (openssl x509 -noout -enddate -in <file>).

Security

  • Read-only. No tool signs, builds or announces transactions. No argument accepts a private key, mnemonic or token. A 64-character hex account argument is taken as a public key and turned into its address on this machine, and the node is asked for the address: a private key pasted by mistake never reaches the node, and errors show at most its first 8 characters. Nothing is stored between calls, except that symbol_harvesting_status, in the modes that save (compare_and_save, save_only), 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).
  • Certificate files only on request. The MCP server reads no certificate or key. Only check --cert <path> does, and only the files named there, on the machine it runs on. It opens no key file on its own, and a PEM private key passed by mistake (a file whose content contains PRIVATE KEY) is refused without being parsed. Nothing of a file is printed except the certificate's expiry date, SHA-256 fingerprint and common name, and nothing is sent anywhere.
  • 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, metadata values, alias names and what a node reports about itself (friendly name, host name, status and version strings) are written by third parties. They are exposed under names that make this obvious (messageText) and made one line: tabs and line breaks become a space, so words stay apart, and runs of spaces become one. Every other control and invisible format character is stripped (zero-width and bidi characters, soft hyphens, and the tag characters U+E0000 to U+E007F that people cannot see but models can read), and length is capped without splitting a character. Variation selectors are kept, so emoji and ideograph variants survive; emoji joined by a zero-width joiner come out as separate emoji. The 16 tools that show such text report in invisibleCharactersRemoved how many characters were removed from it, and when any were, the summary ends with a line saying so. In the summary, such text also appears after a label and in double quotes, with quotes and backslashes escaped as in JSON (untrusted message: "…", friendlyName "…"), so it cannot close the quote and read as the server's own words; alias and namespace names that fit the namespace grammar stay as they are. Treat all of it 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 applied while the body streams in (a larger declared Content-Length is refused unread), at most 4 concurrent requests per node, and schema validation of every response. Redirects are never followed: a node that answers with HTTP 3xx gets an error, and the address it points to is not contacted. Request paths carry plain identifiers only.
  • Repository settings. CodeQL code scanning, secret scanning with push protection, Dependabot (security updates and grouped monthly version updates), branch protection on main (every change lands through a pull request, with linear history) and private vulnerability reporting are enabled.

Vulnerability reports: see SECURITY.md.

Release integrity

  • Published from CI, with provenance. Every npm release is built and published by the GitHub Actions workflow release.yml through npm trusted publishing (OIDC). There is no npm token, neither on a maintainer's machine nor in the repository secrets. Each version carries a provenance attestation that links it to the source commit and the workflow run that built it.

  • Check it yourself.

    • The package page on npmjs.com shows a Provenance section with the commit and the workflow run.
    • npm view symbol-mcp-server dist.attestations prints the attestation URL and the provenance predicate type of the latest version.
    • In a project that installs it, npm audit signatures verifies the registry signatures and provenance attestations of the installed packages.
  • GitHub Releases carry the same package. From 0.8.0 on, each GitHub Release has three files attached (earlier releases have the first two): symbol-mcp-server-<version>.tgz, byte for byte the tarball npm serves (its SHA-512 is checked against the registry's dist.integrity), symbol-mcp-server-<version>.tgz.sigstore.json, npm's SLSA provenance for that tarball as a Sigstore bundle (its subject is checked to be the tarball's SHA-512), and the Claude Desktop bundle symbol-mcp-server-<version>.mcpb (next point). The first two are collected by scripts/release-assets.sh. To verify a downloaded pair with the GitHub CLI:

    gh attestation verify symbol-mcp-server-<version>.tgz \
      --bundle symbol-mcp-server-<version>.tgz.sigstore.json \
      --repo inotakeh/symbol-mcp-server --digest-alg sha512
    

    --digest-alg sha512 is needed because npm's provenance names the tarball by its SHA-512.

  • The Claude Desktop bundle is built from that same tarball. symbol-mcp-server-<version>.mcpb is made by scripts/build-mcpb.sh from the checked npm tarball above plus the production dependencies installed with npm ci --omit=dev from the lockfile of that release; nothing else is compiled or downloaded. The release workflow attaches a GitHub build provenance attestation to it (gh attestation verify … --repo inotakeh/symbol-mcp-server, see Install).

  • Who can release. Only maintainers create release tags (v1.2.3). The workflow first waits in the npm-publish GitHub Environment until a maintainer approves the run; only then does it check that the tag matches package.json and the other release files, run lint, typecheck and tests, and publish. The whole procedure is in docs/RELEASING.md.

Supported networks

NetworkIdentifierDetected by generation hash seed
Symbol mainnet10457F7DA20…72B2D6
Symbol testnet (sai)15249D6E1CE…FC665A4

The network is detected from the node at start-up. Any other generation hash seed (private networks, NEM NIS1) is rejected. Nodes of both networks are listed at https://nodewatch.symbol.tools/; see Choosing a node.

Limitations

  • Node history and limits. 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. A public node may also limit how many requests it accepts. The calls that send the most are symbol_account_rank (up to 50 pages) and symbol_harvesting_income over a long period (up to 200 pages); use your own node for those.
  • 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.
  • No prices. symbol_holdings_value multiplies a balance by a unit price the caller passes in; it does not fetch, check or remember prices, and the result is only as good as that input. Look the price up first (web search, a price MCP server, or the user) and pass it with priceSource and priceAsOf. The value is the plain product: no fees, spread or taxes, and no tax lot accounting.
  • Holder rank is a scan, not an index. symbol_account_rank reads the holder list 100 accounts per request down to maxRank (at most 5,000, i.e. 50 requests); an account below that gets rank: null with rankBeyond. Equal balances are ordered by the node and may swap between calls.
  • Harvester history is local. symbol_harvesting_status 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.
  • Harvest income for a year is read in pieces. catapult-rest answers a wide

Installation

Source-derived launch command. Check the maintainer’s required arguments and credentials before running:

bash
npx -y symbol-mcp-server

Set up in your AI client

Merge this template into ~/Library/Application Support/Claude/claude_desktop_config.json. Keep existing servers. Add any arguments, credentials, and permissions required by the maintainer; this template has not been install-tested.

json
{
  "mcpServers": {
    "io-github-inotakeh-symbol": {
      "command": "npx",
      "args": [
        "-y",
        "symbol-mcp-server"
      ]
    }
  }
}

Restart Claude Desktop completely for changes to take effect. Confirm the server appears connected in the client’s tool list, then try a read-only example from its documentation.

Claude Desktop setup reference

Package

symbol-mcp-servernpm

Compatible MCP Clients

io.github.inotakeh/symbol works with any MCP-compatible client. Copy the config snippet from the Configuration section above and add it to the file shown for your client, then restart the application.

  • Claude Desktop~/Library/Application Support/Claude/claude_desktop_config.jsonRestart Claude Desktop completely for changes to take effect.
  • Cursor~/.cursor/mcp.jsonRestart Cursor for changes to take effect.
  • VS Code.vscode/mcp.jsonReload VS Code window for changes to take effect.
  • Windsurf~/.codeium/windsurf/mcp_config.jsonRestart Windsurf for changes to take effect.
  • Claude Code.mcp.jsonSave at the project root, then start Claude Code in that project and review the MCP server approval prompt. Keep real credentials out of shared files.

Learn More