Words the alex and Microsoft styles flag where a plainer one reads as well: disabled/special/invalid as a state, primitive, trap, hang, several, click, first-person FAQ questions, hyphenated adverb pairs. Left as they are: Let's Encrypt, quoted UI labels and phrases, the generated forge CLI reference, and the middle-click gesture.
166 lines
8.8 KiB
Markdown
166 lines
8.8 KiB
Markdown
# 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_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
|
|
|
|
<!-- vale write-good.Passive = NO -->
|
|
`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.
|
|
<!-- vale write-good.Passive = YES -->
|
|
|
|
`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](#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`
|
|
- **Multiple 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`](../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.
|