From c85dcc0deca79a871e74c0c3c17c25a2a6ef95d2 Mon Sep 17 00:00:00 2001 From: atlas Date: Fri, 5 Jun 2026 12:06:59 +0200 Subject: [PATCH] feat: matrix MCP invite_user tool MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds an invite_user tool to the per-agent matrix MCP so an agent can invite another user to a room it's already in (e.g. pull a peer into an existing chat). Mirrors the existing join_room/send_dm wiring: - protocol.rs: DaemonRequest::InviteUser { room, user_id } - handlers.rs: invite_user — parse user_id, resolve room, room.invite_user_by_id - socket.rs: dispatch arm - bin/mcp.rs: invite_user tool + InviteUserArgs room accepts an id or alias; the caller must hold a power level high enough to invite (the homeserver error is surfaced verbatim otherwise). The invitee then accepts via join_room. allowedTools is already '*' for the matrix server, so the tool is exposed without a nix change. --- hive-matrix-mcp/src/bin/mcp.rs | 25 +++++++++++++++++++++++++ hive-matrix-mcp/src/handlers.rs | 22 ++++++++++++++++++++++ hive-matrix-mcp/src/protocol.rs | 7 +++++++ hive-matrix-mcp/src/socket.rs | 3 +++ 4 files changed, 57 insertions(+) diff --git a/hive-matrix-mcp/src/bin/mcp.rs b/hive-matrix-mcp/src/bin/mcp.rs index c3f64b77..aefc5827 100644 --- a/hive-matrix-mcp/src/bin/mcp.rs +++ b/hive-matrix-mcp/src/bin/mcp.rs @@ -127,6 +127,15 @@ struct JoinRoomArgs { room: String, } +#[derive(Debug, Deserialize, JsonSchema)] +struct InviteUserArgs { + /// Matrix room id (`!abc:server`) or canonical alias (`#name:server`) + /// to invite the user into. You must already be a member. + room: String, + /// Matrix user id of the invitee (`@user:server`). + user_id: String, +} + struct MatrixBridge { #[allow(dead_code)] tool_router: rmcp::handler::server::router::tool::ToolRouter, @@ -237,6 +246,22 @@ impl MatrixBridge { render(round_trip(DaemonRequest::JoinRoom { room: args.room }).await) } + #[tool( + description = "Invite a user to a matrix room you're already in. `room` is a \ + room id (!abc:server) or alias (#name:server); `user_id` is the invitee \ + (@user:server). You must have a high enough power level in the room to \ + invite. The invitee then sees a pending invite they accept with `join_room`." + )] + async fn invite_user(&self, Parameters(args): Parameters) -> String { + render( + round_trip(DaemonRequest::InviteUser { + room: args.room, + user_id: args.user_id, + }) + .await, + ) + } + #[tool( description = "List the members of a matrix room (joined-state only). \ Each row carries the user id and resolved display name." diff --git a/hive-matrix-mcp/src/handlers.rs b/hive-matrix-mcp/src/handlers.rs index 08ffb186..353c9053 100644 --- a/hive-matrix-mcp/src/handlers.rs +++ b/hive-matrix-mcp/src/handlers.rs @@ -350,6 +350,28 @@ pub async fn join_room(client: &Client, room_ref: &str) -> DaemonResponse { } } +/// Invite `user_id` to `room_ref`. The calling agent must be a member of +/// the room with a power level high enough to invite; otherwise the +/// homeserver rejects it and the error is surfaced verbatim. +pub async fn invite_user(client: &Client, room_ref: &str, user_id: &str) -> DaemonResponse { + let uid: OwnedUserId = match user_id.parse() { + Ok(u) => u, + Err(e) => return DaemonResponse::error(format!("invalid user_id {user_id}: {e}")), + }; + let room = match resolve_room(client, room_ref).await { + Ok(r) => r, + Err(e) => return e, + }; + match room.invite_user_by_id(&uid).await { + Ok(()) => DaemonResponse::ok(&serde_json::json!({ + "invited": true, + "room_id": room.room_id().to_string(), + "user_id": uid.to_string(), + })), + Err(e) => DaemonResponse::error(format!("invite {uid} to {room_ref}: {e}")), + } +} + pub async fn read_room(client: &Client, room_ref: &str, limit: Option) -> DaemonResponse { use matrix_sdk::ruma::api::Direction; use matrix_sdk::ruma::api::client::message::get_message_events; diff --git a/hive-matrix-mcp/src/protocol.rs b/hive-matrix-mcp/src/protocol.rs index 02c90956..a855c7de 100644 --- a/hive-matrix-mcp/src/protocol.rs +++ b/hive-matrix-mcp/src/protocol.rs @@ -81,6 +81,13 @@ pub enum DaemonRequest { #[serde(rename = "join_room")] JoinRoom { room: String }, + /// Invite `user_id` (`@user:server`) to `room` (id or alias). The + /// calling agent must already be a member with a high enough power + /// level to invite. Idempotent-ish: inviting an already-joined or + /// already-invited user surfaces the matrix error from the server. + #[serde(rename = "invite_user")] + InviteUser { room: String, user_id: String }, + /// Return the count of rooms with unread notifications. Used by /// the harness `get_loose_ends` to surface unread matrix activity /// without exposing message content. diff --git a/hive-matrix-mcp/src/socket.rs b/hive-matrix-mcp/src/socket.rs index 5631b15b..f3b450b2 100644 --- a/hive-matrix-mcp/src/socket.rs +++ b/hive-matrix-mcp/src/socket.rs @@ -81,6 +81,9 @@ async fn dispatch(req: DaemonRequest, client: &Client) -> DaemonResponse { DaemonRequest::ListRooms => handlers::list_rooms(client).await, DaemonRequest::ListInvites => handlers::list_invites(client).await, DaemonRequest::JoinRoom { room } => handlers::join_room(client, &room).await, + DaemonRequest::InviteUser { room, user_id } => { + handlers::invite_user(client, &room, &user_id).await + } DaemonRequest::ListRoomMembers { room } => handlers::list_room_members(client, &room).await, DaemonRequest::ReadRoom { room, limit } => handlers::read_room(client, &room, limit).await, DaemonRequest::UnreadCount => handlers::unread_count(client),