diff --git a/hive-matrix-mcp/src/bin/mcp.rs b/hive-matrix-mcp/src/bin/mcp.rs index 38f4f715..c3f64b77 100644 --- a/hive-matrix-mcp/src/bin/mcp.rs +++ b/hive-matrix-mcp/src/bin/mcp.rs @@ -118,6 +118,15 @@ struct ReadRoomArgs { limit: Option, } +#[derive(Debug, Deserialize, JsonSchema)] +struct ListInvitesArgs {} + +#[derive(Debug, Deserialize, JsonSchema)] +struct JoinRoomArgs { + /// Matrix room id (`!abc:server`) or canonical alias (`#name:server`). + room: String, +} + struct MatrixBridge { #[allow(dead_code)] tool_router: rmcp::handler::server::router::tool::ToolRouter, @@ -210,6 +219,24 @@ impl MatrixBridge { render(round_trip(DaemonRequest::ListRooms).await) } + #[tool( + description = "List rooms this agent has been invited to but not yet joined. \ + Each row has the room id, canonical alias (when set), and display name. \ + Use `join_room` to accept an invite." + )] + async fn list_invites(&self, Parameters(_): Parameters) -> String { + render(round_trip(DaemonRequest::ListInvites).await) + } + + #[tool( + description = "Join a matrix room by id (!abc:server) or alias (#name:server). \ + Accepts a pending invite if one exists; also joins public rooms. \ + After joining the room will appear in `list_rooms`." + )] + async fn join_room(&self, Parameters(args): Parameters) -> String { + render(round_trip(DaemonRequest::JoinRoom { room: args.room }).await) + } + #[tool( description = "List the members of a matrix room (joined-state only). \ Each row carries the user id and resolved display name." @@ -237,8 +264,10 @@ impl MatrixBridge { specific user, `send_reaction` to react with an emoji, `send_reply` \ to thread a reply, `mark_read` to acknowledge an event. Discover \ rooms with `list_rooms`, members with `list_room_members`, recent \ - timeline with `read_room`. Room references accept ids (!abc:server) \ - or aliases (#name:server); user references use @user:server.")] + timeline with `read_room`. See pending invites with `list_invites`; \ + accept an invite or join a public room with `join_room`. Room \ + references accept ids (!abc:server) or aliases (#name:server); \ + user references use @user:server.")] impl ServerHandler for MatrixBridge {} #[tokio::main] diff --git a/hive-matrix-mcp/src/handlers.rs b/hive-matrix-mcp/src/handlers.rs index 98026452..3e5a5ea8 100644 --- a/hive-matrix-mcp/src/handlers.rs +++ b/hive-matrix-mcp/src/handlers.rs @@ -13,7 +13,7 @@ use matrix_sdk::{ Client, room::reply::{EnforceThread, Reply}, ruma::{ - OwnedEventId, OwnedRoomId, OwnedUserId, RoomOrAliasId, + OwnedEventId, OwnedRoomId, OwnedServerName, OwnedUserId, RoomOrAliasId, api::client::receipt::create_receipt::v3::ReceiptType, events::{ reaction::ReactionEventContent, @@ -27,6 +27,14 @@ use serde::Serialize; use crate::protocol::DaemonResponse; +/// JSON-shape for `list_invites`: one row per pending room invite. +#[derive(Debug, Serialize)] +pub struct InviteInfo { + pub room_id: String, + pub canonical_alias: Option, + pub name: Option, +} + /// JSON-shape for `list_rooms`: one row per joined room. #[derive(Debug, Serialize)] pub struct RoomInfo { @@ -267,6 +275,36 @@ pub async fn list_room_members(client: &Client, room_ref: &str) -> DaemonRespons DaemonResponse::ok(&list) } +pub async fn list_invites(client: &Client) -> DaemonResponse { + let invites: Vec = client + .invited_rooms() + .into_iter() + .map(|room| InviteInfo { + room_id: room.room_id().to_string(), + canonical_alias: room.canonical_alias().map(|a| a.to_string()), + name: room.name(), + }) + .collect(); + DaemonResponse::ok(&invites) +} + +pub async fn join_room(client: &Client, room_ref: &str) -> DaemonResponse { + let parsed: &RoomOrAliasId = match room_ref.try_into() { + Ok(p) => p, + Err(e) => { + return DaemonResponse::error(format!("invalid room reference {room_ref}: {e}")); + } + }; + let server_names: Vec = vec![]; + match client.join_room_by_id_or_alias(parsed, &server_names).await { + Ok(room) => DaemonResponse::ok(&serde_json::json!({ + "joined": true, + "room_id": room.room_id().to_string(), + })), + Err(e) => DaemonResponse::error(format!("join room {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 d5982620..445c0387 100644 --- a/hive-matrix-mcp/src/protocol.rs +++ b/hive-matrix-mcp/src/protocol.rs @@ -68,6 +68,19 @@ pub enum DaemonRequest { #[serde(rename = "read_room")] ReadRoom { room: String, limit: Option }, + /// List rooms this agent has been invited to but not yet joined. + /// Returns each room's id, canonical alias (when present), and + /// display name. + #[serde(rename = "list_invites")] + ListInvites, + + /// Join a room by id (`!abc:server`) or alias (`#name:server`). + /// Accepts a pending invite if one exists; also joins public rooms + /// the agent hasn't been explicitly invited to. After joining the + /// room will appear in `list_rooms`. + #[serde(rename = "join_room")] + JoinRoom { room: String }, + /// Liveness probe — fast "are you up?" round-trip that doesn't /// touch matrix-sdk. Not used by the in-tree stdio MCP bridge /// (which surfaces a daemon-down condition as a normal tool-call diff --git a/hive-matrix-mcp/src/socket.rs b/hive-matrix-mcp/src/socket.rs index 5e0d6b9b..d5e36b03 100644 --- a/hive-matrix-mcp/src/socket.rs +++ b/hive-matrix-mcp/src/socket.rs @@ -79,6 +79,8 @@ async fn dispatch(req: DaemonRequest, client: &Client) -> DaemonResponse { handlers::mark_read(client, &room, &event_id).await } DaemonRequest::ListRooms => handlers::list_rooms(client).await, + DaemonRequest::ListInvites => handlers::list_invites(client).await, + DaemonRequest::JoinRoom { room } => handlers::join_room(client, &room).await, DaemonRequest::ListRoomMembers { room } => handlers::list_room_members(client, &room).await, DaemonRequest::ReadRoom { room, limit } => handlers::read_room(client, &room, limit).await, }