Model Context Protocol server for the Exfer blockchain — gives an AI agent direct, typed access to an exfer-walletd hot wallet
Project description
exfer-mcp
Model Context Protocol server for the Exfer blockchain. Gives an AI agent (Claude Desktop, Claude Code, any MCP-aware host) typed, direct access to an exfer-walletd hot wallet.
Warning — this is a hot wallet. By default there are no spend caps and no human-approval gates: anything that can talk to this MCP server can spend the wallet in full. Recent
exfer-walletdadds operator-configured spend ceilings — setWALLETD_SPEND_CAP_PER_TX(max per spend) and/orWALLETD_SPEND_CAP_PER_PERIOD+WALLETD_SPEND_CAP_PERIOD_SECS(rolling-window total), in exfers. In managed mode the spawned walletd inherits these from the MCP host's environment, so setting them in your host config bounds the blast radius. Until you configure caps (or unless you only keep a small float here), runexfer-mcponly against accounts you would be okay losing in full.
What it exposes
22 tools spanning the full wallet, payment, HTLC, reputation, and EXFER-QUOTE surface.
Wallet & chain (read)
| Tool | What it does |
|---|---|
exfer_generate_address |
Create a new managed address — returns {address, pubkey, index} (use the pubkey as a quote's payee_pubkey) |
exfer_list_addresses |
List the managed addresses (with index / label) |
exfer_get_balance |
Confirmed balance of a managed address |
exfer_get_block_height |
Current chain tip {height, block_id} |
Payments
| Tool | What it does |
|---|---|
exfer_simulate_transfer |
Dry-run a payment — exact fee + inputs, no broadcast |
exfer_transfer |
Build, sign, broadcast a payment (optional on-chain datum) |
exfer_wait_for_tx |
Block until a tx reaches a confirmation depth |
exfer_wait_for_payment |
Block until a payment to an address is seen — sub-second push, no polling |
exfer_payment_uri_encode / _decode |
Build / parse a BIP21-style exfer: URI |
Identity & EXFER-QUOTE
| Tool | What it does |
|---|---|
exfer_sign_message / exfer_verify_message |
Sign / verify an arbitrary message (proof of key control) |
exfer_quote_issue |
Issue a signed EXFER-QUOTE price credential (Spend-posture; signs, moves no money) |
exfer_quote_verify |
Verify a signed EXFER-QUOTE (pure read) |
Conditional payment & reputation
| Tool | What it does |
|---|---|
exfer_htlc_lock / _claim / _reclaim / _status / _list |
Hash-time-locked contracts (atomic swaps) |
exfer_get_address_history |
An address's on-chain activity (indexer-backed) — raw history you interpret, not a trust score |
The reputation / history tools and non-owned HTLC lookups require walletd to be pointed at an
exfer-indexer(it is, by default, in managed mode).
Two ways to run
exfer-mcp talks to an exfer-walletd hot wallet. You can either point it at a walletd you run, or let it spawn and supervise its own — like the browser MCP manages its own headless browser. The mode is selected automatically by whether WALLETD_URL is set:
| External | Managed (zero-setup) | |
|---|---|---|
| Selected when | WALLETD_URL set |
WALLETD_URL unset |
| Who runs walletd | you, separately | exfer-mcp spawns + supervises it |
| Required env | WALLETD_URL + WALLETD_AUTH_TOKEN |
WALLETD_KEYSTORE_PASSPHRASE (+ EXFER_WALLETD_BIN if not on PATH) |
| Node / indexer | whatever your walletd uses | the project's public mainnet node + indexer (overridable) |
| Lifecycle | independent of the MCP | tied to the MCP — spawned on start, killed on exit (no orphans) |
1. External walletd (original behaviour)
You already run exfer-walletd somewhere; exfer-mcp just connects to it. Set WALLETD_URL + WALLETD_AUTH_TOKEN and (for https:// with a self-signed cert) WALLETD_FINGERPRINT. Nothing about this path changed — see Configure (Claude Desktop) below.
// codex / Claude config — EXTERNAL mode
{
"mcpServers": {
"exfer": {
"command": "uvx",
"args": ["exfer-mcp"],
"env": {
"WALLETD_URL": "http://127.0.0.1:7448",
"WALLETD_AUTH_TOKEN": "<paste your walletd token here>"
}
}
}
}
2. Managed (zero-setup)
Leave WALLETD_URL unset. exfer-mcp then spawns its own walletd against the project's public mainnet reference node + indexer and wires the rest of the server to it automatically. You only have to provide a keystore passphrase:
// codex / Claude config — MANAGED mode (zero-setup)
{
"mcpServers": {
"exfer": {
"command": "uvx",
"args": ["exfer-mcp"],
"env": {
// No WALLETD_URL → managed mode.
"WALLETD_KEYSTORE_PASSPHRASE": "<a strong passphrase for the managed wallet>"
}
}
}
}
The walletd binary is obtained automatically. exfer-mcp resolves it in order: EXFER_WALLETD_BIN → exfer-walletd on PATH → otherwise it downloads the prebuilt binary for your platform from the pinned exfer-walletd release, verifies it against that release's SHA256SUMS, and caches it under ~/.cache/exfer-mcp/walletd/<version>/. Since the binary is a hot-wallet daemon, a download is run only after its checksum matches; a release without checksums or a mismatch is refused. Pin a version with EXFER_WALLETD_VERSION, or set EXFER_WALLETD_BIN to a walletd you built yourself to skip the download.
On first run, the managed wallet has no keystore, so exfer-mcp initialises a fresh seeded one and prints the 24-word recovery phrase prominently to stderr (the MCP host surfaces stderr to you). That phrase is the only backup for the funds in this wallet — write it down.
Warning — the managed wallet is a hot wallet. Anything that can reach this MCP server can spend its funds (see Safety). The keystore + scoped bearer tokens live under
WALLETD_DATADIR(default~/.exfer-walletd-mcp) and persist across restarts, so the phrase is shown once — back it up the first time.
Managed mode is lazy / non-blocking — the MCP handshake is instant, so the host (codex / Claude) never appears frozen on startup:
- Picks a loopback bind —
127.0.0.1:7448by default, or a free loopback port if 7448 is busy (so a managed walletd never collides with a walletd you run yourself) — synchronously, so the bind is known immediately. - Answers the MCP handshake +
list_toolsright away (the tool list is static and needs no walletd); walletd is brought up in the background. - In the background: initialises a seeded keystore under
WALLETD_DATADIRif one isn't there yet (surfacing the recovery phrase; reuses an existing keystore otherwise), then spawnsexfer-walletd --datadir … --bind … --node-rpc … --indexer-rpc …with your passphrase in its env, forwarding its logs to the MCP's stderr with a[walletd]prefix (bearer tokens redacted). - The first tool call waits until walletd answers a
get_block_heighthealth probe (typically a few seconds; up to ~30 s), reads the spend-scope bearer token, and caches the client — so the agent sees at most one slightly-slow first call, never a frozen handshake. A ready-timeout returns a clear error, not a hang. - On shutdown (clean exit,
SIGINT, orSIGTERM) terminates walletd —SIGTERM, thenSIGKILLafter a grace period; the first-run init child is torn down too. No orphaned walletd processes.
Install
Recommended — uvx (zero global install)
uvx runs the server in an isolated, on-demand environment. No pip install, no virtual-env management, no PATH wrestling. The host (Claude Desktop / Claude Code / Cursor / …) spawns it on demand and uv handles the rest.
Install uv once:
# macOS / Linux
curl -LsSf https://astral.sh/uv/install.sh | sh
# Windows
powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"
Then wire uvx exfer-mcp into your MCP host config — examples below. The first time the host launches the server, uv fetches the package from PyPI; subsequent runs are cached.
Fallback — pip install
For developer / CI use, or environments where adding uv is awkward:
pip install exfer-mcp # installs the `exfer-mcp` console script
Requires Python ≥ 3.10. Pulls exfer-walletd ≥ 0.8.0 (the JSON-RPC client) and mcp ≥ 1.0 (the MCP server framework).
Configure (Claude Desktop)
Edit ~/Library/Application Support/Claude/claude_desktop_config.json on macOS (or the equivalent path on your OS), then restart Claude Desktop:
{
"mcpServers": {
"exfer": {
"command": "uvx",
"args": ["exfer-mcp"],
"env": {
"WALLETD_URL": "http://127.0.0.1:7448",
"WALLETD_AUTH_TOKEN": "<paste your walletd token here>"
}
}
}
}
The token is whatever exfer-walletd was started with — by default it's written to ~/.exfer-walletd/token on first run (chmod 0600).
If walletd is running with --tls and a self-signed cert, pin its fingerprint:
{
"mcpServers": {
"exfer": {
"command": "uvx",
"args": ["exfer-mcp"],
"env": {
"WALLETD_URL": "https://127.0.0.1:7448",
"WALLETD_AUTH_TOKEN": "<token>",
"WALLETD_FINGERPRINT": "sha256:<paste from cert.fingerprint>"
}
}
}
}
For a publicly-fronted walletd (e.g. behind a reverse proxy with a CA-signed cert), drop the WALLETD_FINGERPRINT field — the SDK falls back to the system CA chain.
Pin a specific version
"args": ["exfer-mcp@0.1.0"]
Configure (Claude Code)
One-shot via claude mcp add:
claude mcp add exfer \
-e WALLETD_URL=http://127.0.0.1:7448 \
-e WALLETD_AUTH_TOKEN=<token> \
-- uvx exfer-mcp
Or by editing the project / global Claude Code MCP config directly:
{
"mcpServers": {
"exfer": {
"command": "uvx",
"args": ["exfer-mcp"],
"env": {
"WALLETD_URL": "http://127.0.0.1:7448",
"WALLETD_AUTH_TOKEN": "<token>"
}
}
}
}
Configure (other MCP hosts)
Cursor, Cline, Continue.dev, and most other MCP-aware hosts accept the same command / args / env shape. Use the uvx exfer-mcp invocation above.
Environment
WALLETD_URL is the mode switch. Set it → External mode (connect to your walletd). Leave it unset → Managed mode (exfer-mcp spawns its own walletd against the public mainnet defaults below).
External mode
| Variable | Required | Default | Meaning |
|---|---|---|---|
WALLETD_URL |
yes (set → external) | — | walletd base URL |
WALLETD_AUTH_TOKEN |
yes | — | walletd bearer token |
WALLETD_FINGERPRINT |
only for https:// with a self-signed cert |
— | SHA-256 of walletd's TLS cert (sha256:<hex>) |
Managed mode (WALLETD_URL unset)
| Variable | Required | Default | Meaning |
|---|---|---|---|
WALLETD_KEYSTORE_PASSPHRASE |
yes | — | Passphrase to unlock (and, on first run, create) the managed wallet keystore. Required in managed mode. |
EXFER_WALLETD_BIN |
only if exfer-walletd isn't on PATH |
auto-detect exfer-walletd on PATH |
Full path to the walletd binary |
EXFER_NODE_RPC |
http://64.176.231.198:9334,http://89.127.232.155:9334 |
Upstream Exfer node(s) — the project's public mainnet reference node + a backup, comma-separated. walletd round-robins and fails over across them. | |
EXFER_INDEXER_RPC |
http://64.176.231.198:9335 |
The project's public mainnet indexer, for observability queries that need data outside this wallet's own keys. Set to an empty string to disable indexer delegation. | |
WALLETD_DATADIR |
~/.exfer-walletd-mcp |
Where the managed keystore + bearer tokens live. Persists across restarts; reused if present. | |
EXFER_WALLETD_BIND |
127.0.0.1:7448 |
Preferred loopback host:port. If the port is busy, exfer-mcp picks a free loopback port instead so it never collides with a walletd you run yourself. |
Common to both modes
| Variable | Required | Default | Meaning |
|---|---|---|---|
EXFER_MCP_DEFAULT_FEE_RATE |
walletd default | fee_rate (exfers/byte) for spends when the agent didn't specify | |
EXFER_MCP_HTTPX_TIMEOUT |
30 | per-RPC timeout in seconds |
Recommended agent flow
When the user asks the agent to send a payment, the expected sequence is:
exfer_simulate_transfer→ compute exact fee- Show the user the fee and ask for confirmation
exfer_transfer→ broadcastexfer_wait_for_tx→ confirm
The simulate-first pattern means the agent always knows the cost before committing. The user is the one who decides whether the cost is acceptable.
Safety
WALLETD_AUTH_TOKENis all-or-nothing access to the wallet. Treat it like a payment-card number.exfer-mcpdoes no per-call confirmation by itself — that's the host's job. For spend caps, configure them on the walletd side with--spend-cap-per-tx/--spend-cap-per-period(or theWALLETD_SPEND_CAP_*env vars; the managed walletd inherits them from this process's environment). A spend that would breach a cap is refused before broadcast withAllowanceExceeded(-32038). Caps default to unlimited, so also consider running a walletd that only holds a small float you would be comfortable losing.- The MCP transport is stdio. The agent does not see the wire token; only this process does.
- Errors from walletd surface as MCP
isError=truecontent the agent reads and reacts to, including specific cases likeInsufficientBalanceError(over-spend) andWaitTimeoutError(confirmation depth not reached in time). - Managed mode is also a hot wallet. The walletd exfer-mcp spawns binds loopback only and its bearer tokens never leave the local machine — exfer-mcp also redacts any 64-hex token from walletd's forwarded
[walletd]log lines, so the first-run spend token never lands in the host's stderr log file. Anything that can reach the MCP server can still spend its funds. The 24-word recovery phrase is printed to stderr once, on first run (BACK UP THIS RECOVERY PHRASE) — it is the only backup for that wallet, so write it down then. TreatWALLETD_KEYSTORE_PASSPHRASEand the contents ofWALLETD_DATADIR(default~/.exfer-walletd-mcp) as wallet secrets, and keep only a float you'd be comfortable losing in the managed wallet.
Coming soon
.mcpbdesktop bundle — Anthropic's one-click.mcpbinstall format for Claude Desktop, with the env vars surfaced as a form at install time.- PyPI release — a
uvx exfer-mcp/pip install exfer-mcpone-liner, once theexfer-walletdclient is published to PyPI. - MCP directory — submission to Anthropic's curated MCP directory + community directories.
Development
git clone https://github.com/exfer-stack/exfer-mcp
cd exfer-mcp
uv venv && source .venv/bin/activate
uv pip install -e '.[dev]'
pytest # unit tests
mypy && ruff check # lint
python -m scripts.e2e_smoke # end-to-end smoke (needs a live walletd)
See scripts/e2e_smoke.py for a runnable example that exercises the full Exfer stack — walletd, indexer, MCP — against a real deployment.
License
MIT
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file exfer_mcp-0.2.0.tar.gz.
File metadata
- Download URL: exfer_mcp-0.2.0.tar.gz
- Upload date:
- Size: 50.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
845ee16874f7ba533caabdcdcf81afc23823ce12e2b82610438f9624b1a824c6
|
|
| MD5 |
9d6490b5c3e4a645e57c2edd096a189d
|
|
| BLAKE2b-256 |
ada272cea8428ccfaa1bcddad57113eb3cb1655c993097d8f5e25f12ab0c3a08
|
Provenance
The following attestation bundles were made for exfer_mcp-0.2.0.tar.gz:
Publisher:
release.yml on exfer-stack/exfer-mcp
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
exfer_mcp-0.2.0.tar.gz -
Subject digest:
845ee16874f7ba533caabdcdcf81afc23823ce12e2b82610438f9624b1a824c6 - Sigstore transparency entry: 1777526802
- Sigstore integration time:
-
Permalink:
exfer-stack/exfer-mcp@008e3345b4206168121e825575c4e73772d27e83 -
Branch / Tag:
refs/tags/v0.2.0 - Owner: https://github.com/exfer-stack
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@008e3345b4206168121e825575c4e73772d27e83 -
Trigger Event:
push
-
Statement type:
File details
Details for the file exfer_mcp-0.2.0-py3-none-any.whl.
File metadata
- Download URL: exfer_mcp-0.2.0-py3-none-any.whl
- Upload date:
- Size: 45.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
e2867adb3eaac0b2c0df806cd5ca376694870c1fac4aa02a73b10fc8c6e7bbfe
|
|
| MD5 |
3b5bc50fe8a776772f57c02ca46770ca
|
|
| BLAKE2b-256 |
26811d396e1ec2a205971c653f4fb7d9524704b449bc205a301976c03cf93902
|
Provenance
The following attestation bundles were made for exfer_mcp-0.2.0-py3-none-any.whl:
Publisher:
release.yml on exfer-stack/exfer-mcp
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
exfer_mcp-0.2.0-py3-none-any.whl -
Subject digest:
e2867adb3eaac0b2c0df806cd5ca376694870c1fac4aa02a73b10fc8c6e7bbfe - Sigstore transparency entry: 1777528973
- Sigstore integration time:
-
Permalink:
exfer-stack/exfer-mcp@008e3345b4206168121e825575c4e73772d27e83 -
Branch / Tag:
refs/tags/v0.2.0 - Owner: https://github.com/exfer-stack
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@008e3345b4206168121e825575c4e73772d27e83 -
Trigger Event:
push
-
Statement type: