supercode_harness/
mail_mcp.rs1use async_trait::async_trait;
17use serde_json::{json, Value};
18
19use crate::error::{Error, Result};
20use crate::mail_route::{process_ancestry, resolve_caller, LiveSessions};
21use crate::mail_send::SendOptions;
22use crate::mailbox::{mail_root, Mailbox};
23use crate::tools::{Tool, ToolContext, ToolRegistry};
24use crate::HarnessHomes;
25
26pub fn registry() -> ToolRegistry {
28 let mut registry = ToolRegistry::new();
29 registry.register(ListAgents);
30 registry.register(SendMessage);
31 registry.register(ReadMessages);
32 registry
33}
34
35fn caller(tool: &str, homes: &HarnessHomes) -> Result<crate::mail_route::Caller> {
36 resolve_caller(homes, &process_ancestry()).map_err(|message| Error::tool(tool, message))
37}
38
39struct ListAgents;
40
41#[async_trait]
42impl Tool for ListAgents {
43 fn name(&self) -> &str {
44 "list_agents"
45 }
46
47 fn description(&self) -> &str {
48 "List the coding-agent sessions you can message, of every harness (Claude Code, Codex, \
49 sessions supercode hosts): each one's name, harness, status (busy, idle, hosted), how it \
50 is reached, and address. `query` keeps the ones whose name or address contains it."
51 }
52
53 fn parameters(&self) -> Value {
54 json!({
55 "type": "object",
56 "properties": {
57 "query": {"type": "string", "description": "Keep sessions whose name or address contains this."}
58 },
59 "additionalProperties": false
60 })
61 }
62
63 async fn execute(&self, args: Value, _ctx: &ToolContext) -> Result<String> {
64 let query = args
65 .get("query")
66 .and_then(Value::as_str)
67 .map(str::to_lowercase);
68 let sessions = LiveSessions::read(&HarnessHomes::default());
69 let rows: Vec<String> = sessions
70 .all()
71 .iter()
72 .filter(|session| {
73 query.as_deref().is_none_or(|query| {
74 session.name.to_lowercase().contains(query)
75 || session.address.to_string().to_lowercase().contains(query)
76 })
77 })
78 .map(|session| {
79 format!(
80 "{} ({}, {}, {}) {}",
81 session.name,
82 session.address.harness,
83 session.status,
84 session.door.name(),
85 session.address
86 )
87 })
88 .collect();
89 Ok(if rows.is_empty() {
90 "No session to message matches.".into()
91 } else {
92 rows.join("\n")
93 })
94 }
95}
96
97struct SendMessage;
98
99#[async_trait]
100impl Tool for SendMessage {
101 fn name(&self) -> &str {
102 "send_message"
103 }
104
105 fn description(&self) -> &str {
106 "Send a message to another coding-agent session, of any harness, on this machine or \
107 another of your team's (`name@machine`). `to` is a name from list_agents, `name@machine`, \
108 or an address. To answer a message, send to its `from` with `reply_to` set to its id. \
109 The result says whether it was delivered, queued, held for approval, refused or stored."
110 }
111
112 fn parameters(&self) -> Value {
113 json!({
114 "type": "object",
115 "properties": {
116 "to": {"type": "string", "description": "A session name, name@machine, or address."},
117 "message": {"type": "string", "description": "The message."},
118 "reply_to": {"type": "string", "description": "Id of the message this answers."},
119 "notify_when_idle": {"type": "boolean", "description": "Also get one notice when the receiver's next turn ends."}
120 },
121 "required": ["to", "message"],
122 "additionalProperties": false
123 })
124 }
125
126 async fn execute(&self, args: Value, _ctx: &ToolContext) -> Result<String> {
127 let text = |key: &str| args.get(key).and_then(Value::as_str).map(str::to_string);
128 let (Some(to), Some(message)) = (text("to"), text("message")) else {
129 return Err(Error::tool(self.name(), "`to` and `message` are required"));
130 };
131 if message.trim().is_empty() {
132 return Err(Error::tool(
133 self.name(),
134 "Nothing was sent: the message is empty.",
135 ));
136 }
137 let homes = HarnessHomes::default();
138 let caller = caller(self.name(), &homes)?;
139 let options = SendOptions {
140 in_reply_to: text("reply_to"),
141 notify_when_idle: args
142 .get("notify_when_idle")
143 .and_then(Value::as_bool)
144 .unwrap_or(false),
145 ..Default::default()
146 };
147 let outcome = crate::mail_send::send(&homes, &caller, &to, &message, options)
148 .await
149 .map_err(|error| Error::tool(self.name(), error.to_string()))?;
150 if outcome.code == 0 {
151 Ok(outcome.text)
152 } else {
153 Err(Error::tool(self.name(), outcome.text))
154 }
155 }
156}
157
158struct ReadMessages;
159
160#[async_trait]
161impl Tool for ReadMessages {
162 fn name(&self) -> &str {
163 "read_messages"
164 }
165
166 fn description(&self) -> &str {
167 "Read your unread messages from other sessions, each with how to answer it. Messages \
168 usually arrive in your conversation by themselves; this reads any that are waiting."
169 }
170
171 fn parameters(&self) -> Value {
172 json!({"type": "object", "properties": {}, "additionalProperties": false})
173 }
174
175 async fn execute(&self, _args: Value, _ctx: &ToolContext) -> Result<String> {
176 let homes = HarnessHomes::default();
177 let caller = caller(self.name(), &homes)?;
178 let mailbox = Mailbox::open(&mail_root(), &caller.address)
179 .map_err(|error| Error::tool(self.name(), error.to_string()))?;
180 let claimed = mailbox
181 .claim_unread()
182 .map_err(|error| Error::tool(self.name(), error.to_string()))?;
183 let text = if claimed.is_empty() {
184 "No unread messages.".to_string()
185 } else {
186 claimed
187 .iter()
188 .map(|stored| stored.envelope.render())
189 .collect::<Vec<_>>()
190 .join("\n\n")
191 };
192 for stored in &claimed {
193 mailbox
194 .acknowledge(stored)
195 .map_err(|error| Error::tool(self.name(), error.to_string()))?;
196 }
197 Ok(text)
198 }
199}