use rmcp::{
ErrorData, Json, handler::server::wrapper::Parameters, service::RequestContext, tool,
tool_router,
};
use schemars::JsonSchema;
use serde::Deserialize;
use super::{Bus, auth_of};
use crate::{
model::{AgentInfo, AgentList, SessionList},
store::presence,
};
#[derive(Debug, Deserialize, JsonSchema)]
pub struct HeartbeatArgs {
#[serde(default)]
pub status: Option<String>,
#[serde(default)]
pub repo: Option<String>,
#[serde(default)]
pub branch: Option<String>,
#[serde(default)]
pub activity: Option<String>,
#[serde(default)]
pub project: Option<String>,
#[serde(default)]
pub role: Option<String>,
#[serde(default)]
pub ttl_seconds: Option<i64>,
}
#[derive(Debug, Deserialize, JsonSchema)]
pub struct ListSessionsArgs {
#[serde(default)]
pub project: Option<String>,
#[serde(default)]
pub role: Option<String>,
#[serde(default)]
pub online_only: bool,
#[serde(default)]
pub limit: Option<i64>,
}
#[derive(Debug, Deserialize, JsonSchema)]
pub struct ListAgentsArgs {
#[serde(default)]
pub online_only: bool,
}
#[tool_router(router = presence_router, vis = "pub")]
impl Bus {
#[tool(
description = "Publish what you are currently working on so teammates' agents can \
see it. Call this when you start a piece of work and whenever the \
focus changes. Omitted fields keep their previous value."
)]
async fn heartbeat(
&self,
ctx: RequestContext<rmcp::RoleServer>,
Parameters(args): Parameters<HeartbeatArgs>,
) -> Result<Json<AgentInfo>, ErrorData> {
let auth = auth_of(&ctx)?;
let input = presence::HeartbeatInput {
status: args.status,
repo: args.repo,
branch: args.branch,
activity: args.activity,
project: args.project,
role: args.role,
ttl_seconds: args.ttl_seconds,
};
Ok(Json(presence::heartbeat(&self.db, &auth, input).await?))
}
#[tool(
description = "See who else is on the bus, whether they are online, and what each \
one is working on. Useful before claiming work or asking a question."
)]
async fn list_agents(
&self,
ctx: RequestContext<rmcp::RoleServer>,
Parameters(args): Parameters<ListAgentsArgs>,
) -> Result<Json<AgentList>, ErrorData> {
let auth = auth_of(&ctx)?;
Ok(Json(
presence::list_agents(&self.db, &auth, args.online_only).await?,
))
}
#[tool(
description = "Find the window to talk to. Lists every session in your team \
with its address (`agent/session`), project and role, so you can \
reach 'the design window of market-data' rather than guessing. \
Filter by project and/or role; several sessions may share both \
(two reviewers), and each keeps its own address — pick one, never \
broadcast a private instruction to all of them. Use the address in \
post_message `to` or ask_agent `to`. CHECK `exact` FIRST: it is \
false for the shared session, whose address is the bare agent name \
and reaches EVERY window that agent has, so it is never a private \
target. Labels are what a session said about itself, not proof of \
anything."
)]
async fn list_sessions(
&self,
ctx: RequestContext<rmcp::RoleServer>,
Parameters(args): Parameters<ListSessionsArgs>,
) -> Result<Json<SessionList>, ErrorData> {
let auth = auth_of(&ctx)?;
Ok(Json(
presence::list_sessions(
&self.db,
&auth,
presence::SessionFilter {
project: args.project,
role: args.role,
online_only: args.online_only,
limit: args.limit,
},
)
.await?,
))
}
}