hyperhive/hive-matrix-mcp/src/protocol.rs

78 lines
3.3 KiB
Rust

//! Shared response/DTO shapes for the matrix tool surface.
//!
//! `hive-matrix-daemon` serves its MCP tools directly over
//! streamable-http (see [`crate::mcp`]) — there is no separate bridge
//! process and no wire protocol between two binaries any more, so this
//! module carries only the handler-facing result type
//! ([`DaemonResponse`]) and small DTOs ([`InviteAction`],
//! [`RoomUnread`]) shared between [`crate::handlers`] and its callers
//! (the MCP tool router, the wake-signal formatter).
use serde::{Deserialize, Serialize};
/// Whether to accept or reject a pending invite (`resolve_invite`
/// tool). Serialises as `"accept"` / `"reject"` on the wire (kept
/// `Serialize`/`Deserialize` for the JSON DTOs handlers build).
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum InviteAction {
/// Accept the invite — join the room (same effect as `join_room`).
Accept,
/// Reject the invite — decline it and leave the room.
Reject,
}
/// One entry in the `unread_summary` response payload.
#[derive(Debug, Serialize, Deserialize)]
pub struct RoomUnread {
/// Canonical alias (`#name:server`) or room id (`!id:server`).
pub label: String,
/// Always `1` — an exact unread-message count is no longer tracked
/// (see `handlers::room_unread_state`'s doc comment for why); the
/// agent reads the room via `read_room` for full context, so only
/// "there's something new" plus a one-message preview matters here.
pub count: u32,
/// Truncated body of the room's latest (unread) event. Present when
/// that event is a text-like message; absent for a reaction/state
/// event or when the fetch failed.
#[serde(skip_serializing_if = "Option::is_none")]
pub last_body: Option<String>,
/// Sender of the last message (`@user:server`). Present when
/// `last_body` is present.
#[serde(skip_serializing_if = "Option::is_none")]
pub last_sender: Option<String>,
}
/// Result shape every [`crate::handlers`] function returns: `Ok`
/// carries the payload (any JSON; the MCP tool router renders it as
/// the tool result string), `Error` carries a human-readable error
/// string. Kept as a distinct type (rather than each handler
/// returning a bare `String`) so the tool router can uniformly render
/// success vs error without every handler duplicating that
/// formatting.
#[derive(Debug, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum DaemonResponse {
Ok { payload: serde_json::Value },
Error { message: String },
}
impl DaemonResponse {
/// Convenience: build an Ok response from any serializable value.
/// Serialisation failure (impossible for the small handler structs
/// we use, but the API is generic) surfaces as a structured
/// `{"serialise_error": "..."}` payload so callers see WHY the
/// data is missing instead of a silent `null`.
pub fn ok<T: Serialize>(payload: &T) -> Self {
let payload = serde_json::to_value(payload)
.unwrap_or_else(|e| serde_json::json!({ "serialise_error": e.to_string() }));
Self::Ok { payload }
}
/// Convenience: build an Error response from any `Display` value.
pub fn error(msg: impl std::fmt::Display) -> Self {
Self::Error {
message: msg.to_string(),
}
}
}