replace === output parsing with MCP tools via rmcp
This commit is contained in:
parent
09259ee5fa
commit
9354837830
8 changed files with 761 additions and 437 deletions
185
src/socket.rs
Normal file
185
src/socket.rs
Normal file
|
|
@ -0,0 +1,185 @@
|
|||
use std::path::Path;
|
||||
|
||||
use matrix_sdk::{
|
||||
Client,
|
||||
ruma::{
|
||||
OwnedRoomId, OwnedUserId,
|
||||
events::{
|
||||
reaction::ReactionEventContent,
|
||||
relation::Annotation,
|
||||
room::message::RoomMessageEventContent,
|
||||
},
|
||||
},
|
||||
};
|
||||
use serde_json::json;
|
||||
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
|
||||
use tokio::net::{UnixListener, UnixStream};
|
||||
|
||||
use crate::handlers;
|
||||
use crate::protocol::{DaemonRequest, DaemonResponse};
|
||||
use crate::timeline;
|
||||
|
||||
pub async fn start_listener(socket_path: &Path, client: Client) -> anyhow::Result<()> {
|
||||
let _ = tokio::fs::remove_file(socket_path).await;
|
||||
let listener = UnixListener::bind(socket_path)?;
|
||||
tracing::info!(path = %socket_path.display(), "mcp socket listener started");
|
||||
|
||||
loop {
|
||||
let (stream, _) = listener.accept().await?;
|
||||
let client = client.clone();
|
||||
tokio::spawn(async move {
|
||||
if let Err(e) = handle_connection(stream, client).await {
|
||||
tracing::warn!("mcp socket connection error: {e}");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_connection(stream: UnixStream, client: Client) -> anyhow::Result<()> {
|
||||
let (reader, mut writer) = stream.into_split();
|
||||
let mut lines = BufReader::new(reader).lines();
|
||||
|
||||
while let Some(line) = lines.next_line().await? {
|
||||
let response = match serde_json::from_str::<DaemonRequest>(&line) {
|
||||
Ok(request) => {
|
||||
tracing::debug!(?request, "mcp socket request");
|
||||
handle_request(request, &client).await
|
||||
}
|
||||
Err(e) => DaemonResponse::err(format!("invalid request: {e}")),
|
||||
};
|
||||
tracing::debug!(?response, "mcp socket response");
|
||||
let mut json = serde_json::to_string(&response)?;
|
||||
json.push('\n');
|
||||
writer.write_all(json.as_bytes()).await?;
|
||||
writer.flush().await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn handle_request(request: DaemonRequest, client: &Client) -> DaemonResponse {
|
||||
match request {
|
||||
DaemonRequest::SendMessage { room_id, body } => send_message(client, &room_id, &body).await,
|
||||
DaemonRequest::SendDm { user_id, body } => send_dm(client, &user_id, &body).await,
|
||||
DaemonRequest::SendReaction {
|
||||
room_id,
|
||||
event_id,
|
||||
key,
|
||||
} => send_reaction(client, &room_id, &event_id, &key).await,
|
||||
DaemonRequest::ListRooms {} => list_rooms(client).await,
|
||||
DaemonRequest::ListRoomMembers { room_id } => list_room_members(client, &room_id).await,
|
||||
}
|
||||
}
|
||||
|
||||
async fn send_message(client: &Client, room_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 content = RoomMessageEventContent::text_plain(body);
|
||||
match room.send(content).await {
|
||||
Ok(_) => {
|
||||
tracing::info!(room = %rid, "mcp: sent message");
|
||||
DaemonResponse::ok(format!("sent to {rid}"))
|
||||
}
|
||||
Err(e) => DaemonResponse::err(format!("send failed: {e}")),
|
||||
}
|
||||
}
|
||||
|
||||
async fn send_dm(client: &Client, user_id: &str, body: &str) -> DaemonResponse {
|
||||
let uid = match user_id.parse::<OwnedUserId>() {
|
||||
Ok(u) => u,
|
||||
Err(e) => return DaemonResponse::err(format!("invalid user_id: {e}")),
|
||||
};
|
||||
let room = match handlers::find_or_create_dm(client, &uid).await {
|
||||
Ok(r) => r,
|
||||
Err(e) => return DaemonResponse::err(format!("failed to get/create DM: {e}")),
|
||||
};
|
||||
let content = RoomMessageEventContent::text_plain(body);
|
||||
match room.send(content).await {
|
||||
Ok(_) => {
|
||||
tracing::info!(user = %uid, "mcp: sent DM");
|
||||
DaemonResponse::ok(format!("DM sent to {uid}"))
|
||||
}
|
||||
Err(e) => DaemonResponse::err(format!("send DM failed: {e}")),
|
||||
}
|
||||
}
|
||||
|
||||
async fn send_reaction(
|
||||
client: &Client,
|
||||
room_id: &str,
|
||||
event_id: &str,
|
||||
key: &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()),
|
||||
};
|
||||
|
||||
// Load timeline to resolve possibly-shortened event id
|
||||
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 = ReactionEventContent::new(Annotation::new(full_eid.clone(), key.to_owned()));
|
||||
match room.send(content).await {
|
||||
Ok(_) => {
|
||||
tracing::info!(target = %full_eid, %key, "mcp: sent reaction");
|
||||
DaemonResponse::ok(format!("reacted {key} to {full_eid}"))
|
||||
}
|
||||
Err(e) => DaemonResponse::err(format!("send reaction failed: {e}")),
|
||||
}
|
||||
}
|
||||
|
||||
async fn list_rooms(client: &Client) -> DaemonResponse {
|
||||
let mut rooms = Vec::new();
|
||||
for room in client.joined_rooms() {
|
||||
let name = room
|
||||
.display_name()
|
||||
.await
|
||||
.map_or_else(|_| room.room_id().to_string(), |n| n.to_string());
|
||||
rooms.push(json!({
|
||||
"room_id": room.room_id().as_str(),
|
||||
"name": name,
|
||||
}));
|
||||
}
|
||||
DaemonResponse::ok(rooms)
|
||||
}
|
||||
|
||||
async fn list_room_members(client: &Client, room_id: &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 members = match room.members(matrix_sdk::RoomMemberships::JOIN).await {
|
||||
Ok(m) => m,
|
||||
Err(e) => return DaemonResponse::err(format!("failed to list members: {e}")),
|
||||
};
|
||||
let list: Vec<_> = members
|
||||
.iter()
|
||||
.map(|m| {
|
||||
json!({
|
||||
"user_id": m.user_id().as_str(),
|
||||
"display_name": m.display_name().unwrap_or_default(),
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
DaemonResponse::ok(list)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue