tessera_codegraph/
mcp_http.rs1use std::io::{Read, Write};
2use std::net::{TcpListener, TcpStream};
3use std::path::Path;
4
5use anyhow::Result;
6
7use crate::db;
8use crate::mcp;
9
10pub fn serve(addr: &str, db_path: &Path) -> Result<()> {
11 let conn = db::open_existing(db_path)?;
12 let listener = TcpListener::bind(addr)?;
13 println!("Tessera MCP HTTP listening on http://{addr}");
14 println!(" POST /mcp JSON-RPC MCP endpoint");
15 println!(" GET /sse readiness event stream");
16 println!(" GET /health health check");
17
18 for stream in listener.incoming() {
19 match stream {
20 Ok(mut stream) => {
21 if let Err(error) = handle_stream(&mut stream, &conn, db_path) {
22 let _ = write_response(
23 &mut stream,
24 "500 Internal Server Error",
25 "text/plain; charset=utf-8",
26 &error.to_string(),
27 );
28 }
29 }
30 Err(error) => eprintln!("mcp-http accept error: {error}"),
31 }
32 }
33 Ok(())
34}
35
36fn handle_stream(
37 stream: &mut TcpStream,
38 conn: &rusqlite::Connection,
39 db_path: &Path,
40) -> Result<()> {
41 let mut buffer = Vec::new();
42 let mut chunk = [0u8; 8192];
43 loop {
44 let read = stream.read(&mut chunk)?;
45 if read == 0 {
46 break;
47 }
48 buffer.extend_from_slice(&chunk[..read]);
49 if request_complete(&buffer) {
50 break;
51 }
52 if buffer.len() > 2 * 1024 * 1024 {
53 write_response(
54 stream,
55 "413 Payload Too Large",
56 "text/plain; charset=utf-8",
57 "request too large",
58 )?;
59 return Ok(());
60 }
61 }
62
63 let request = String::from_utf8_lossy(&buffer);
64 let Some((head, body)) = request.split_once("\r\n\r\n") else {
65 write_response(
66 stream,
67 "400 Bad Request",
68 "text/plain; charset=utf-8",
69 "malformed HTTP request",
70 )?;
71 return Ok(());
72 };
73 let mut lines = head.lines();
74 let request_line = lines.next().unwrap_or_default();
75 let mut parts = request_line.split_whitespace();
76 let method = parts.next().unwrap_or_default();
77 let path = parts.next().unwrap_or_default();
78
79 match (method, path) {
80 ("GET", "/health") => write_response(
81 stream,
82 "200 OK",
83 "application/json",
84 r#"{"ok":true,"service":"tessera-mcp-http"}"#,
85 )?,
86 ("GET", "/sse") => {
87 let body = "event: ready\ndata: {\"endpoint\":\"/mcp\"}\n\n";
88 write_raw(
89 stream,
90 "HTTP/1.1 200 OK\r\nContent-Type: text/event-stream\r\nCache-Control: no-cache\r\nConnection: close\r\n\r\n",
91 body,
92 )?;
93 }
94 ("POST", "/mcp") => {
95 let response = mcp::handle_json_rpc(conn, db_path, body);
96 let json = serde_json::to_string(&response)?;
97 write_response(stream, "200 OK", "application/json", &json)?;
98 }
99 _ => write_response(
100 stream,
101 "404 Not Found",
102 "text/plain; charset=utf-8",
103 "not found",
104 )?,
105 }
106 Ok(())
107}
108
109fn request_complete(buffer: &[u8]) -> bool {
110 let Some(header_end) = find_header_end(buffer) else {
111 return false;
112 };
113 let head = String::from_utf8_lossy(&buffer[..header_end]);
114 let content_length = head.lines().find_map(|line| {
115 let (name, value) = line.split_once(':')?;
116 if name.eq_ignore_ascii_case("content-length") {
117 value.trim().parse::<usize>().ok()
118 } else {
119 None
120 }
121 });
122 match content_length {
123 Some(length) => buffer.len() >= header_end + 4 + length,
124 None => true,
125 }
126}
127
128fn find_header_end(buffer: &[u8]) -> Option<usize> {
129 buffer.windows(4).position(|window| window == b"\r\n\r\n")
130}
131
132fn write_response(
133 stream: &mut TcpStream,
134 status: &str,
135 content_type: &str,
136 body: &str,
137) -> Result<()> {
138 let head = format!(
139 "HTTP/1.1 {status}\r\nContent-Type: {content_type}\r\nContent-Length: {}\r\nConnection: close\r\n\r\n",
140 body.len()
141 );
142 write_raw(stream, &head, body)
143}
144
145fn write_raw(stream: &mut TcpStream, head: &str, body: &str) -> Result<()> {
146 stream.write_all(head.as_bytes())?;
147 stream.write_all(body.as_bytes())?;
148 stream.flush()?;
149 Ok(())
150}