Files
neuron/mcp-proxy/src/main.el
T
will.anderson 5a4ef04005
Deploy Soul to GKE / deploy (push) Failing after 36s
Neuron Soul CI / build (push) Failing after 4m59s
feat: add mcp-proxy and mcp-wrapper source (MCP front-door for Claude Code)
2026-06-10 17:44:01 -05:00

75 lines
2.8 KiB
EmacsLisp

// mcp-proxy - stable forwarder for the mcp-wrapper.
//
// Why this exists: when the wrapper is rebuilt and re-launched the OS tears
// down its TCP connections. Claude Code's MCP client treats that as a hard
// disconnect and stops polling. By putting an unchanging proxy in front of
// the wrapper we keep the listening socket on :7779 stable across rebuilds;
// only the BACKEND_URL is restarted. Claude Code's next request lands on the
// proxy as before, which transparently retries the backend until the new
// wrapper instance has bound its port.
//
// Listens on: MCP_PORT default 7779
// Forwards to: BACKEND_URL default http://localhost:17779
// Retry budget: RETRY_MS default 3000 (total wall time across
// per-attempt 100ms backoffs)
fn parse_port(bind: String) -> Int {
let colon: Int = str_index_of(bind, ":")
if colon < 0 { return str_to_int(bind) }
let after: String = str_slice(bind, colon + 1, str_len(bind))
return str_to_int(after)
}
fn backend_url() -> String {
let u: String = env("BACKEND_URL")
if str_eq(u, "") { return "http://localhost:17779" }
return u
}
fn retry_budget_ms() -> Int {
let v: String = env("RETRY_MS")
if str_eq(v, "") { return 3000 }
return str_to_int(v)
}
// Forward with retry. Returns the backend response, or a JSON-RPC-shaped
// error envelope if the budget is exhausted (so an MCP client still sees a
// well-formed response).
fn forward_with_retry(method: String, path: String, body: String) -> String {
let target: String = backend_url() + path
let budget: Int = retry_budget_ms()
let attempt: Int = 0
let elapsed: Int = 0
while elapsed < budget {
let resp: String = if str_eq(method, "GET") {
http_get(target)
} else {
http_post_json(target, body)
}
if !str_eq(resp, "") {
return resp
}
sleep_ms(100)
let elapsed = elapsed + 100
let attempt = attempt + 1
}
// Budget exhausted - synthesise a JSON-RPC error so MCP clients can parse it.
return "{\"jsonrpc\":\"2.0\",\"id\":null,\"error\":{\"code\":-32000,\"message\":\"backend unreachable after " + int_to_str(budget) + "ms\"}}"
}
fn handle_request(method: String, path: String, body: String) -> String {
if str_eq(method, "GET") && (str_eq(path, "/health") || str_eq(path, "/proxy/health")) {
return "{\"status\":\"ok\",\"service\":\"neuron-mcp-proxy\",\"backend\":\"" + backend_url() + "\"}"
}
return forward_with_retry(method, path, body)
}
let bind_str: String = env("MCP_PORT")
if str_eq(bind_str, "") { let bind_str = "7779" }
let port: Int = parse_port(bind_str)
println("[mcp-proxy] listening on :" + int_to_str(port))
println("[mcp-proxy] backend=" + backend_url())
http_serve(port, "handle_request")