hyperhive/docs/tools/matrix.md
atlas 992468dccc docs: reword three vale-flagged sentences in matrix.md
Preserve meaning exactly: 'There is no' -> 'No ... exists', 'That is'
-> 'that's', 'is not' -> isn't.

Refs #4475
2026-09-18 10:35:16 +02:00

186 lines
9.2 KiB
Markdown

# Matrix MCP tools and extra MCP servers
## 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 host-level
matrix tuwunel is configured. Tools land as `mcp__matrix__<name>`:
### 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 (for example 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
`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 `tokenFile` is pinned to
`<state>/matrix-token` (hive-c0re provisions it there), 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 — it replaces the removed
`services.hyperhive.agent.matrix.enable = false`, which now fails
evaluation with a message saying so. Declaring an external account with
its own `homeserver` is enough on its own; a hive homeserver isn't
required.
Every matrix tool above takes an optional `account` parameter (a name
from this map) to act as that identity instead of the primary one.
Declaring an extra account also changes what the agent *receives*: wake
bodies and invite todos gain an `[acct:<name>]` 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 `<state>/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] <sender> in <room>: <body> —
use read_room to view, mark_read to clear`
- **Single room, multiple messages**: `[matrix] N unread in <room> —
use read_room to view, mark_read to clear`
- **Multiple rooms**: `[matrix] unread messages:` followed by a
bulleted list (`- <room>: <sender>: <body>` or `- <room>: N unread`
per room)
**Multi-account prefix**: when the daemon serves more than one account
(`services.hyperhive.agent.matrixAccounts`), it prefixes every wake body and every invite todo
below with `[acct:<name>] `, so a wake arrives as
`[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 was dropped 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 keyed todo
is cleared 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.<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 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.