1use axum::{
5 extract::State,
6 http::StatusCode,
7 response::IntoResponse,
8 routing::{get, post},
9 Json, Router,
10};
11use serde::{Deserialize, Serialize};
12use serde_json::Value;
13use std::sync::Arc;
14use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
15use tokio::sync::mpsc;
16
17use crate::agent::AgentRunner;
18use crate::channels::traits::{Channel, IncomingMessage, OutgoingMessage};
19use crate::tools::traits::{Tool, ToolSpec};
20
21#[derive(Debug, Deserialize)]
22struct JsonRpcRequest {
23 #[allow(dead_code)]
24 jsonrpc: String,
25 id: Option<Value>,
26 method: String,
27 #[serde(default)]
28 params: Option<Value>,
29}
30
31#[derive(Debug, Serialize)]
32struct JsonRpcResponse {
33 jsonrpc: String,
34 id: Option<Value>,
35 #[serde(skip_serializing_if = "Option::is_none")]
36 result: Option<Value>,
37 #[serde(skip_serializing_if = "Option::is_none")]
38 error: Option<JsonRpcErrorObj>,
39}
40
41#[derive(Debug, Serialize)]
42struct JsonRpcErrorObj {
43 code: i32,
44 message: String,
45 #[serde(skip_serializing_if = "Option::is_none")]
46 data: Option<Value>,
47}
48
49impl JsonRpcResponse {
50 fn success(id: Option<Value>, result: Value) -> Self {
51 Self {
52 jsonrpc: "2.0".to_string(),
53 id,
54 result: Some(result),
55 error: None,
56 }
57 }
58
59 fn error(id: Option<Value>, code: i32, message: impl Into<String>) -> Self {
60 Self {
61 jsonrpc: "2.0".to_string(),
62 id,
63 result: None,
64 error: Some(JsonRpcErrorObj {
65 code,
66 message: message.into(),
67 data: None,
68 }),
69 }
70 }
71}
72
73fn tool_to_mcp_schema(spec: &ToolSpec) -> Value {
74 serde_json::json!({
75 "name": spec.name,
76 "description": spec.description,
77 "inputSchema": spec.parameters
78 })
79}
80
81struct HttpChannel;
83
84#[async_trait::async_trait]
85impl Channel for HttpChannel {
86 fn name(&self) -> &str {
87 "http"
88 }
89 async fn start(&mut self) -> anyhow::Result<mpsc::Receiver<IncomingMessage>> {
90 let (_tx, rx) = mpsc::channel(1);
91 Ok(rx)
92 }
93 async fn send(&self, _msg: OutgoingMessage) -> anyhow::Result<Option<String>> {
94 Ok(None)
95 }
96 async fn stop(&mut self) -> anyhow::Result<()> {
97 Ok(())
98 }
99}
100
101struct TelegramHttpChannel {
104 token: String,
105 client: reqwest::Client,
106 last_edit: std::sync::Mutex<Option<std::time::Instant>>,
108}
109
110impl TelegramHttpChannel {
111 fn new(token: String) -> Self {
112 Self {
113 token,
114 client: crate::http::shared(),
115 last_edit: std::sync::Mutex::new(None),
116 }
117 }
118
119 fn api_url(&self, method: &str) -> String {
120 format!("https://api.telegram.org/bot{}/{}", self.token, method)
121 }
122
123 async fn tg_post(
124 &self,
125 method: &str,
126 body: serde_json::Value,
127 ) -> anyhow::Result<serde_json::Value> {
128 let resp = self
129 .client
130 .post(self.api_url(method))
131 .json(&body)
132 .send()
133 .await?;
134 Ok(resp.json().await?)
135 }
136}
137
138#[async_trait::async_trait]
139impl Channel for TelegramHttpChannel {
140 fn name(&self) -> &str {
141 "telegram-http"
142 }
143
144 async fn start(&mut self) -> anyhow::Result<mpsc::Receiver<IncomingMessage>> {
145 let (_tx, rx) = mpsc::channel(1);
146 Ok(rx)
147 }
148
149 async fn send(&self, msg: OutgoingMessage) -> anyhow::Result<Option<String>> {
150 let body = serde_json::json!({
151 "chat_id": msg.chat_id,
152 "text": msg.text,
153 });
154 let resp = self.tg_post("sendMessage", body).await?;
155 Ok(resp["result"]["message_id"]
156 .as_i64()
157 .map(|id| id.to_string()))
158 }
159
160 fn as_draft(&self) -> Option<&dyn crate::channels::DraftChannel> {
161 Some(self)
162 }
163
164 async fn stop(&mut self) -> anyhow::Result<()> {
165 Ok(())
166 }
167}
168
169#[async_trait::async_trait]
170impl crate::channels::DraftChannel for TelegramHttpChannel {
171 async fn send_draft(&self, chat_id: &str, text: &str) -> anyhow::Result<Option<String>> {
172 let resp = self
173 .tg_post(
174 "sendMessage",
175 serde_json::json!({
176 "chat_id": chat_id,
177 "text": text,
178 }),
179 )
180 .await?;
181 Ok(resp["result"]["message_id"]
182 .as_i64()
183 .map(|id| id.to_string()))
184 }
185
186 async fn update_draft_progress(
187 &self,
188 chat_id: &str,
189 message_id: &str,
190 text: &str,
191 ) -> anyhow::Result<()> {
192 {
194 let mut last = self.last_edit.lock().unwrap();
195 if let Some(t) = *last {
196 if t.elapsed().as_millis() < 1500 {
197 return Ok(());
198 }
199 }
200 *last = Some(std::time::Instant::now());
201 }
202
203 let msg_id: i64 = message_id.parse().unwrap_or(0);
204 let _ = self
205 .tg_post(
206 "editMessageText",
207 serde_json::json!({
208 "chat_id": chat_id,
209 "message_id": msg_id,
210 "text": text,
211 }),
212 )
213 .await;
214 Ok(())
215 }
216
217 async fn finalize_draft(
218 &self,
219 chat_id: &str,
220 message_id: &str,
221 text: &str,
222 ) -> anyhow::Result<()> {
223 let msg_id: i64 = message_id.parse().unwrap_or(0);
224 let html = md_to_telegram_html(text);
225
226 let r = self
228 .tg_post(
229 "editMessageText",
230 serde_json::json!({
231 "chat_id": chat_id,
232 "message_id": msg_id,
233 "text": &html,
234 "parse_mode": "HTML",
235 }),
236 )
237 .await?;
238
239 if !r["ok"].as_bool().unwrap_or(false) {
240 let _ = self
241 .tg_post(
242 "editMessageText",
243 serde_json::json!({
244 "chat_id": chat_id,
245 "message_id": msg_id,
246 "text": strip_html(&html),
247 }),
248 )
249 .await;
250 }
251 Ok(())
252 }
253}
254
255fn md_to_telegram_html(md: &str) -> String {
257 let mut out = Vec::new();
258 let mut in_code = false;
259 let mut code_buf: Vec<&str> = Vec::new();
260
261 for line in md.lines() {
262 if line.starts_with("```") {
263 if !in_code {
264 in_code = true;
265 code_buf.clear();
266 } else {
267 let code = esc_html(&code_buf.join("\n"));
268 out.push(format!("<pre>{}</pre>", code));
269 code_buf.clear();
270 in_code = false;
271 }
272 continue;
273 }
274 if in_code {
275 code_buf.push(line);
276 continue;
277 }
278 if line.starts_with('|') && line.contains("---") {
280 continue;
281 }
282 if line.starts_with('|') {
284 let cells: Vec<&str> = line
285 .split('|')
286 .map(str::trim)
287 .filter(|s| !s.is_empty())
288 .collect();
289 if !cells.is_empty() {
290 out.push(inline_to_html(&cells.join(" • ")));
291 }
292 continue;
293 }
294 if line
296 .chars()
297 .all(|c| c == '-' || c == '*' || c == '_' || c == ' ')
298 && line.len() >= 3
299 {
300 out.push(String::new());
301 continue;
302 }
303 if let Some(rest) = line
305 .strip_prefix("### ")
306 .or_else(|| line.strip_prefix("## "))
307 .or_else(|| line.strip_prefix("# "))
308 {
309 out.push(format!("<b>{}</b>", inline_to_html(rest)));
310 continue;
311 }
312 if let Some(rest) = line
314 .strip_prefix("- ")
315 .or_else(|| line.strip_prefix("* "))
316 .or_else(|| line.strip_prefix("• "))
317 {
318 out.push(format!("• {}", inline_to_html(rest)));
319 continue;
320 }
321 out.push(inline_to_html(line));
322 }
323 if in_code && !code_buf.is_empty() {
324 out.push(format!("<pre>{}</pre>", esc_html(&code_buf.join("\n"))));
325 }
326
327 let joined = out.join("\n");
329 let mut collapsed = String::with_capacity(joined.len());
330 let mut blank_count = 0;
331 for line in joined.lines() {
332 if line.trim().is_empty() {
333 blank_count += 1;
334 if blank_count <= 2 {
335 collapsed.push('\n');
336 }
337 } else {
338 blank_count = 0;
339 collapsed.push_str(line);
340 collapsed.push('\n');
341 }
342 }
343 collapsed.trim().to_string()
344}
345
346fn esc_html(s: &str) -> String {
347 s.replace('&', "&")
348 .replace('<', "<")
349 .replace('>', ">")
350}
351
352fn inline_to_html(text: &str) -> String {
353 let mut out = String::new();
354 let bytes = text.as_bytes();
355 let mut i = 0;
356 while i < bytes.len() {
357 if bytes[i] == b'`' {
359 if let Some(j) = text[i + 1..].find('`') {
360 out.push_str(&format!(
361 "<code>{}</code>",
362 esc_html(&text[i + 1..i + 1 + j])
363 ));
364 i = i + 1 + j + 1;
365 continue;
366 }
367 }
368 if bytes.get(i) == Some(&b'*') && bytes.get(i + 1) == Some(&b'*') {
370 if let Some(j) = text[i + 2..].find("**") {
371 out.push_str(&format!("<b>{}</b>", esc_html(&text[i + 2..i + 2 + j])));
372 i = i + 2 + j + 2;
373 continue;
374 }
375 }
376 if bytes[i] == b'[' {
378 if let Some(j) = text[i + 1..].find("](") {
379 let link_text = &text[i + 1..i + 1 + j];
380 let rest = &text[i + 1 + j + 2..];
381 if let Some(k) = rest.find(')') {
382 let url = &rest[..k];
383 out.push_str(&format!("<a href=\"{}\">{}</a>", url, esc_html(link_text)));
384 i = i + 1 + j + 2 + k + 1;
385 continue;
386 }
387 }
388 }
389 let c = text[i..].chars().next().unwrap_or(' ');
391 out.push_str(&esc_html(&c.to_string()));
392 i += c.len_utf8();
393 }
394 out
395}
396
397fn strip_html(html: &str) -> String {
398 let mut out = String::with_capacity(html.len());
399 let mut in_tag = false;
400 for c in html.chars() {
401 match c {
402 '<' => in_tag = true,
403 '>' => in_tag = false,
404 _ if !in_tag => out.push(c),
405 _ => {}
406 }
407 }
408 out
409}
410
411#[derive(Clone)]
412struct McpState {
413 tools: Arc<Vec<Arc<dyn Tool>>>,
414 provider: Option<Arc<dyn crate::providers::traits::Provider>>,
415 model: Option<String>,
416 runner: Option<Arc<AgentRunner>>,
417}
418
419#[derive(Debug, Deserialize)]
420struct HttpChatRequest {
421 text: String,
422 chat_id: String,
423 telegram_token: Option<String>,
425}
426
427#[derive(Debug, Serialize)]
428struct HttpChatResponse {
429 text: String,
430}
431
432pub async fn run_mcp_server(
434 tools: Vec<Arc<dyn Tool>>,
435 provider: Option<Arc<dyn crate::providers::traits::Provider>>,
436 model: Option<String>,
437) -> anyhow::Result<()> {
438 let stdin = BufReader::new(tokio::io::stdin());
439 let mut stdout = tokio::io::stdout();
440 let mut lines = stdin.lines();
441
442 let tools = Arc::new(tools);
443
444 tracing::info!("MCP server started on stdio");
445
446 while let Ok(Some(line)) = lines.next_line().await {
447 let line = line.trim().to_string();
448 if line.is_empty() {
449 continue;
450 }
451
452 let request: JsonRpcRequest = match serde_json::from_str(&line) {
453 Ok(r) => r,
454 Err(_) => {
455 let resp = JsonRpcResponse::error(None, -32700, "Parse error");
456 write_response(&mut stdout, &resp).await?;
457 continue;
458 }
459 };
460
461 let response = handle_request(&request, &tools, &provider, &model).await;
462 write_response(&mut stdout, &response).await?;
463 }
464
465 Ok(())
466}
467
468async fn health_handler() -> Json<serde_json::Value> {
470 Json(serde_json::json!({
471 "status": "ok",
472 "version": env!("CARGO_PKG_VERSION"),
473 "service": "apollo",
474 "timestamp": chrono::Utc::now().to_rfc3339(),
475 }))
476}
477
478pub async fn run_mcp_server_http(
479 tools: Vec<Arc<dyn Tool>>,
480 provider: Option<Arc<dyn crate::providers::traits::Provider>>,
481 model: Option<String>,
482 runner: Option<Arc<AgentRunner>>,
483 port: u16,
484) -> anyhow::Result<()> {
485 let state = McpState {
486 tools: Arc::new(tools),
487 provider,
488 model,
489 runner,
490 };
491
492 let app = Router::new()
493 .route("/health", get(health_handler))
494 .route("/chat", post(handle_http_chat))
495 .route("/mcp", post(handle_http_mcp))
496 .route("/", post(handle_http_mcp))
497 .with_state(state);
498
499 let addr = std::net::SocketAddr::from(([0, 0, 0, 0], port));
500 tracing::info!("MCP HTTP server listening on {}", addr);
501 eprintln!("MCP HTTP server listening on {}", addr);
502
503 let listener = tokio::net::TcpListener::bind(addr).await?;
504 axum::serve(listener, app).await?;
505
506 Ok(())
507}
508
509async fn handle_http_chat(
510 State(state): State<McpState>,
511 Json(req): Json<HttpChatRequest>,
512) -> (StatusCode, Json<HttpChatResponse>) {
513 let Some(runner) = state.runner else {
514 return (
515 StatusCode::SERVICE_UNAVAILABLE,
516 Json(HttpChatResponse {
517 text: "Agent not initialized".to_string(),
518 }),
519 );
520 };
521
522 let msg = IncomingMessage {
523 id: uuid::Uuid::new_v4().to_string(),
524 sender_id: req.chat_id.clone(),
525 sender_name: None,
526 chat_id: req.chat_id.clone(),
527 text: req.text,
528 is_group: false,
529 reply_to: None,
530 timestamp: chrono::Utc::now(),
531 };
532
533 let result = if let Some(token) = req.telegram_token {
535 let channel = TelegramHttpChannel::new(token);
536 runner.handle_message(&msg, &channel).await
537 } else {
538 runner.handle_message(&msg, &HttpChannel).await
539 };
540
541 match result {
542 Ok(text) => (StatusCode::OK, Json(HttpChatResponse { text })),
543 Err(e) => (
544 StatusCode::INTERNAL_SERVER_ERROR,
545 Json(HttpChatResponse {
546 text: format!("Error: {e}"),
547 }),
548 ),
549 }
550}
551
552async fn handle_http_mcp(
553 State(state): State<McpState>,
554 Json(request): Json<JsonRpcRequest>,
555) -> impl IntoResponse {
556 let response = handle_request(&request, &state.tools, &state.provider, &state.model).await;
557 Json(response)
558}
559
560async fn write_response(
561 stdout: &mut tokio::io::Stdout,
562 response: &JsonRpcResponse,
563) -> anyhow::Result<()> {
564 let json = serde_json::to_string(response)?;
565 stdout.write_all(json.as_bytes()).await?;
566 stdout.write_all(b"\n").await?;
567 stdout.flush().await?;
568 Ok(())
569}
570
571async fn handle_request(
572 req: &JsonRpcRequest,
573 tools: &[Arc<dyn Tool>],
574 provider: &Option<Arc<dyn crate::providers::traits::Provider>>,
575 model: &Option<String>,
576) -> JsonRpcResponse {
577 match req.method.as_str() {
578 "initialize" => handle_initialize(req.id.clone()),
579 "notifications/initialized" => JsonRpcResponse::success(req.id.clone(), Value::Null),
580 "tools/list" => handle_tools_list(req.id.clone(), tools, provider.is_some()),
581 "tools/call" => {
582 handle_tools_call(req.id.clone(), req.params.as_ref(), tools, provider, model).await
583 }
584 "shutdown" => {
585 tracing::info!("MCP server shutting down");
586 JsonRpcResponse::success(req.id.clone(), Value::Null)
587 }
588 _ => JsonRpcResponse::error(req.id.clone(), -32601, "Method not found"),
589 }
590}
591
592fn handle_initialize(id: Option<Value>) -> JsonRpcResponse {
593 JsonRpcResponse::success(
594 id,
595 serde_json::json!({
596 "protocolVersion": "2024-11-05",
597 "capabilities": {
598 "tools": {}
599 },
600 "serverInfo": {
601 "name": "apollo",
602 "version": env!("CARGO_PKG_VERSION")
603 }
604 }),
605 )
606}
607
608fn handle_tools_list(
609 id: Option<Value>,
610 tools: &[Arc<dyn Tool>],
611 has_provider: bool,
612) -> JsonRpcResponse {
613 let mut mcp_tools: Vec<Value> = tools
614 .iter()
615 .map(|t| tool_to_mcp_schema(&t.spec()))
616 .collect();
617
618 if has_provider {
620 mcp_tools.push(serde_json::json!({
621 "name": "ask",
622 "description": "Send a message to the apollo AI agent and get a response. Use this to prompt apollo for complex tasks like coding, research, or analysis.",
623 "inputSchema": {
624 "type": "object",
625 "properties": {
626 "message": {
627 "type": "string",
628 "description": "The message/prompt to send to apollo"
629 }
630 },
631 "required": ["message"]
632 }
633 }));
634 }
635
636 JsonRpcResponse::success(id, serde_json::json!({ "tools": mcp_tools }))
637}
638
639async fn handle_tools_call(
640 id: Option<Value>,
641 params: Option<&Value>,
642 tools: &[Arc<dyn Tool>],
643 provider: &Option<Arc<dyn crate::providers::traits::Provider>>,
644 model: &Option<String>,
645) -> JsonRpcResponse {
646 let params = match params {
647 Some(p) => p,
648 None => {
649 return JsonRpcResponse::error(id, -32602, "Missing params");
650 }
651 };
652
653 let name = match params.get("name").and_then(|n| n.as_str()) {
654 Some(n) => n,
655 None => {
656 return JsonRpcResponse::error(id, -32602, "Missing tool name");
657 }
658 };
659
660 let arguments = params
661 .get("arguments")
662 .cloned()
663 .unwrap_or(serde_json::json!({}));
664
665 if name == "ask" {
667 if let Some(provider) = provider {
668 let message = arguments
669 .get("message")
670 .and_then(|m| m.as_str())
671 .unwrap_or("");
672 let model_name = model.as_deref().unwrap_or("claude-sonnet-4-5");
673
674 match provider.simple_chat(message, model_name).await {
675 Ok(response) => {
676 return JsonRpcResponse::success(
677 id,
678 serde_json::json!({
679 "content": [{"type": "text", "text": response}]
680 }),
681 );
682 }
683 Err(e) => {
684 return JsonRpcResponse::success(
685 id,
686 serde_json::json!({
687 "content": [{"type": "text", "text": format!("Error: {}", e)}],
688 "isError": true
689 }),
690 );
691 }
692 }
693 } else {
694 return JsonRpcResponse::error(id, -32602, "No provider configured for ask tool");
695 }
696 }
697
698 let tool = match tools.iter().find(|t| t.name() == name) {
700 Some(t) => t,
701 None => {
702 return JsonRpcResponse::error(id, -32602, format!("Unknown tool: {}", name));
703 }
704 };
705
706 let args_str = serde_json::to_string(&arguments).unwrap_or_default();
707 match tool.execute(&args_str).await {
708 Ok(result) => JsonRpcResponse::success(
709 id,
710 serde_json::json!({
711 "content": [{"type": "text", "text": result.output}],
712 "isError": result.is_error
713 }),
714 ),
715 Err(e) => JsonRpcResponse::success(
716 id,
717 serde_json::json!({
718 "content": [{"type": "text", "text": format!("Tool error: {}", e)}],
719 "isError": true
720 }),
721 ),
722 }
723}