# 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__`. 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_redact` and `open_dm`. A room is `!id:server` or `#alias:server`, and the daemon resolves either. `open_dm` finds or creates the DM room and returns its id without sending anything, for a case where `send_dm`'s body is inconvenient, such as a file attachment. `send_file` caps an upload at 50 MiB. Redacting someone else's event needs moderator power. - **Reading:** `read_room`, `download_file`, `list_rooms` and `list_room_members`. `read_room` shows media as `[file: …]`, `[image: …]`, `[audio: …]` or `[video: …]` markers, and the event id of one goes to `download_file`. - **Room membership:** `invite_user`, `list_invites`, `resolve_invite` and `join_room`. `resolve_invite` accepts or rejects a pending invite and is the path for invites; `join_room` is for a public room the agent wasn't invited to. `list_invites` gives the inviter and the topic, so an agent can judge an invite before it answers. - **Receipts:** `mark_read` advances 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 `/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//matrix/`. 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:]` prefix — see [Architecture](#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 # [matrix , s old]: (cancel_loose_end …)`. The summary depends on the room's unread state: - **One unread message**: `[matrix] in : — use read_room to view, mark_read to clear` - **Multiple unread messages**: `[matrix] N unread in — 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:] `, so `get_loose_ends` shows `- todo # [matrix , …]: [acct:ccc] [matrix] in : …`. 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:`) 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 () — 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`](../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. = { 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.`) and `--allowedTools` (as `mcp____`). 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..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____*` (every tool from that server autoapproved). Restrict to specific tool names when you want finer control.