add send_reply and get_room_history mcp tools, show reply targets in prompt
This commit is contained in:
parent
41da93a71f
commit
3f5208cab1
5 changed files with 190 additions and 3 deletions
|
|
@ -40,11 +40,24 @@ mod protocol_inline {
|
|||
key: String,
|
||||
},
|
||||
|
||||
#[serde(rename = "send_reply")]
|
||||
SendReply {
|
||||
room_id: String,
|
||||
event_id: String,
|
||||
body: String,
|
||||
},
|
||||
|
||||
#[serde(rename = "list_rooms")]
|
||||
ListRooms {},
|
||||
|
||||
#[serde(rename = "list_room_members")]
|
||||
ListRoomMembers { room_id: String },
|
||||
|
||||
#[serde(rename = "get_room_history")]
|
||||
GetRoomHistory {
|
||||
room_id: String,
|
||||
limit: Option<usize>,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
|
|
@ -94,6 +107,24 @@ struct ListRoomMembersParams {
|
|||
room_id: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, JsonSchema)]
|
||||
struct SendReplyParams {
|
||||
/// The event ID to reply to. Can be the shortened form shown in the
|
||||
/// timeline (e.g. $abc123de...).
|
||||
event_id: String,
|
||||
/// The reply text.
|
||||
body: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, JsonSchema)]
|
||||
struct GetRoomHistoryParams {
|
||||
/// The room ID to fetch history for.
|
||||
room_id: String,
|
||||
/// Number of recent timeline items to return. Default 20, max 100.
|
||||
#[serde(default)]
|
||||
limit: Option<usize>,
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// MCP server struct
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
@ -221,6 +252,35 @@ impl MatrixBridge {
|
|||
.await?;
|
||||
Self::response_to_result(resp)
|
||||
}
|
||||
|
||||
#[tool(description = "Reply to a specific message in the source room. Threads via m.in_reply_to relation so clients render the quote.")]
|
||||
async fn send_reply(
|
||||
&self,
|
||||
Parameters(params): Parameters<SendReplyParams>,
|
||||
) -> Result<CallToolResult, McpError> {
|
||||
let resp = self
|
||||
.call(&DaemonRequest::SendReply {
|
||||
room_id: self.source_room.clone(),
|
||||
event_id: params.event_id,
|
||||
body: params.body,
|
||||
})
|
||||
.await?;
|
||||
Self::response_to_result(resp)
|
||||
}
|
||||
|
||||
#[tool(description = "Get recent message history for any joined room. Returns JSON list of messages and reactions with timestamps.")]
|
||||
async fn get_room_history(
|
||||
&self,
|
||||
Parameters(params): Parameters<GetRoomHistoryParams>,
|
||||
) -> Result<CallToolResult, McpError> {
|
||||
let resp = self
|
||||
.call(&DaemonRequest::GetRoomHistory {
|
||||
room_id: params.room_id,
|
||||
limit: params.limit,
|
||||
})
|
||||
.await?;
|
||||
Self::response_to_result(resp)
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -58,7 +58,7 @@ pub async fn invoke_claude(
|
|||
"--add-dir",
|
||||
&identity_str,
|
||||
"--allowedTools",
|
||||
"Read,Edit,Write,Glob,Grep,mcp__matrix__send_message,mcp__matrix__send_dm,mcp__matrix__send_reaction,mcp__matrix__list_rooms,mcp__matrix__list_room_members",
|
||||
"Read,Edit,Write,Glob,Grep,mcp__matrix__send_message,mcp__matrix__send_dm,mcp__matrix__send_reaction,mcp__matrix__send_reply,mcp__matrix__list_rooms,mcp__matrix__list_room_members,mcp__matrix__get_room_history",
|
||||
"--mcp-config",
|
||||
&mcp_config_str,
|
||||
"-p",
|
||||
|
|
|
|||
|
|
@ -17,11 +17,24 @@ pub enum DaemonRequest {
|
|||
key: String,
|
||||
},
|
||||
|
||||
#[serde(rename = "send_reply")]
|
||||
SendReply {
|
||||
room_id: String,
|
||||
event_id: String,
|
||||
body: String,
|
||||
},
|
||||
|
||||
#[serde(rename = "list_rooms")]
|
||||
ListRooms {},
|
||||
|
||||
#[serde(rename = "list_room_members")]
|
||||
ListRoomMembers { room_id: String },
|
||||
|
||||
#[serde(rename = "get_room_history")]
|
||||
GetRoomHistory {
|
||||
room_id: String,
|
||||
limit: Option<usize>,
|
||||
},
|
||||
}
|
||||
|
||||
/// Response from daemon to MCP server.
|
||||
|
|
|
|||
110
src/socket.rs
110
src/socket.rs
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -95,11 +95,15 @@ pub fn render_timeline_item(
|
|||
body,
|
||||
is_self,
|
||||
ts,
|
||||
..
|
||||
in_reply_to,
|
||||
} => {
|
||||
let ts_str = format_ts(*ts);
|
||||
let id = short_event_id(event_id);
|
||||
let prefix = if *is_self { "(you) " } else { "" };
|
||||
let reply_str = match in_reply_to {
|
||||
Some(target) => format!(" [reply to {}]", short_event_id(target)),
|
||||
None => String::new(),
|
||||
};
|
||||
let readers_str = match read_markers.get(event_id) {
|
||||
Some(rs) if !rs.is_empty() => {
|
||||
let mut sorted = rs.clone();
|
||||
|
|
@ -111,7 +115,7 @@ pub fn render_timeline_item(
|
|||
};
|
||||
writeln!(
|
||||
prompt,
|
||||
"[{ts_str}] {id} {prefix}{sender}: {body}{readers_str}"
|
||||
"[{ts_str}] {id} {prefix}{sender}:{reply_str} {body}{readers_str}"
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue