add fetch_event tool and backfill get_room_history beyond event cache
This commit is contained in:
parent
e538be2c3a
commit
829a60854f
4 changed files with 251 additions and 41 deletions
|
|
@ -58,6 +58,13 @@ mod protocol_inline {
|
||||||
room_id: String,
|
room_id: String,
|
||||||
limit: Option<usize>,
|
limit: Option<usize>,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
#[serde(rename = "fetch_event")]
|
||||||
|
FetchEvent {
|
||||||
|
room_id: String,
|
||||||
|
event_id: String,
|
||||||
|
context_before: Option<u32>,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
|
@ -125,6 +132,24 @@ struct GetRoomHistoryParams {
|
||||||
limit: Option<usize>,
|
limit: Option<usize>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, JsonSchema)]
|
||||||
|
struct FetchEventParams {
|
||||||
|
/// The event ID to fetch. Can be the shortened form shown in the
|
||||||
|
/// timeline (e.g. $abc123de...) or a full ID. Use this to dereference
|
||||||
|
/// reply targets shown as [reply to $abc...] in the prompt, or to look
|
||||||
|
/// up any specific event by ID.
|
||||||
|
event_id: String,
|
||||||
|
/// Number of context messages BEFORE the target to include. Default 0.
|
||||||
|
/// Pass 5-10 for conversational context. The response also includes an
|
||||||
|
/// `earlier_handle` event_id you can pass to fetch_event to page further
|
||||||
|
/// back into history.
|
||||||
|
#[serde(default)]
|
||||||
|
context_before: Option<u32>,
|
||||||
|
/// Room ID to look in. Defaults to source room if omitted.
|
||||||
|
#[serde(default)]
|
||||||
|
room_id: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// MCP server struct
|
// MCP server struct
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
@ -268,7 +293,7 @@ impl MatrixBridge {
|
||||||
Self::response_to_result(resp)
|
Self::response_to_result(resp)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tool(description = "Get recent message history for any joined room. Returns JSON list of messages and reactions with timestamps.")]
|
#[tool(description = "Get recent message history for any joined room. Returns JSON list of messages and reactions with timestamps. Backfills via /messages if cache is short.")]
|
||||||
async fn get_room_history(
|
async fn get_room_history(
|
||||||
&self,
|
&self,
|
||||||
Parameters(params): Parameters<GetRoomHistoryParams>,
|
Parameters(params): Parameters<GetRoomHistoryParams>,
|
||||||
|
|
@ -281,6 +306,22 @@ impl MatrixBridge {
|
||||||
.await?;
|
.await?;
|
||||||
Self::response_to_result(resp)
|
Self::response_to_result(resp)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tool(description = "Fetch a specific event by ID with optional context messages before it. Use to dereference [reply to $...] markers or arbitrary event IDs. Response includes earlier_handle for paging further back.")]
|
||||||
|
async fn fetch_event(
|
||||||
|
&self,
|
||||||
|
Parameters(params): Parameters<FetchEventParams>,
|
||||||
|
) -> Result<CallToolResult, McpError> {
|
||||||
|
let room_id = params.room_id.unwrap_or_else(|| self.source_room.clone());
|
||||||
|
let resp = self
|
||||||
|
.call(&DaemonRequest::FetchEvent {
|
||||||
|
room_id,
|
||||||
|
event_id: params.event_id,
|
||||||
|
context_before: params.context_before,
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
Self::response_to_result(resp)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
|
@ -58,7 +58,7 @@ pub async fn invoke_claude(
|
||||||
"--add-dir",
|
"--add-dir",
|
||||||
&identity_str,
|
&identity_str,
|
||||||
"--allowedTools",
|
"--allowedTools",
|
||||||
"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",
|
"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__matrix__fetch_event",
|
||||||
"--mcp-config",
|
"--mcp-config",
|
||||||
&mcp_config_str,
|
&mcp_config_str,
|
||||||
"-p",
|
"-p",
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,13 @@ pub enum DaemonRequest {
|
||||||
room_id: String,
|
room_id: String,
|
||||||
limit: Option<usize>,
|
limit: Option<usize>,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
#[serde(rename = "fetch_event")]
|
||||||
|
FetchEvent {
|
||||||
|
room_id: String,
|
||||||
|
event_id: String,
|
||||||
|
context_before: Option<u32>,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Response from daemon to MCP server.
|
/// Response from daemon to MCP server.
|
||||||
|
|
|
||||||
240
src/socket.rs
240
src/socket.rs
|
|
@ -77,6 +77,11 @@ async fn handle_request(request: DaemonRequest, client: &Client) -> DaemonRespon
|
||||||
DaemonRequest::GetRoomHistory { room_id, limit } => {
|
DaemonRequest::GetRoomHistory { room_id, limit } => {
|
||||||
get_room_history(client, &room_id, limit).await
|
get_room_history(client, &room_id, limit).await
|
||||||
}
|
}
|
||||||
|
DaemonRequest::FetchEvent {
|
||||||
|
room_id,
|
||||||
|
event_id,
|
||||||
|
context_before,
|
||||||
|
} => fetch_event(client, &room_id, &event_id, context_before).await,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -251,45 +256,202 @@ async fn get_room_history(
|
||||||
};
|
};
|
||||||
let limit = limit.unwrap_or(20).min(100);
|
let limit = limit.unwrap_or(20).min(100);
|
||||||
|
|
||||||
let tl = match timeline::load_timeline(&room, limit, &own_user).await {
|
// Backfill via /messages if cache is short
|
||||||
Ok(t) => t,
|
if let Ok((cache, _)) = room.event_cache().await {
|
||||||
Err(e) => return DaemonResponse::err(format!("failed to load timeline: {e}")),
|
let mut 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 mut tries = 0;
|
||||||
|
while tl.len() < limit && tries < 5 {
|
||||||
|
tries += 1;
|
||||||
|
match cache.pagination().run_backwards_once((limit - tl.len()) as u16).await {
|
||||||
|
Ok(outcome) => {
|
||||||
|
if outcome.reached_start {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
tracing::warn!("backfill failed: {e}");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tl = match timeline::load_timeline(&room, limit, &own_user).await {
|
||||||
|
Ok(t) => t,
|
||||||
|
Err(e) => return DaemonResponse::err(format!("reload after backfill failed: {e}")),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let items: Vec<_> = tl.iter().map(timeline_item_to_json).collect();
|
||||||
|
DaemonResponse::ok(items)
|
||||||
|
} else {
|
||||||
|
DaemonResponse::err("event cache not available".to_owned())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn timeline_item_to_json(item: &crate::types::TimelineItem) -> serde_json::Value {
|
||||||
|
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,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fetch a specific event by ID via the homeserver `/context` endpoint.
|
||||||
|
/// Returns the event plus `context_before` events before it. Includes one
|
||||||
|
/// extra event as `earlier_handle` so the shard can page further backward
|
||||||
|
/// by calling fetch_event again with that id.
|
||||||
|
async fn fetch_event(
|
||||||
|
client: &Client,
|
||||||
|
room_id: &str,
|
||||||
|
event_id: &str,
|
||||||
|
context_before: Option<u32>,
|
||||||
|
) -> DaemonResponse {
|
||||||
|
use matrix_sdk::ruma::events::AnySyncTimelineEvent;
|
||||||
|
use matrix_sdk::ruma::events::room::message::MessageType;
|
||||||
|
|
||||||
|
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 items: Vec<_> = tl
|
// Resolve possibly-shortened event id against recent timeline first;
|
||||||
.iter()
|
// fall back to parsing as a full id.
|
||||||
.map(|item| match item {
|
let resolve_tl = timeline::load_timeline(&room, 50, &own_user)
|
||||||
crate::types::TimelineItem::Message {
|
.await
|
||||||
event_id,
|
.unwrap_or_default();
|
||||||
sender,
|
let full_eid = match timeline::resolve_event_id(&resolve_tl, event_id) {
|
||||||
body,
|
Some(eid) => eid,
|
||||||
is_self,
|
None => match event_id.parse::<matrix_sdk::ruma::OwnedEventId>() {
|
||||||
ts,
|
Ok(eid) => eid,
|
||||||
in_reply_to,
|
Err(e) => {
|
||||||
} => json!({
|
return DaemonResponse::err(format!("invalid or unknown event_id: {e}"));
|
||||||
"kind": "message",
|
}
|
||||||
"event_id": event_id.as_str(),
|
},
|
||||||
"sender": sender.as_str(),
|
};
|
||||||
"body": body,
|
|
||||||
"is_self": is_self,
|
// Request one extra event so we can split it off as the paging handle
|
||||||
"ts": ts,
|
let context_before = context_before.unwrap_or(0).min(50);
|
||||||
"in_reply_to": in_reply_to.as_ref().map(|e| e.as_str()),
|
let request_size = context_before + 1;
|
||||||
}),
|
|
||||||
crate::types::TimelineItem::Reaction {
|
let response = match room
|
||||||
sender,
|
.event_with_context(
|
||||||
target_event_id,
|
&full_eid,
|
||||||
key,
|
false,
|
||||||
is_self,
|
matrix_sdk::ruma::UInt::from(request_size),
|
||||||
ts,
|
None,
|
||||||
} => json!({
|
)
|
||||||
"kind": "reaction",
|
.await
|
||||||
"sender": sender.as_str(),
|
{
|
||||||
"target_event_id": target_event_id.as_str(),
|
Ok(r) => r,
|
||||||
"key": key,
|
Err(e) => return DaemonResponse::err(format!("event_with_context failed: {e}")),
|
||||||
"is_self": is_self,
|
};
|
||||||
"ts": ts,
|
|
||||||
}),
|
let render = |raw: &matrix_sdk::deserialized_responses::TimelineEvent| -> Option<serde_json::Value> {
|
||||||
})
|
let deserialized = raw.raw().deserialize().ok()?;
|
||||||
.collect();
|
let AnySyncTimelineEvent::MessageLike(msg) = deserialized else {
|
||||||
DaemonResponse::ok(items)
|
return None;
|
||||||
|
};
|
||||||
|
match msg {
|
||||||
|
matrix_sdk::ruma::events::AnySyncMessageLikeEvent::RoomMessage(
|
||||||
|
matrix_sdk::ruma::events::SyncMessageLikeEvent::Original(orig),
|
||||||
|
) => {
|
||||||
|
let MessageType::Text(text) = &orig.content.msgtype else {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
let ms: u64 = orig.origin_server_ts.0.into();
|
||||||
|
let ts = (ms / 1000) as i64;
|
||||||
|
let in_reply_to = match &orig.content.relates_to {
|
||||||
|
Some(matrix_sdk::ruma::events::room::message::Relation::Reply { in_reply_to }) => {
|
||||||
|
Some(in_reply_to.event_id.as_str().to_owned())
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
Some(json!({
|
||||||
|
"kind": "message",
|
||||||
|
"event_id": orig.event_id.as_str(),
|
||||||
|
"sender": orig.sender.as_str(),
|
||||||
|
"body": text.body,
|
||||||
|
"is_self": orig.sender == own_user,
|
||||||
|
"ts": ts,
|
||||||
|
"in_reply_to": in_reply_to,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
matrix_sdk::ruma::events::AnySyncMessageLikeEvent::Reaction(
|
||||||
|
matrix_sdk::ruma::events::SyncMessageLikeEvent::Original(orig),
|
||||||
|
) => {
|
||||||
|
let ms: u64 = orig.origin_server_ts.0.into();
|
||||||
|
let ts = (ms / 1000) as i64;
|
||||||
|
Some(json!({
|
||||||
|
"kind": "reaction",
|
||||||
|
"sender": orig.sender.as_str(),
|
||||||
|
"target_event_id": orig.content.relates_to.event_id.as_str(),
|
||||||
|
"key": orig.content.relates_to.key,
|
||||||
|
"is_self": orig.sender == own_user,
|
||||||
|
"ts": ts,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// events_before is newest-first per matrix /context spec - reverse for chronological
|
||||||
|
let mut before: Vec<_> = response.events_before.iter().filter_map(render).collect();
|
||||||
|
before.reverse();
|
||||||
|
|
||||||
|
// The "earlier handle" is the oldest event we got, if we asked for more than 0
|
||||||
|
// It's used by the shard to page further back via another fetch_event call
|
||||||
|
let earlier_handle = if context_before > 0 && before.len() > context_before as usize {
|
||||||
|
before.first().and_then(|e| e.get("event_id").and_then(|v| v.as_str()).map(String::from))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
let context_events: Vec<_> = if earlier_handle.is_some() {
|
||||||
|
// First event is the handle - skip it from the main events list
|
||||||
|
before.into_iter().skip(1).collect()
|
||||||
|
} else {
|
||||||
|
before
|
||||||
|
};
|
||||||
|
|
||||||
|
let target = response.event.as_ref().and_then(render);
|
||||||
|
|
||||||
|
DaemonResponse::ok(json!({
|
||||||
|
"event": target,
|
||||||
|
"context_before": context_events,
|
||||||
|
"earlier_handle": earlier_handle,
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue