diff --git a/Cargo.lock b/Cargo.lock index 001ca1df..28a1a7d2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1378,8 +1378,6 @@ dependencies = [ "futures-util", "hive-sh4re", "matrix-sdk", - "mime", - "mime_guess", "reqwest", "rmcp", "schemars", diff --git a/hive-matrix-mcp/Cargo.toml b/hive-matrix-mcp/Cargo.toml index 5dd9d32c..9f74495a 100644 --- a/hive-matrix-mcp/Cargo.toml +++ b/hive-matrix-mcp/Cargo.toml @@ -11,8 +11,6 @@ anyhow.workspace = true futures-util.workspace = true hive-sh4re.workspace = true matrix-sdk.workspace = true -mime = "0.3" -mime_guess = "2" reqwest.workspace = true rmcp.workspace = true schemars.workspace = true diff --git a/hive-matrix-mcp/src/bin/mcp.rs b/hive-matrix-mcp/src/bin/mcp.rs index 8be34203..8616414e 100644 --- a/hive-matrix-mcp/src/bin/mcp.rs +++ b/hive-matrix-mcp/src/bin/mcp.rs @@ -79,26 +79,6 @@ struct SendDmArgs { body: String, } -#[derive(Debug, Deserialize, JsonSchema)] -struct SendFileArgs { - /// Matrix room id (`!abc:server`) or canonical alias (`#name:server`). - room: String, - /// Absolute path to a local file readable by the agent (e.g. a built - /// PDF under the agent's workspace). MIME type is inferred from the - /// extension. 50 MiB cap. - path: String, - /// Optional caption, sent as a follow-up text message in the room. - #[serde(default)] - caption: Option, -} - -#[derive(Debug, Deserialize, JsonSchema)] -struct OpenDmArgs { - /// Matrix user id (`@user:server`) to open a DM with. The DM room is - /// created if one doesn't already exist. - user_id: String, -} - #[derive(Debug, Deserialize, JsonSchema)] struct SendReactionArgs { room: String, @@ -216,37 +196,6 @@ impl MatrixBridge { ) } - #[tool( - description = "Upload a local file and post it as an attachment to a matrix \ - room. `room` is a room id (!abc:server) or alias (#name:server); `path` \ - is an absolute path to a local file (MIME inferred from extension, 50 MiB \ - cap); optional `caption` is sent as a follow-up message. Rejected if the \ - room has unread messages — read_room then mark_read first." - )] - async fn send_file(&self, Parameters(args): Parameters) -> String { - render( - round_trip(DaemonRequest::SendFile { - room: args.room, - path: args.path, - caption: args.caption, - }) - .await, - ) - } - - #[tool(description = "Resolve (find-or-create) the DM room with `user_id` \ - (@user:server) and return its room id, without sending anything. Use the \ - returned room id with the room-based tools (`send_file`, `send_message`, \ - …) to deliver into the DM — there is no per-tool DM variant.")] - async fn open_dm(&self, Parameters(args): Parameters) -> String { - render( - round_trip(DaemonRequest::OpenDm { - user_id: args.user_id, - }) - .await, - ) - } - #[tool( description = "React to a specific matrix event with an emoji or short \ string `key`. Matrix-spec annotation; renders as a reaction in \ diff --git a/hive-matrix-mcp/src/handlers.rs b/hive-matrix-mcp/src/handlers.rs index 8020dff4..eff7bda9 100644 --- a/hive-matrix-mcp/src/handlers.rs +++ b/hive-matrix-mcp/src/handlers.rs @@ -184,33 +184,25 @@ pub async fn send_message(client: &Client, room_ref: &str, body: &str) -> Daemon } } -/// Find the existing DM room with `user_id` or create one. Shared by -/// `send_dm` and `open_dm`. `direct_targets()` (cached state) is -/// used rather than the async `is_direct()`. -async fn resolve_or_create_dm( - client: &Client, - user_id: &str, -) -> Result { - let uid: OwnedUserId = user_id - .parse() - .map_err(|e| DaemonResponse::error(format!("invalid user_id {user_id}: {e}")))?; - if let Some(room) = client.joined_rooms().into_iter().find(|r| { +pub async fn send_dm(client: &Client, user_id: &str, body: &str) -> DaemonResponse { + let uid: OwnedUserId = match user_id.parse() { + Ok(u) => u, + Err(e) => return DaemonResponse::error(format!("invalid user_id {user_id}: {e}")), + }; + // Find existing DM or create one. + let room = client.joined_rooms().into_iter().find(|r| { + // is_direct() is async; check direct_targets() instead which + // reads from cached state. r.direct_targets() .iter() .any(|t| t.as_str() == uid.as_str()) - }) { - return Ok(room); - } - client - .create_dm(&uid) - .await - .map_err(|e| DaemonResponse::error(format!("create_dm {uid}: {e}"))) -} - -pub async fn send_dm(client: &Client, user_id: &str, body: &str) -> DaemonResponse { - let room = match resolve_or_create_dm(client, user_id).await { - Ok(r) => r, - Err(e) => return e, + }); + let room = match room { + Some(r) => r, + None => match client.create_dm(&uid).await { + Ok(r) => r, + Err(e) => return DaemonResponse::error(format!("create_dm {uid}: {e}")), + }, }; if let Some(reject) = unread_guard(&room) { return reject; @@ -220,105 +212,12 @@ pub async fn send_dm(client: &Client, user_id: &str, body: &str) -> DaemonRespon Ok(resp) => DaemonResponse::ok(&serde_json::json!({ "event_id": resp.event_id.to_string(), "room_id": room.room_id().to_string(), - "user_id": user_id, + "user_id": uid.to_string(), })), - Err(e) => DaemonResponse::error(format!("send DM to {user_id}: {e}")), + Err(e) => DaemonResponse::error(format!("send DM to {uid}: {e}")), } } -/// Maximum file size accepted by `send_file`. -const MAX_UPLOAD_BYTES: u64 = 50 * 1024 * 1024; - -/// Read a local file and post it as a matrix attachment to `room`, -/// inferring the MIME type from the file extension. An optional -/// `caption` is sent as a best-effort follow-up text message (the -/// attachment has already landed, so a caption failure is non-fatal). -/// Used by `send_file`. -async fn upload_attachment( - room: &matrix_sdk::Room, - path: &str, - caption: Option<&str>, -) -> DaemonResponse { - let p = std::path::Path::new(path); - let meta = match tokio::fs::metadata(p).await { - Ok(m) => m, - Err(e) => return DaemonResponse::error(format!("stat {path}: {e}")), - }; - if !meta.is_file() { - return DaemonResponse::error(format!("{path} is not a regular file")); - } - if meta.len() > MAX_UPLOAD_BYTES { - return DaemonResponse::error(format!( - "{path} is {} bytes, over the {MAX_UPLOAD_BYTES}-byte upload cap", - meta.len() - )); - } - let data = match tokio::fs::read(p).await { - Ok(d) => d, - Err(e) => return DaemonResponse::error(format!("read {path}: {e}")), - }; - let filename = p - .file_name() - .and_then(|n| n.to_str()) - .unwrap_or("file") - .to_owned(); - let content_type: mime::Mime = mime_guess::from_path(p).first_or_octet_stream(); - let resp = match room - .send_attachment( - filename.clone(), - &content_type, - data, - matrix_sdk::attachment::AttachmentConfig::new(), - ) - .await - { - Ok(r) => r, - Err(e) => { - return DaemonResponse::error(format!("send attachment to {}: {e}", room.room_id())); - } - }; - if let Some(c) = caption.filter(|c| !c.is_empty()) { - let _ = room.send(RoomMessageEventContent::text_markdown(c)).await; - } - DaemonResponse::ok(&serde_json::json!({ - "event_id": resp.event_id.to_string(), - "room_id": room.room_id().to_string(), - "filename": filename, - "mime": content_type.essence_str(), - })) -} - -pub async fn send_file( - client: &Client, - room_ref: &str, - path: &str, - caption: Option<&str>, -) -> DaemonResponse { - let room = match resolve_room(client, room_ref).await { - Ok(r) => r, - Err(e) => return e, - }; - if let Some(reject) = unread_guard(&room) { - return reject; - } - upload_attachment(&room, path, caption).await -} - -/// Resolve (find-or-create) the DM room with `user_id` and return its -/// room id without sending anything. The caller then uses the room-based -/// tools (`send_file`, `send_message`, …) against that id — so there is -/// no per-tool `_dm` variant. -pub async fn open_dm(client: &Client, user_id: &str) -> DaemonResponse { - let room = match resolve_or_create_dm(client, user_id).await { - Ok(r) => r, - Err(e) => return e, - }; - DaemonResponse::ok(&serde_json::json!({ - "room_id": room.room_id().to_string(), - "user_id": user_id, - })) -} - pub async fn send_reaction( client: &Client, room_ref: &str, diff --git a/hive-matrix-mcp/src/protocol.rs b/hive-matrix-mcp/src/protocol.rs index 58a3b231..bd71884f 100644 --- a/hive-matrix-mcp/src/protocol.rs +++ b/hive-matrix-mcp/src/protocol.rs @@ -27,24 +27,6 @@ pub enum DaemonRequest { #[serde(rename = "send_dm")] SendDm { user_id: String, body: String }, - /// Upload a local file and post it as an attachment to `room` - /// (id or alias). `caption`, when set, is sent as a follow-up - /// text message in the same room. - #[serde(rename = "send_file")] - SendFile { - room: String, - path: String, - caption: Option, - }, - - /// Resolve (find-or-create) the DM room with `user_id` and return - /// its room id, without sending anything. Lets a caller obtain the - /// DM room id and then use the room-based tools (`send_file`, - /// `send_message`, …) against it — so there is no per-tool `_dm` - /// variant. - #[serde(rename = "open_dm")] - OpenDm { user_id: String }, - /// React to a specific event with an emoji `key`. Matrix-spec /// `m.reaction` annotation. #[serde(rename = "send_reaction")] diff --git a/hive-matrix-mcp/src/socket.rs b/hive-matrix-mcp/src/socket.rs index 2a8a588f..0f09b876 100644 --- a/hive-matrix-mcp/src/socket.rs +++ b/hive-matrix-mcp/src/socket.rs @@ -65,12 +65,6 @@ async fn dispatch(req: DaemonRequest, client: &Client) -> DaemonResponse { handlers::send_message(client, &room, &body).await } DaemonRequest::SendDm { user_id, body } => handlers::send_dm(client, &user_id, &body).await, - DaemonRequest::SendFile { - room, - path, - caption, - } => handlers::send_file(client, &room, &path, caption.as_deref()).await, - DaemonRequest::OpenDm { user_id } => handlers::open_dm(client, &user_id).await, DaemonRequest::SendReaction { room, event_id,