use rmcp::handler::server::tool::ToolRouter;
use rmcp::handler::server::wrapper::Parameters;
use rmcp::{schemars, tool, tool_router};
use super::client::TmaiHttpClient;
#[derive(Debug)]
pub struct TmaiMcpServer {
pub tool_router: ToolRouter<Self>,
client: TmaiHttpClient,
}
impl TmaiMcpServer {
pub fn new(client: TmaiHttpClient) -> Self {
Self {
tool_router: Self::tool_router(),
client,
}
}
}
#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
pub struct EmptyParams {}
#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
pub struct AgentIdParams {
pub id: String,
}
#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
pub struct SendTextParams {
pub id: String,
pub text: String,
}
#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
pub struct SendKeyParams {
pub id: String,
pub key: String,
}
#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
pub struct SelectChoiceParams {
pub id: String,
pub index: u32,
}
#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
pub struct RepoParams {
#[serde(default)]
pub repo: Option<String>,
}
#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
pub struct BranchParams {
pub branch: String,
#[serde(default)]
pub repo: Option<String>,
}
#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
pub struct PrNumberParams {
pub pr_number: u32,
#[serde(default)]
pub repo: Option<String>,
}
#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
pub struct SpawnAgentParams {
pub directory: String,
#[serde(default)]
pub prompt: Option<String>,
}
#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
pub struct SpawnWorktreeParams {
pub name: String,
#[serde(default)]
pub repo: Option<String>,
#[serde(default)]
pub base_branch: Option<String>,
#[serde(default)]
pub prompt: Option<String>,
}
#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
pub struct WorktreeDeleteParams {
pub worktree_name: String,
#[serde(default)]
pub repo: Option<String>,
#[serde(default)]
pub force: bool,
}
#[tool_router]
impl TmaiMcpServer {
#[tool(description = "List all monitored AI agents with their status")]
fn list_agents(&self, Parameters(_): Parameters<EmptyParams>) -> String {
match self.client.get::<serde_json::Value>("/agents") {
Ok(agents) => format_json(&agents),
Err(e) => format!("Error: {e}"),
}
}
#[tool(description = "Get detailed info about a specific agent")]
fn get_agent(&self, Parameters(p): Parameters<AgentIdParams>) -> String {
match self.client.get::<serde_json::Value>("/agents") {
Ok(data) => {
if let Some(agents) = data.as_array() {
if let Some(agent) = agents.iter().find(|a| {
a.get("id").and_then(|v| v.as_str()) == Some(&p.id)
|| a.get("pty_session_id").and_then(|v| v.as_str()) == Some(&p.id)
}) {
return format_json(agent);
}
}
format!("Agent not found: {}", p.id)
}
Err(e) => format!("Error: {e}"),
}
}
#[tool(description = "Get the terminal output of an agent")]
fn get_agent_output(&self, Parameters(p): Parameters<AgentIdParams>) -> String {
match self.client.get_text(&format!("/agents/{}/output", p.id)) {
Ok(text) => text,
Err(e) => format!("Error: {e}"),
}
}
#[tool(description = "Get the conversation transcript of an agent")]
fn get_transcript(&self, Parameters(p): Parameters<AgentIdParams>) -> String {
match self
.client
.get::<serde_json::Value>(&format!("/agents/{}/transcript", p.id))
{
Ok(data) => format_json(&data),
Err(e) => format!("Error: {e}"),
}
}
#[tool(description = "Approve a pending permission request for an agent")]
fn approve(&self, Parameters(p): Parameters<AgentIdParams>) -> String {
match self
.client
.post_ok(&format!("/agents/{}/approve", p.id), &serde_json::json!({}))
{
Ok(()) => format!("Approved agent {}", p.id),
Err(e) => format!("Error: {e}"),
}
}
#[tool(description = "Send text input to an agent")]
fn send_text(&self, Parameters(p): Parameters<SendTextParams>) -> String {
match self.client.post_ok(
&format!("/agents/{}/input", p.id),
&serde_json::json!({"text": p.text}),
) {
Ok(()) => format!("Sent text to agent {}", p.id),
Err(e) => format!("Error: {e}"),
}
}
#[tool(description = "Send a special key to an agent")]
fn send_key(&self, Parameters(p): Parameters<SendKeyParams>) -> String {
match self.client.post_ok(
&format!("/agents/{}/key", p.id),
&serde_json::json!({"key": p.key}),
) {
Ok(()) => format!("Sent key '{}' to agent {}", p.key, p.id),
Err(e) => format!("Error: {e}"),
}
}
#[tool(description = "Select a choice for an agent's question")]
fn select_choice(&self, Parameters(p): Parameters<SelectChoiceParams>) -> String {
match self.client.post_ok(
&format!("/agents/{}/select", p.id),
&serde_json::json!({"index": p.index}),
) {
Ok(()) => format!("Selected choice {} for agent {}", p.index, p.id),
Err(e) => format!("Error: {e}"),
}
}
#[tool(description = "List all agent teams")]
fn list_teams(&self, Parameters(_): Parameters<EmptyParams>) -> String {
match self.client.get::<serde_json::Value>("/teams") {
Ok(teams) => format_json(&teams),
Err(e) => format!("Error: {e}"),
}
}
#[tool(description = "List all git worktrees")]
fn list_worktrees(&self, Parameters(_): Parameters<EmptyParams>) -> String {
match self.client.get::<serde_json::Value>("/worktrees") {
Ok(wt) => format_json(&wt),
Err(e) => format!("Error: {e}"),
}
}
#[tool(description = "Spawn a new AI agent in a directory")]
fn spawn_agent(&self, Parameters(p): Parameters<SpawnAgentParams>) -> String {
let mut body = serde_json::json!({"directory": p.directory});
if let Some(prompt) = &p.prompt {
body["initial_prompt"] = serde_json::json!(prompt);
}
match self.client.post::<serde_json::Value>("/spawn", &body) {
Ok(data) => format_json(&data),
Err(e) => format!("Error: {e}"),
}
}
#[tool(description = "Create a worktree and spawn an agent in it")]
fn spawn_worktree(&self, Parameters(p): Parameters<SpawnWorktreeParams>) -> String {
let cwd = match self.client.resolve_repo(&p.repo) {
Ok(r) => r,
Err(e) => return format!("Error: {e}"),
};
let mut body = serde_json::json!({"name": p.name, "cwd": cwd});
if let Some(base) = &p.base_branch {
body["base_branch"] = serde_json::json!(base);
}
if let Some(prompt) = &p.prompt {
body["initial_prompt"] = serde_json::json!(prompt);
}
match self
.client
.post::<serde_json::Value>("/spawn/worktree", &body)
{
Ok(data) => format_json(&data),
Err(e) => format!("Error: {e}"),
}
}
#[tool(description = "Delete a git worktree")]
fn delete_worktree(&self, Parameters(p): Parameters<WorktreeDeleteParams>) -> String {
let repo = match self.client.resolve_repo(&p.repo) {
Ok(r) => r,
Err(e) => return format!("Error: {e}"),
};
let repo_path = if repo.ends_with(".git") {
repo.clone()
} else {
format!("{}/.git", repo)
};
match self.client.post_ok(
"/worktrees/delete",
&serde_json::json!({
"repo_path": repo_path,
"worktree_name": p.worktree_name,
"force": p.force
}),
) {
Ok(()) => format!("Deleted worktree: {}", p.worktree_name),
Err(e) => format!("Error: {e}"),
}
}
#[tool(description = "List open pull requests")]
fn list_prs(&self, Parameters(p): Parameters<RepoParams>) -> String {
let repo = match self.client.resolve_repo(&p.repo) {
Ok(r) => r,
Err(e) => return format!("Error: {e}"),
};
match self
.client
.get::<serde_json::Value>(&format!("/github/prs?repo={}", encode(&repo)))
{
Ok(prs) => format_json(&prs),
Err(e) => format!("Error: {e}"),
}
}
#[tool(description = "List open issues")]
fn list_issues(&self, Parameters(p): Parameters<RepoParams>) -> String {
let repo = match self.client.resolve_repo(&p.repo) {
Ok(r) => r,
Err(e) => return format!("Error: {e}"),
};
match self
.client
.get::<serde_json::Value>(&format!("/github/issues?repo={}", encode(&repo)))
{
Ok(issues) => format_json(&issues),
Err(e) => format!("Error: {e}"),
}
}
#[tool(description = "Get CI check results for a branch")]
fn get_ci_status(&self, Parameters(p): Parameters<BranchParams>) -> String {
let repo = match self.client.resolve_repo(&p.repo) {
Ok(r) => r,
Err(e) => return format!("Error: {e}"),
};
match self.client.get::<serde_json::Value>(&format!(
"/github/checks?branch={}&repo={}",
encode(&p.branch),
encode(&repo)
)) {
Ok(checks) => format_json(&checks),
Err(e) => format!("Error: {e}"),
}
}
#[tool(description = "Get PR comments and reviews")]
fn get_pr_comments(&self, Parameters(p): Parameters<PrNumberParams>) -> String {
let repo = match self.client.resolve_repo(&p.repo) {
Ok(r) => r,
Err(e) => return format!("Error: {e}"),
};
match self.client.get::<serde_json::Value>(&format!(
"/github/pr/comments?pr={}&repo={}",
p.pr_number,
encode(&repo)
)) {
Ok(data) => format_json(&data),
Err(e) => format!("Error: {e}"),
}
}
#[tool(description = "Get PR merge status")]
fn get_pr_merge_status(&self, Parameters(p): Parameters<PrNumberParams>) -> String {
let repo = match self.client.resolve_repo(&p.repo) {
Ok(r) => r,
Err(e) => return format!("Error: {e}"),
};
match self.client.get::<serde_json::Value>(&format!(
"/github/pr/merge-status?pr={}&repo={}",
p.pr_number,
encode(&repo)
)) {
Ok(data) => format_json(&data),
Err(e) => format!("Error: {e}"),
}
}
#[tool(description = "Get CI failure log for a branch")]
fn get_ci_failure_log(&self, Parameters(p): Parameters<BranchParams>) -> String {
let repo = match self.client.resolve_repo(&p.repo) {
Ok(r) => r,
Err(e) => return format!("Error: {e}"),
};
match self.client.get_text(&format!(
"/github/ci/failure-log?branch={}&repo={}",
encode(&p.branch),
encode(&repo)
)) {
Ok(log) => log,
Err(e) => format!("Error: {e}"),
}
}
#[tool(description = "Rerun failed CI checks")]
fn rerun_ci(&self, Parameters(p): Parameters<BranchParams>) -> String {
let repo = match self.client.resolve_repo(&p.repo) {
Ok(r) => r,
Err(e) => return format!("Error: {e}"),
};
match self.client.post_ok(
"/github/ci/rerun",
&serde_json::json!({"branch": p.branch, "repo": repo}),
) {
Ok(()) => format!("Rerunning failed checks for branch: {}", p.branch),
Err(e) => format!("Error: {e}"),
}
}
#[tool(description = "List git branches")]
fn list_branches(&self, Parameters(p): Parameters<RepoParams>) -> String {
let repo = match self.client.resolve_repo(&p.repo) {
Ok(r) => r,
Err(e) => return format!("Error: {e}"),
};
match self
.client
.get::<serde_json::Value>(&format!("/git/branches?repo={}", encode(&repo)))
{
Ok(branches) => format_json(&branches),
Err(e) => format!("Error: {e}"),
}
}
#[tool(description = "Get diff stats for a branch")]
fn git_diff_stat(&self, Parameters(p): Parameters<BranchParams>) -> String {
let repo = match self.client.resolve_repo(&p.repo) {
Ok(r) => r,
Err(e) => return format!("Error: {e}"),
};
match self.client.get::<serde_json::Value>(&format!(
"/git/diff-stat?branch={}&repo={}",
encode(&p.branch),
encode(&repo)
)) {
Ok(data) => format_json(&data),
Err(e) => format!("Error: {e}"),
}
}
}
fn encode(s: &str) -> String {
s.replace('%', "%25")
.replace(' ', "%20")
.replace('#', "%23")
.replace('&', "%26")
.replace('=', "%3D")
.replace('+', "%2B")
}
fn format_json(value: &serde_json::Value) -> String {
serde_json::to_string_pretty(value).unwrap_or_else(|_| value.to_string())
}