Skip to main content

tessera_codegraph/
mcp_http.rs

1use 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}