hydra-ops-mcp
Provides tools for operating a Hydra head on the Cardano blockchain, including lifecycle management (init, commit, decommit, close, fanout), in-head transactions, L1 wallet queries, and diagnosis of node logs and error codes.
Click on "Install Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@hydra-ops-mcpWhat state is the head in, and what does alice hold on L1?"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
hydra-ops-mcp
Spin up a hydra-node, form a head with peers, and operate it — by talking
to it. An MCP server that exposes the whole journey as tools an LLM client
can call: generate keys, build the head's ledger parameters, exchange peer
info with a counterparty, start your node, then drive the head — deposits,
in-head transactions, decommit, close, fanout, selective partial
fanout, deposit recovery — plus read-only views of head state and the L1,
node logs, and on-chain error decoding.
Every operation that changes state describes what it would do and waits for your explicit confirmation before doing it.
v2 adds node provisioning, pluggable L1 access (Blockfrost / local cardano-cli / the devnet container), network configurability (devnet/preview/preprod/mainnet), and timing-safe lifecycle semantics for real contestation periods. The devnet remains the default and the zero-cost regression bed.
Contents
Related MCP server: mcp-cli-catalog
Why
Operating a head means holding several tools at once. The TUI shows you head
state but not why a transaction was rejected. The WebSocket API gives you
events but you're parsing JSON by hand. When something goes wrong the answer
is usually in docker compose logs, correlated against head state, and
decoded against error codes that live in the Plutus source.
This server puts all of that behind one conversational interface:
"The head won't fan out. What's wrong?"
Claude can check head state, pull the failing transaction from the node logs,
decode the H39 abort code to FanoutUTxOHashMismatch, and tell you the two
things that actually cause it — in one turn, because it has the head API, the
container logs, and the error tables all in reach.
It's also useful for the routine parts: opening and funding a head, moving
funds, and settling out, with each step explained and confirmed before it
runs. And unlike a TUI session bound to a single node, every tool takes a
node argument, so you can compare what alice, bob and carol each believe
about the same head.
Currently targets the hydra demo devnet (three nodes, three parties). The API layer is not devnet-specific; the L1 helpers and key handling are (see Limitations).
Quick start
Prerequisites — Docker, Python 3.10+, and a checkout of cardano-scaling/hydra (for the demo devnet and the Plutus error tables).
git clone https://github.com/skoniog/hydra-ops-mcp && cd hydra-ops-mcp
python3 -m venv .venv # or: uv venv .venv
.venv/bin/pip install -r requirements.txt
./reset_devnet.sh # cardano-node + 3 hydra-nodes, seededRegister the server with your MCP client. Claude Code:
claude mcp add hydra-ops -- /absolute/path/to/hydra-ops-mcp/.venv/bin/python \
/absolute/path/to/hydra-ops-mcp/server.pyClaude Desktop — add to claude_desktop_config.json:
{
"mcpServers": {
"hydra-ops": {
"command": "/absolute/path/to/hydra-ops-mcp/.venv/bin/python",
"args": ["/absolute/path/to/hydra-ops-mcp/server.py"]
}
}
}MCP launchers spawn the server with a stripped environment, so pass any
overrides (HYDRA_DEMO_DIR, HYDRA_REPO) in an "env" block rather than
exporting them in your shell.
Then ask:
"What state is the head in, and what does alice hold on L1?" "Open a head and commit alice's funds." "Send 5 ADA from alice to bob, then show me the head UTXO set."
New to operating a head this way? RUNBOOK.md walks the whole lifecycle — open, fund, transact, decommit, close, settle, and break it on purpose — as a series of guided sessions.
Architecture
MCP client (Claude Code / Claude Desktop / anything speaking MCP)
│ stdio
▼
server.py FastMCP registration; thin wrappers only
│
tools/ one module per domain, plain functions
├── observe.py head state, UTXOs, L1 funds, params, events
├── lifecycle.py init, commit, decommit, close, fanout, recover
├── transact.py in-head transfers
├── diagnose.py node logs, error-code decoding
└── types.py ok() / err() / needs_confirmation()
│
├──▶ hydra_client.py WebSocket + HTTP to hydra-node
│ async core, sync facade, event buffer
├──▶ tx_builder.py PyCardano: build + sign in-head txs
├──▶ cardano.py cardano-cli in the node container (L1)
└──▶ errors.py parses hydra-plutus for abort codeshydra_client.py holds a WebSocket connection per node, running an async
event loop on a daemon thread behind a synchronous facade — so tool functions
stay simple while still awaiting protocol events. It buffers every server
output for recent_events, tracks head status, and correlates confirmed
transactions. Commands wait for their specific outcome event
(Decommit → DecommitFinalized, Fanout → HeadIsFinalized) rather than
returning optimistically, so a tool call that succeeds means the protocol
step actually completed.
tx_builder.py builds and signs transactions with PyCardano — no
cardano-cli round-trip per transaction. cardano.py handles the L1 side
(address derivation, UTXO queries, signing and submitting deposit
transactions) by exec'ing cardano-cli inside the running cardano-node
container, which is also where the keys live.
errors.py parses HeadError.hs, DepositError.hs, HeadTokensError.hs
and friends out of your local hydra checkout at call time, so decoded codes
always match the version you're running rather than a table that drifts.
The confirmation model
Every tool that changes state takes confirm: bool = False. Called without
it, the tool validates everything it can, resolves what it would actually do,
and returns a description — having changed nothing:
{
"status": "requires_confirmation",
"action": "deposit alice's UTXO 4a3f…#0 (100,000,000,000 lovelace) into the head via node 1",
"message": "This would deposit… Nothing has been done. Retry with confirm=True to execute.",
"party": "alice", "utxo_ref": "4a3f…#0", "lovelace": 100000000000
}In practice this means Claude proposes, you approve, and only then does
anything happen on-chain. It matters most for the operations that are
unilateral and irreversible: close_head affects every participant in the
head, and fanout settles the head's final state.
The preview is resolved, not hypothetical — commit_funds names the exact
UTXO it selected, decommit names the owner and amount it derived from the
head's UTXO set, send_tx reports the transaction id it built. Validation
runs before the gate, so you're never asked to confirm something that would
have failed anyway. Read-only tools have no gate and run immediately.
Tool reference
All tools return {status, error, ...}; failures are
{"status": "error", "error": "<message>", ...} rather than exceptions. Every
tool accepts node: int = 1 (1 = alice, 2 = bob, 3 = carol) except
l1_funds and explain_error.
Observability (read-only)
Tool | Signature | Returns |
|
| Head tag, WS-observed status, UTXO count, total lovelace, snapshot number, head version, contestation deadline |
|
| The head's UTxO set grouped by address, each with ref and value |
|
| A party's L1 address, UTXO count, total lovelace, and per-UTXO values |
|
| The head's ledger parameters — full set plus a summary of the ones that bite (fees, min-UTXO, sizes) |
|
| Deposits observed but not yet absorbed — the recovery candidates |
|
| Server outputs seen on this connection, optionally filtered by tag |
recent_events covers events since the server connected — the WS connection
requests no history, so it's a live tail rather than the full log. For
anything older, use node_logs.
Lifecycle (confirmation-gated)
Tool | Signature | Notes |
|
| Refuses unless the head is |
|
| Drafts the deposit via |
|
| Withdraws one head UTxO to L1 with the head still open. Derives the owner from the UTxO's address and builds a full-value self-transfer as the decommit tx |
|
| Posts the latest confirmed snapshot and starts the contestation period. Affects all participants |
|
| Waits for |
|
| Settles a chosen subset; reports what was distributed and what remains. See Limitations — needs a node newer than 2.3.0 |
|
|
|
commit_funds deliberately deposits a single UTXO per call: multi-UTXO
deposits are what wedge fanout with H39 on 2.3.0
(see Operational notes).
Transactions (confirmation-gated)
Tool | Signature | Notes |
|
| In-head transfer. |
Amounts below 1 ADA are refused. The head zeroes min-UTXO, so such an output is valid on L2 and then impossible to recreate on L1 — it would wedge fanout permanently. The transaction is rebuilt against the current UTxO set at confirmation time, so a preview that sat around doesn't spend stale inputs. The call returns once the transaction appears in a confirmed snapshot, not merely when it's accepted.
Diagnosis (read-only)
Tool | Signature | Notes |
|
| Container logs, optionally regex-filtered. Returns how many lines matched and the last |
|
| Decodes an abort code ( |
Compared with hydra-tui
The tool surface deliberately matches what hydra-tui exposes, so anything
you can do in the TUI you can do here:
hydra-tui | here |
|
|
commit dialog |
|
|
|
|
|
|
|
|
|
|
|
|
|
main tab |
|
funds tab |
|
event history tab |
|
— |
|
Like the TUI, this doesn't expose Contest, SafeClose or
SideLoadSnapshot. Those are protocol responses to specific on-chain
conditions where one action is correct and timing is load-bearing; they
belong in deterministic tooling with alerting, not behind a prompt.
Where this goes further:
Diagnosis.
node_logsandexplain_errorhave no TUI equivalent. This is the biggest practical gain — a wedged head goes from "the TUI says it failed" to a decoded abort code and the matching log lines.Cross-node. A TUI session attaches to one node. Here every tool takes
node, so you can ask what alice, bob and carol each believe about the same head — the fastest way to spot a node that has fallen behind.L1 and L2 together.
l1_fundsqueries the chain directly, so "did that decommit actually land?" is one question rather than a context switch tocardano-cli.Guardrails. Sub-min-UTXO outputs and multi-UTXO deposits are refused by construction, because both silently wedge fanout later.
Composition. Multi-step operations happen in one request: "close the head, wait out contestation, fan out, and show me everyone's final L1 balances" is a single ask.
Where the TUI still wins: it's a live dashboard. MCP is request/response, so you get snapshots rather than a continuously updating view — for watching a head over time, keep the TUI open. Keystrokes also beat a model round-trip for repetitive work, and the TUI's UTxO pickers are visual where here you list then select.
Configuration
Everything is in config.py, with environment overrides:
Setting | Default | Meaning |
|
| Node index → WS/HTTP endpoints and party name |
|
| Demo devnet: docker compose project and credentials |
|
| Hydra checkout, for decoding abort codes |
|
| Devnet magic |
|
| Refusal threshold for in-head outputs |
Signing keys are the demo's {alice,bob,carol}-funds pairs. Container-side
paths are used for cardano-cli (signing and submitting on L1); host-side
copies of the same keys are read by PyCardano for in-head transactions.
Pointing at a different deployment with the same layout is a config change;
pointing at a different topology is not (see Limitations).
Testing
.venv/bin/python test_ops.py # offline — no devnet needed
.venv/bin/python test_ops_devnet.py # live — needs a devnet with the head Idletest_ops.py asserts that every state-changing tool returns
requires_confirmation and reaches no client without confirm=True (the
stub client raises if a command escapes the gate), that request payloads match
the API, that the min-UTXO refusal and UTxO validation fire, that the error
table parses and decodes, and that all 16 tools register with the server.
test_ops_devnet.py drives a real head through the whole lifecycle and
asserts observability at each stage: gate check → init → commit →
six read tools → two in-head payments → decommit, verified by the funds
appearing on L1 while the head stays open → close → fanout → back to
Idle → logs and error decoding. It skips with a clear message if the devnet
isn't up or the head isn't Idle.
Operational notes
Things worth knowing before they cost you a head.
H39 / FanoutUTxOHashMismatch wedges a head permanently. Fanout can't
reproduce what the closed head committed to, so the head cannot settle and its
funds are stuck. Two causes, both preventable and both guarded against here:
multi-UTXO deposits on 2.3.0, and any head output below the L1 min-UTXO. Ask
explain_error("H39") for the details.
The head zeroes min-UTXO; L1 does not. A 0.5 ADA output transacts happily
on L2 and then cannot be recreated on L1. send_tx refuses below 1 ADA for
this reason.
Deposits are absorbed after a deposit period, not instantly.
commit_funds waits and reports if absorption doesn't happen; a deposit that
never lands shows up in pending_deposits and comes back with
recover_deposit.
Close is unilateral and affects everyone. Any participant can close, and the whole head must then settle. The gate exists mostly for this.
A head needs every participant online. If a payment hangs, check
docker compose ps before suspecting the tooling.
The demo devnet's block producer can stall after long idle periods —
cardano-cli query tip returns the same slot twice and everything hangs.
./reset_devnet.sh fixes it; the devnet is disposable by design.
Unparseable WebSocket input returns no tag. A command a node doesn't
recognize comes back as a bare {"input", "reason"} object rather than a
tagged event — worth knowing if you script against the API directly, since a
client waiting on tagged events will hang. The client here handles it.
Provisioning a node (v2)
The path from nothing to a running participant, all as tool calls:
generate_keys("alice") fuel + funds Cardano pairs, Hydra keys
└─ fund the fuel address ~30 ada — the one step no tool can do
build_protocol_parameters() live network params, only fees zeroed
share_peer_info("alice", host) → send to your counterparty
node_plan(...peers...) preview the exact container command
start_node(...) run it; node_health() confirms peeredNodes run as containers of HYDRA_NODE_IMAGE (default: the 2.3.0 release)
on a shared docker network, so same-host nodes reach each other by container
name; remote peers use published ports and a reachable advertise_host.
Verified live: two parties provisioned from scratch on the devnet with zero
funds, mutually peered (hydra_head_peers_connected=1), and — funded from
the devnet faucet — a full head lifecycle through selective partial
fanout on a master-built (unstable) node.
Select the axes with environment variables: HYDRA_OPS_NETWORK
(devnet/preview/preprod/mainnet), HYDRA_OPS_PROVIDER
(docker/cli/blockfrost), HYDRA_OPS_WORKSPACE (keys, configs, persistence),
HYDRA_NODE_IMAGE, HYDRA_SCRIPTS_TX_ID (overrides published script ids —
required for unstable builds, whose validators differ from every release).
Limitations
partial_fanout needs a node newer than 2.3.0 — the command postdates
the latest release (hydra PR #2750). Against release images the tool reports
the version gap precisely; against HYDRA_NODE_IMAGE=…:unstable the full
selective-drain flow is verified working (see Provisioning above), which
also requires publishing that build's own scripts.
Verified on preview, end to end, over Blockfrost — no cardano-node
anywhere: two parties provisioned from scratch, faucet-funded, both nodes on
the --blockfrost backend, then init → 10,000 tADA deposit → in-head
payments (0.2 s finality) → decommit (funds verified back on L1 with the
head open) → close (~3 min, the automatic close-retry fired and recovered a
dropped Close) → 60 s contestation → fanout (~3 min) → final L1 balances
exact to the lovelace.
Real-network operating notes learned from that run:
--unsynced-perioddefaults to CP/2, which on a short testnet CP (60 s) sits below preview's real inter-block gaps — the node then randomly rejects inputs withRejectedInputBecauseUnsynced. SetHYDRA_OPS_UNSYNCED_PERIOD(e.g. 600) for short-CP testing; on proper CPs the default is fine.A 60 s deposit period is too tight on preview — the 3×DP deadline expires before absorption can settle and the deposit lapses (
DepositExpired;recover_depositgets the funds back, verified live). DP=180 absorbed reliably in ~6 min.Right after absorption there is a brief settling window where spending the deposited UTXO is rejected with "all inputs are spent"; retry moments later succeeds.
L1-touching operations take minutes, not seconds: budget ~2–4 min each for close, decommit settlement, and fanout at preview block times.
Devnet keys for alice/bob/carol come from the demo directory; provisioned
parties keep their keys in the workspace, chmod 600, and tools never return
secret material. There is still no authentication on the MCP surface itself.
ADA only. Transaction building handles pure-lovelace UTXOs — no native tokens, scripts, datums, or minting.
Hard-won signing rule: a node-built draft (deposits) must never be
re-serialized before witnessing — neither PyCardano nor cbor2 round-trips
them byte-exactly, and a re-encoded body means the signature is over the
wrong hash (InvalidWitnessesUTXOW). tx_builder.sign_envelope locates the
body's exact byte span with a CBOR scanner and signs that.
Master's deposit timing differs: unstable builds add
--deposit-activation (default 3600s) governing when a deposit becomes
absorbable, independent of --deposit-period. Without setting it, deposits
sit inactive for an hour. node_plan passes it automatically when the image
supports it.
recover_deposit is untested against a genuinely stuck deposit. It
follows the API, but the demo devnet absorbs deposits too reliably to produce
one on demand.
No auth. Anyone who can reach the server can operate the head. That's appropriate for a local operator tool and would not be for anything exposed.
Extending
Adding a tool: write a plain function in the relevant tools/ module
returning ok() / err() / needs_confirmation(), then register a thin
wrapper in server.py. Tool modules don't import FastMCP, so they're directly
callable from tests — which is how both suites drive them.
Adding a protocol command: add a method to HydraClient using
_command_and_wait(command, ok_tags), which sends and waits for the outcome
event while treating both CommandFailed and untagged parse rejections as
errors.
Targeting another deployment: point NODES at the endpoints and
HYDRA_DEMO_DIR / HYDRA_REPO at the right paths. Anything beyond the demo's
three-party layout means revisiting key handling in cardano.py and
tx_builder.py, and fees.
Further reading
The Hydra documentation for the protocol itself, and RUNBOOK.md for the guided tour of operating a head with these tools.
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections. Dates show when Glama detected each change.
No tool schema history has been recorded yet.
This server cannot be installed
Maintenance
Resources
Unclaimed servers have limited discoverability.
Looking for Admin?
If you are the server author, to access and configure the admin panel.
Related MCP Connectors
Remote MCP server for supportsheep: run AI interviews and manage support content for your blog.
Nifty's MCP server — exposes tasks, projects, messages, and files as tools for AI agents.
MCP server exposing the Backtest360 engine API as tools for AI agents.
An MCP server that provides an API to LLMs to manage their JumpCloud resources.
Related MCP Servers
- MIT
- AlicenseNot gradedqualityDmaintenanceAn MCP server that publishes CLI tools on your machine for discoverability by LLMs71MIT
- AlicenseNot gradedqualityCmaintenanceMCP server for Cardano blockchain data — exposes on-chain queries, address lookups, transaction history, token metadata, stake pool info, and network parameters to LLM agents.874MIT
- FlicenseAqualityCmaintenanceMCP server exposing tools for WORM chain sealing, sovereign agent creation, Ada governance contract generation, and SSM state injection from Lean theorems.64-
Latest Blog Posts
- Who's Calling? MCP Hosts Are an Identity Blind Spot (And the Spec Knows It)By Om-Shree-0709 on .mcpAgent IdentityOAuth 2.1
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
MCP directory API
We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/skoniog/hydra-ops-mcp'
If you have feedback or need assistance with the MCP directory API, please join our Discord server