Per mara on hyperhive#1898: the top-level README should not go into details about impl details or specific subsystems. Trims the Multi-account Matrix support and GitHub account sections down to a one-line pointer each. The matrixAccounts option detail that was only ever documented in the README (checked: docs/matrix.md is entirely about the host hive-matrix container, not this per-agent option) moves to docs/tools/matrix.md instead, next to the account parameter every matrix tool already takes - the natural home, not a link to a page that never covered it.
152 lines
7.2 KiB
Markdown
152 lines
7.2 KiB
Markdown
# 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__<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 (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 `<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)
|
|
|
|
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**: when the daemon's sync loop receives an
|
|
`m.room.member` invite event, it upserts a todo (keyed `invite:<room>`)
|
|
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] pending invite: <room> — use list_invites to see,
|
|
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.<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 external MCP-server flakes are pulled
|
|
in by adding them to `inputs.*` and referenced 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 auto-approved). Restrict
|
|
to specific tool names when you want finer control.
|