add send_reply and get_room_history mcp tools, show reply targets in prompt

This commit is contained in:
Damocles 2026-05-01 03:12:52 +02:00
parent 41da93a71f
commit 3f5208cab1
5 changed files with 190 additions and 3 deletions

View file

@ -2,6 +2,7 @@ use std::path::Path;
use matrix_sdk::{
Client,
room::reply::{EnforceThread, Reply},
ruma::{
OwnedRoomId, OwnedUserId,
events::{
@ -66,8 +67,16 @@ async fn handle_request(request: DaemonRequest, client: &Client) -> DaemonRespon
event_id,
key,
} => send_reaction(client, &room_id, &event_id, &key).await,
DaemonRequest::SendReply {
room_id,
event_id,
body,
} => send_reply(client, &room_id, &event_id, &body).await,
DaemonRequest::ListRooms {} => list_rooms(client).await,
DaemonRequest::ListRoomMembers { room_id } => list_room_members(client, &room_id).await,
DaemonRequest::GetRoomHistory { room_id, limit } => {
get_room_history(client, &room_id, limit).await
}
}
}
@ -183,3 +192,104 @@ async fn list_room_members(client: &Client, room_id: &str) -> DaemonResponse {
.collect();
DaemonResponse::ok(list)
}
async fn send_reply(client: &Client, room_id: &str, event_id: &str, body: &str) -> DaemonResponse {
let rid = match room_id.parse::<OwnedRoomId>() {
Ok(r) => r,
Err(e) => return DaemonResponse::err(format!("invalid room_id: {e}")),
};
let Some(room) = client.get_room(&rid) else {
return DaemonResponse::err(format!("room {rid} not found"));
};
let own_user = match client.user_id() {
Some(u) => u.to_owned(),
None => return DaemonResponse::err("not logged in".to_owned()),
};
// Resolve possibly-shortened event id against recent timeline
let tl = match timeline::load_timeline(&room, 50, &own_user).await {
Ok(t) => t,
Err(e) => return DaemonResponse::err(format!("failed to load timeline: {e}")),
};
let Some(full_eid) = timeline::resolve_event_id(&tl, event_id) else {
return DaemonResponse::err(format!("event {event_id} not found in timeline"));
};
let content = RoomMessageEventContent::text_plain(body).into();
let reply = Reply {
event_id: full_eid.clone(),
enforce_thread: EnforceThread::MaybeThreaded,
};
let reply_content = match room.make_reply_event(content, reply).await {
Ok(c) => c,
Err(e) => return DaemonResponse::err(format!("make_reply_event failed: {e}")),
};
match room.send(reply_content).await {
Ok(_) => {
tracing::info!(target = %full_eid, "mcp: sent reply");
DaemonResponse::ok(format!("replied to {full_eid}"))
}
Err(e) => DaemonResponse::err(format!("send reply failed: {e}")),
}
}
async fn get_room_history(
client: &Client,
room_id: &str,
limit: Option<usize>,
) -> DaemonResponse {
let rid = match room_id.parse::<OwnedRoomId>() {
Ok(r) => r,
Err(e) => return DaemonResponse::err(format!("invalid room_id: {e}")),
};
let Some(room) = client.get_room(&rid) else {
return DaemonResponse::err(format!("room {rid} not found"));
};
let own_user = match client.user_id() {
Some(u) => u.to_owned(),
None => return DaemonResponse::err("not logged in".to_owned()),
};
let limit = limit.unwrap_or(20).min(100);
let tl = match timeline::load_timeline(&room, limit, &own_user).await {
Ok(t) => t,
Err(e) => return DaemonResponse::err(format!("failed to load timeline: {e}")),
};
let items: Vec<_> = tl
.iter()
.map(|item| match item {
crate::types::TimelineItem::Message {
event_id,
sender,
body,
is_self,
ts,
in_reply_to,
} => json!({
"kind": "message",
"event_id": event_id.as_str(),
"sender": sender.as_str(),
"body": body,
"is_self": is_self,
"ts": ts,
"in_reply_to": in_reply_to.as_ref().map(|e| e.as_str()),
}),
crate::types::TimelineItem::Reaction {
sender,
target_event_id,
key,
is_self,
ts,
} => json!({
"kind": "reaction",
"sender": sender.as_str(),
"target_event_id": target_event_id.as_str(),
"key": key,
"is_self": is_self,
"ts": ts,
}),
})
.collect();
DaemonResponse::ok(items)
}