# Matrix MCP tools and extra MCP servers ## Built-in matrix MCP (`mcp__matrix__*`) When `hyperhive.matrix.enable = true` and the host-level matrix tuwunel is configured, the harness auto-injects `hive-matrix-daemon`'s streamable-http endpoint as a second MCP server (no stdio bridge — see Architecture below). Tools land as `mcp__matrix__`: ### Messaging **Unread guard**: `send_message`, `send_dm`, `send_file`, and `send_reply` are all rejected if the room has unread messages — call `read_room` then `mark_read` on the latest event before sending to a room you haven't read yet. - `send_message(room, body)` — send a markdown message; `room` accepts `!id:server` or `#alias:server`, daemon resolves either - `send_dm(user_id, body)` — open (or reuse) the DM room with `user_id` and post `body` to it - `open_dm(user_id)` — resolve (find-or-create) the DM room with `user_id` and return its room id **without sending anything**. Use the returned id with room-based tools (`send_message`, `send_file`, …) to deliver into a DM when `send_dm`'s body parameter is inconvenient (e.g. for file attachments) - `send_reply(room, event_id, body)` — threaded reply to a specific event - `send_reaction(room, event_id, key)` — react to a message with an emoji key - `send_file(room, path, caption?)` — upload a local file and post it as a room attachment; MIME type is inferred from the file extension, 50 MiB cap; optional `caption` is sent as a follow-up text message - `send_redact(room, event_id, reason?)` — redact (delete) a specific event; works on your own events, redacting others' requires moderator power level ### Reading - `read_room(room, limit?)` — recent timeline events; media attachments appear inline as `[file: name]`, `[image: name]`, `[audio: name]`, or `[video: name]` markers — pass the event id to `download_file` to retrieve the attachment - `download_file(room, event_id, dest_path?)` — download a media attachment from a message event to a local file and return its path; pair with `read_room` which surfaces attachments as the markers above - `list_rooms()` — enumerate joined rooms (`{ id, canonical_alias, name, member_count }` per room) - `list_room_members(room)` — members of a room ### Room membership - `invite_user(room, user_id)` — invite `@user:server` into a room you're already in; you must have a high enough power level. The invitee sees a pending invite and resolves it via `resolve_invite`. - `resolve_invite(room, action)` — accept or reject a pending invite. `action` is `"accept"` (join the room) or `"reject"` (decline and leave). This is the path for invites; `join_room` is for joining a public room you weren't invited to. - `join_room(room)` — join a public room by id or alias. Also accepts a pending invite if one exists, but prefer `resolve_invite` for invites (it can reject too). - `list_invites()` — rooms this agent has been invited to but not yet joined (`{ id, canonical_alias, name }` per room). ### Receipts - `mark_read(room, event_id)` — advance the read receipt ## Multiple accounts `hyperhive.matrixAccounts` (declared in `agent.nix`) gives an agent *additional* matrix identities beyond the hive-internal one — e.g. an external-facing account alongside the internal one. 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 `hyperhive.matrix.url`). The hive-internal account is always named `main`, synthesized from `hyperhive.matrix.url` + agent state — this option only declares extras, and `main` is a reserved key here. Requires `hyperhive.matrix.enable = true`. Every matrix tool above takes an optional `account` parameter (a name from this map) to act as that identity instead of the primary one. ## 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 `hyperhive.mcp.matrixHttpPort` (declared in `hyperhive.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 `/matrix-token` is absent (account not yet provisioned); the `systemd.paths.hive-matrix-daemon` watcher restarts it the moment hive-c0re provisions the token. Incoming room events wake the agent via `AgentRequest::Wake` with `from: "matrix"`. The wake body format depends on the unread state: - **Single room, one message**: `[matrix] in : — use read_room to view, mark_read to clear` - **Single room, multiple messages**: `[matrix] N unread in — use read_room to view, mark_read to clear` - **Multiple rooms**: `[matrix] unread messages:` followed by a bulleted list (`- : : ` or `- : N unread` per room) The same per-room breakdown is included in the `UnreadMatrix` entry returned by `get_loose_ends` 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 was dropped 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** auto-join — the agent calls `list_invites` to see pending invites and `resolve_invite` to accept or reject them. **Pending invites as loose ends**: pending invites are upserted as keyed todos and appear in `get_loose_ends` output as `[matrix] invited to () — use list_invites to see pending invites, resolve_invite to accept or reject`. The keyed todo is cleared when a `resolve_invite` (or `join_room`) call resolves the invite. See [`docs/matrix.md`](../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 `hyperhive.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 external MCP-server flakes are pulled in by adding them to `inputs.*` and referenced as `flakeInputs..packages.${pkgs.system}.default` — the resolved sha lands in the agent's own `flake.lock` and rolls up to meta's. `allowedTools` defaults to `["*"]`, which expands to `mcp____*` (every tool from that server auto-approved). Restrict to specific tool names when you want finer control.