Each page opens with its audience (the operator). The per-parameter tool listings, which repeat the tools' own MCP descriptions, become short summaries of the behaviour an operator needs: the completion todo, what status clears, the process-group kill, the unread guard, how invites resolve. The wiring and architecture sections are unchanged.
8.8 KiB
Matrix MCP tools and extra MCP servers
For the operator: this page covers how the harness adds the matrix tools and what they do around the edges. How an agent calls them is in the tools' own MCP descriptions.
Built-in matrix MCP (mcp__matrix__*)
When the agent has at least one services.hyperhive.agent.matrixAccounts
entry, the harness autoinjects hive-matrix-daemon's
streamable-http endpoint as a second MCP server (no stdio bridge —
see Architecture below). No matrix.enable switch exists: the account
set is the switch, and the module declares the hive-internal main
account for you as soon as services.hyperhive.agent.matrix.url is
non-null — which hive-c0re fills in per agent whenever the operator
configures the host-level matrix tuwunel. Tools land as mcp__matrix__<name>.
Each tool's parameters and return values are in its own MCP description, which is what an agent reads at call time. They fall into four groups:
- Messaging:
send_message,send_dm,send_reply,send_reaction,send_file,send_redactandopen_dm. A room is!id:serveror#alias:server, and the daemon resolves either.open_dmfinds or creates the DM room and returns its id without sending anything, for a case wheresend_dm's body is inconvenient, such as a file attachment.send_filecaps an upload at 50 MiB. Redacting someone else's event needs moderator power. - Reading:
read_room,download_file,list_roomsandlist_room_members.read_roomshows media as[file: …],[image: …],[audio: …]or[video: …]markers, and the event id of one goes todownload_file. - Room membership:
invite_user,list_invites,resolve_inviteandjoin_room.resolve_inviteaccepts or rejects a pending invite and is the path for invites;join_roomis for a public room the agent wasn't invited to.list_invitesgives the inviter and the topic, so an agent can judge an invite before it answers. - Receipts:
mark_readadvances the read receipt.
Unread guard. send_message, send_dm, send_file and send_reply fail
while the room has unread messages. The agent calls read_room and then
mark_read on the latest event before sending to a room it hasn't read.
Multiple accounts
services.hyperhive.agent.matrixAccounts (declared in agent.nix) is
the agent's full set of matrix identities — for example an
external-facing account alongside the internal one — and, being
non-empty, is what turns matrix on for the agent at all. Each entry is
keyed by account name and specifies tokenFile (bearer token,
provisioned out-of-band; basename must start with matrix-token),
sessionDir (per-account matrix-sdk sqlite state — crypto keys +
cache), and an optional homeserver (defaults to
services.hyperhive.agent.matrix.url). The hive-internal account is
always named main and is always the primary; the matrix module
declares it for you as an ordinary entry of this map, from
services.hyperhive.agent.matrix.url + agent state, so what you add
here are the further accounts. Its token comes from the swarm secret
store (swarm-controller mints it); its tokenFile stays pinned to
<state>/matrix-token as the fallback, and the dashboard's link-account
route refuses to create an account by that name.
main is present exactly when that URL is non-null, which is the whole
mechanism behind "accounts are the enable signal": an agent with no
homeserver and no account of its own has an empty set, so it gets no
daemon, no path watcher and no injected MCP entry. That's how you give
an agent no matrix tools. Declaring an external account with
its own homeserver is enough on its own; a hive homeserver isn't
required.
An account linked from the swarm UI needs no entry in this map.
swarm-controller stores it at swarm/agents/<agent>/matrix/<account>.
The daemon lists that directory under the agent's own certificate at
start and every two minutes, and restarts itself when the set of linked
accounts changes. A declared entry of the same name wins.
Every matrix tool above takes an optional account parameter (a name
from this map, or a linked account's) to act as that identity instead
of the primary one.
Declaring an extra account also changes what the agent receives: the
summaries of unread-room and invite todos gain an [acct:<name>] prefix — see
Architecture below.
Architecture
hive-matrix-daemon is a single long-running process (one per agent
container, systemd service in nix/agent-modules/matrix.nix) — no
stdio bridge, no separate bin. It owns the matrix-sdk Client + sync
loop per configured account and serves the matrix tool surface
directly over streamable-http on services.hyperhive.agent.mcp.matrixHttpPort
(declared in services.hyperhive.agent.extraMcpServers.matrix as
{ type = "http"; url = ...; }). Same shape as hive-bash-daemon and
the built-in hyperhive surface (hive-mcp-http) — claude reconnects
to the stable URL every turn instead of respawning a stdio child.
Silently exits when the primary account has no token yet; the
systemd.paths.hive-matrix-daemon watcher restarts it when a token file
appears, and, on an agent with a store, a timer restarts it while it's
down so it re-reads a token the swarm minted into the store.
Wake path: after every sync the daemon upserts a harness-local todo
on the in-agent socket for each unread room (keyed by its room id) and
each pending invite (below). A new or changed todo wakes the agent with
the generic prompt you have todos — call get_loose_ends to see them
(from todo). The [matrix] lines reach the agent through
get_loose_ends, one per room, as
- todo #<id> [matrix <room_id>, <age>s old]: <summary> (cancel_loose_end …).
The summary depends on the room's unread state:
- One unread message:
[matrix] <sender> in <room>: <body> — use read_room to view, mark_read to clear - Several unread messages:
[matrix] N unread in <room> — use read_room to view, mark_read to clear
Multi-account prefix: when the daemon serves more than one account
(services.hyperhive.agent.matrixAccounts), it prefixes the summary of every
unread-room todo above and every invite todo below with [acct:<name>] , so
get_loose_ends shows
- todo #<id> [matrix <room_id>, …]: [acct:ccc] [matrix] <sender> in <room>: …. A single-account agent gets the
formats exactly as written — the daemon adds nothing — which is why matching
on a leading [matrix] works right up until you declare a second account.
hive-matrix-daemon includes the same per-room breakdown in the UnreadMatrix
entry get_loose_ends returns, so unread rooms surface in the
loose-ends list between turns.
Invite wakes: the daemon sweeps invited_rooms() after every sync
callback (deliberately not a one-shot m.room.member event handler —
a one-shot signal that raced a socket-down window vanished with no
retry, leaving the agent deaf until manually prompted) and upserts a
todo (keyed invite:<room>) for each pending invite on the harness's
in-agent socket, which drives a turn. The daemon does not
autojoin — the agent calls list_invites to see pending invites and
resolve_invite to accept or reject them.
Pending invites as loose ends: the daemon upserts pending invites as
keyed todos, which appear in get_loose_ends output as
[matrix] invited to <room> (<room_id>) — use list_invites to see pending invites, resolve_invite to accept or reject. The daemon clears
the keyed todo when a resolve_invite (or join_room) call resolves
the invite.
See docs/integrations/matrix.md for the homeserver setup,
provisioning flow, and federation config.
Extra MCP servers (per-agent)
Each agent's NixOS config can declare additional MCP servers via
services.hyperhive.agent.extraMcpServers.<key> = { type, command, args, env, url, allowedTools } — type = "stdio" (the default, uses command/args/
env) or type = "http" (uses url, a long-lived streamable-http
endpoint — see hive-bash-daemon and hive-matrix-daemon above for
the "http" shape). The module writes the map to
/etc/hyperhive/extra-mcp.json; the harness reads it at boot and
merges every entry into --mcp-config (under mcpServers.<key>)
and --allowedTools (as mcp__<key>__<pattern>).
The agent's flake.nix forwards every flake input to agent.nix as
the flakeInputs module arg, so you pull in external MCP-server flakes
by adding them to inputs.* and reference them as
flakeInputs.<name>.packages.${pkgs.system}.default — the resolved
sha lands in the agent's own flake.lock and rolls up to internal/meta's.
allowedTools defaults to ["*"], which expands to
mcp__<key>__* (every tool from that server autoapproved). Restrict
to specific tool names when you want finer control.