origin_mcp_http/
transport.rs1use crate::activity::Activity;
8use crate::discovery::Discovery;
9use crate::http::{HttpRequest, HttpResponse, parse_request};
10use crate::{MCP_PATH, Token};
11use origin_domain::{AppError, Result};
12use origin_mcp_core::McpServer;
13use std::net::SocketAddr;
14use std::sync::Arc;
15use tokio::io::{AsyncReadExt, AsyncWriteExt};
16use tokio::net::{TcpListener, TcpStream};
17use tokio_util::sync::CancellationToken;
18
19#[derive(Debug)]
21pub struct HttpTransport {
22 listener: TcpListener,
23 addr: SocketAddr,
24 token: Option<Token>,
25 activity: Activity,
27}
28
29impl HttpTransport {
30 pub async fn bind(token: Option<Token>) -> Result<Self> {
32 let listener = TcpListener::bind(("127.0.0.1", 0))
33 .await
34 .map_err(|error| AppError::internal(format!("cannot bind loopback: {error}")))?;
35 let addr = listener
36 .local_addr()
37 .map_err(|error| AppError::internal(format!("cannot read local address: {error}")))?;
38
39 tracing::debug!(%addr, "mcp http endpoint bound");
40 Ok(Self {
41 listener,
42 addr,
43 token,
44 activity: Activity::default(),
45 })
46 }
47
48 pub fn addr(&self) -> SocketAddr {
49 self.addr
50 }
51
52 pub fn activity(&self) -> Activity {
55 self.activity.clone()
56 }
57
58 pub fn url(&self) -> String {
59 format!("http://{}{}", self.addr, MCP_PATH)
60 }
61
62 pub fn token(&self) -> Option<&Token> {
63 self.token.as_ref()
64 }
65
66 pub fn discovery(&self) -> Discovery {
68 Discovery {
69 url: self.url(),
70 token: self
71 .token
72 .as_ref()
73 .map(|token| token.expose().to_owned())
74 .unwrap_or_default(),
75 }
76 }
77
78 pub async fn serve(self, server: Arc<McpServer>, stop: CancellationToken) -> Result<()> {
85 tracing::info!(addr = %self.addr, "mcp http endpoint serving");
86
87 loop {
88 tokio::select! {
89 _ = stop.cancelled() => break,
90 accepted = self.listener.accept() => {
91 match accepted {
92 Ok((stream, _peer)) => {
93 let server = server.clone();
94 let token = self.token.clone();
95 let activity = self.activity.clone();
96 tokio::spawn(async move {
97 if let Err(error) = serve_connection(stream, &server, token.as_ref(), &activity).await {
98 tracing::warn!(%error, "mcp http connection failed");
99 }
100 });
101 }
102 Err(error) => tracing::warn!(%error, "mcp http accept failed"),
103 }
104 }
105 }
106 }
107
108 tracing::debug!("mcp http endpoint stopped");
109 Ok(())
110 }
111}
112
113async fn serve_connection(
114 mut stream: TcpStream,
115 server: &McpServer,
116 token: Option<&Token>,
117 activity: &Activity,
118) -> Result<()> {
119 let mut buffer = Vec::new();
120 let mut chunk = [0u8; 4096];
121
122 loop {
123 let read = stream
124 .read(&mut chunk)
125 .await
126 .map_err(|error| AppError::internal(format!("cannot read request: {error}")))?;
127
128 if read == 0 {
130 return Ok(());
131 }
132
133 buffer.extend_from_slice(&chunk[..read]);
134
135 if let Some((request, _consumed)) = parse_request(&buffer)? {
136 activity.touch();
138 let response = handle(server, token, &request).await;
139 stream
140 .write_all(&response.encode())
141 .await
142 .map_err(|error| AppError::internal(format!("cannot write response: {error}")))?;
143 stream
144 .flush()
145 .await
146 .map_err(|error| AppError::internal(format!("cannot flush response: {error}")))?;
147 return Ok(());
148 }
149 }
150}
151
152pub async fn handle(
154 server: &McpServer,
155 token: Option<&Token>,
156 request: &HttpRequest,
157) -> HttpResponse {
158 if request.path != MCP_PATH {
159 return HttpResponse::text(404, format!("unknown path `{}`", request.path));
160 }
161
162 if request.method != "POST" {
163 return HttpResponse::text(405, "MCP uses POST");
164 }
165
166 if let Some(expected) = token {
167 match request.bearer_token() {
168 Some(candidate) if expected.matches(candidate) => {}
169 _ => {
170 tracing::warn!("mcp http request rejected: missing or wrong bearer token");
171 return HttpResponse::text(401, "unauthorized");
172 }
173 }
174 }
175
176 let body = match std::str::from_utf8(&request.body) {
177 Ok(body) => body,
178 Err(_) => return HttpResponse::text(400, "request body is not utf-8"),
179 };
180
181 match server.handle_line(body).await {
182 Some(response) => {
183 let value = serde_json::to_value(&response).unwrap_or(serde_json::Value::Null);
184 HttpResponse::json(200, &value)
185 }
186 None => HttpResponse {
188 status: 202,
189 body: Vec::new(),
190 },
191 }
192}
193
194pub async fn post(url: &str, token: Option<&str>, body: &str) -> Result<String> {
198 let (host, port, path) = split_url(url)?;
199
200 let mut stream = TcpStream::connect((host.as_str(), port))
201 .await
202 .map_err(|error| AppError::internal(format!("cannot connect to {url}: {error}")))?;
203
204 let mut request = format!(
205 "POST {path} HTTP/1.1\r\nHost: {host}:{port}\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n",
206 body.len()
207 );
208 if let Some(value) = token {
209 request.push_str("Authorization: Bearer ");
210 request.push_str(value);
211 request.push_str("\r\n");
212 }
213 request.push_str("\r\n");
214
215 stream
216 .write_all(request.as_bytes())
217 .await
218 .map_err(|error| AppError::internal(format!("cannot write request: {error}")))?;
219 stream
220 .write_all(body.as_bytes())
221 .await
222 .map_err(|error| AppError::internal(format!("cannot write body: {error}")))?;
223 stream
224 .flush()
225 .await
226 .map_err(|error| AppError::internal(format!("cannot flush: {error}")))?;
227
228 let mut response = Vec::new();
229 stream
230 .read_to_end(&mut response)
231 .await
232 .map_err(|error| AppError::internal(format!("cannot read response: {error}")))?;
233
234 let text =
235 String::from_utf8(response).map_err(|_| AppError::internal("response is not utf-8"))?;
236
237 let (head, body) = text
238 .split_once("\r\n\r\n")
239 .ok_or_else(|| AppError::internal("malformed http response"))?;
240
241 let status = head
242 .lines()
243 .next()
244 .and_then(|line| line.split_whitespace().nth(1))
245 .and_then(|code| code.parse::<u16>().ok())
246 .unwrap_or(0);
247
248 if status != 200 && status != 202 {
251 return Err(AppError::internal(format!(
252 "endpoint answered with status {status}: {body}"
253 )));
254 }
255
256 Ok(body.to_owned())
257}
258
259fn split_url(url: &str) -> Result<(String, u16, String)> {
260 let rest = url
261 .strip_prefix("http://")
262 .ok_or_else(|| AppError::validation(format!("unsupported url scheme: {url}")))?;
263
264 let (authority, path) = match rest.split_once('/') {
265 Some((authority, path)) => (authority, format!("/{path}")),
266 None => (rest, "/".to_owned()),
267 };
268
269 let (host, port) = authority
270 .split_once(':')
271 .ok_or_else(|| AppError::validation(format!("url has no port: {url}")))?;
272
273 let port = port
274 .parse::<u16>()
275 .map_err(|_| AppError::validation(format!("invalid port in {url}")))?;
276
277 Ok((host.to_owned(), port, path))
278}
279
280#[cfg(test)]
281mod tests {
282 use super::*;
283
284 #[test]
285 fn a_url_splits_into_host_port_and_path() {
286 let (host, port, path) = split_url("http://127.0.0.1:54321/mcp").unwrap();
287 assert_eq!(host, "127.0.0.1");
288 assert_eq!(port, 54321);
289 assert_eq!(path, "/mcp");
290 }
291
292 #[test]
293 fn a_url_without_a_port_is_rejected() {
294 assert!(split_url("http://127.0.0.1/mcp").is_err());
295 }
296}