1#![deny(missing_docs)]
30#![deny(unsafe_code)]
31#![warn(clippy::all)]
32#![warn(clippy::pedantic)]
33#![warn(clippy::nursery)]
34#![allow(clippy::module_name_repetitions)]
35
36pub use config::Config;
38pub use error::{Error, Result};
39
40pub mod cli;
42pub mod config;
43pub mod error;
44mod mcp;
45mod outline;
46mod tools;
47
48pub async fn run_stdio(config: Config) -> Result<()> {
56 use std::io::{self, Write};
57 use tracing::{debug, error};
58
59 let stdin = io::stdin();
60 let mut stdout = io::stdout();
61
62 let outline_client = outline::Client::new(config.outline_api_key, config.outline_api_url)?;
64
65 debug!("✅ STDIO server ready");
66
67 loop {
69 let input = {
70 let mut line = String::new();
71 match stdin.read_line(&mut line) {
72 Ok(0) => break, Ok(_) => line.trim_end().to_string(),
74 Err(e) => {
75 error!("Error reading STDIN: {}", e);
76 break;
77 }
78 }
79 };
80
81 if input.trim().is_empty() {
82 continue;
83 }
84
85 match mcp::handle_request(&input, &outline_client).await {
87 Ok(Some(response)) => {
88 writeln!(stdout, "{response}")?;
89 stdout.flush()?;
90 }
91 Ok(None) => {
92 }
94 Err(e) => {
95 error!("Error processing request: {}", e);
96 let error_response = mcp::create_error_response(&e);
97 writeln!(stdout, "{error_response}")?;
98 stdout.flush()?;
99 }
100 }
101 }
102
103 Ok(())
104}
105
106pub async fn run_http(config: Config) -> Result<()> {
114 use tokio::net::TcpListener;
115 use tracing::{debug, error, info};
116
117 let addr = format!("{}:{}", config.http_host, config.http_port.as_u16());
118 let listener = TcpListener::bind(&addr).await?;
119
120 info!("🌐 HTTP server started on {}", addr);
121 info!("📡 Available at /mcp for MCP requests");
122
123 let outline_client = outline::Client::new(config.outline_api_key, config.outline_api_url)?;
125
126 loop {
127 match listener.accept().await {
128 Ok((stream, addr)) => {
129 debug!("🔗 New connection: {}", addr);
130 let client = outline_client.clone();
131
132 tokio::spawn(async move {
133 if let Err(e) = handle_http_connection(stream, client).await {
134 error!("Error handling HTTP connection: {}", e);
135 }
136 });
137 }
138 Err(e) => {
139 error!("Error accepting connection: {}", e);
140 }
141 }
142 }
143}
144
145async fn handle_http_connection(
147 mut stream: tokio::net::TcpStream,
148 outline_client: outline::Client,
149) -> Result<()> {
150 use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
151
152 let mut reader = BufReader::new(&mut stream);
153 let mut request_line = String::new();
154 reader.read_line(&mut request_line).await?;
155
156 if request_line.starts_with("POST /mcp") {
158 let mut content_length = 0;
160 loop {
161 let mut line = String::new();
162 reader.read_line(&mut line).await?;
163
164 if line.trim().is_empty() {
165 break;
166 }
167
168 if line.to_lowercase().starts_with("content-length:") {
169 if let Some(len_str) = line.split(':').nth(1) {
170 content_length = len_str.trim().parse().unwrap_or(0);
171 }
172 }
173 }
174
175 if content_length > 0 {
177 let mut buffer = vec![0; content_length];
178 tokio::io::AsyncReadExt::read_exact(&mut reader, &mut buffer).await?;
179 let body = String::from_utf8(buffer)?;
180
181 match mcp::handle_request(&body, &outline_client).await {
183 Ok(Some(response)) => {
184 let http_response = format!(
185 "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: {}\r\n\r\n{}",
186 response.len(),
187 response
188 );
189 stream.write_all(http_response.as_bytes()).await?;
190 }
191 Ok(None) => {
192 let http_response = "HTTP/1.1 204 No Content\r\n\r\n";
194 stream.write_all(http_response.as_bytes()).await?;
195 }
196 Err(e) => {
197 let error_response = mcp::create_error_response(&e);
198 let http_response = format!(
199 "HTTP/1.1 500 Internal Server Error\r\nContent-Type: application/json\r\nContent-Length: {}\r\n\r\n{}",
200 error_response.len(),
201 error_response
202 );
203 stream.write_all(http_response.as_bytes()).await?;
204 }
205 }
206 }
207 } else {
208 let response = "HTTP/1.1 404 Not Found\r\nContent-Length: 0\r\n\r\n";
210 stream.write_all(response.as_bytes()).await?;
211 }
212
213 Ok(())
214}
215
216#[cfg(test)]
217mod tests {
218 use super::*;
219
220 #[test]
221 fn test_config_creation() {
222 let config = Config::for_testing();
223 assert!(config.validate().is_ok());
224 }
225
226 #[test]
227 fn test_error_types() {
228 let _error = Error::Config {
229 message: "test error".to_string(),
230 source: None,
231 };
232
233 }
236}