75 lines
2.8 KiB
EmacsLisp
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")
|