cc-libs/tools/mcp-bridge/src/opencode-proxy.ts

74 lines
2.4 KiB
TypeScript

import { WebSocketServer, type WebSocket } from "ws";
import { parseHttpRequestFrame, parseJsonFrame, type HttpResponseFrame } from "./protocol.js";
export type OpencodeProxyOptions = {
host: string;
port: number;
opencodeUrl: string;
username?: string;
password?: string;
maxRequestMs?: number;
};
const DEFAULT_MAX_REQUEST_MS = 600_000;
// WebSocket endpoint where the ComputerCraft client is the requester and the
// bridge is the responder: it performs the opencode http call server-side (no
// short timeout) and returns the raw response. This lets the in-game client use
// opencode's synchronous /message endpoint without hitting CC's ~60s http cap.
export function startOpencodeProxy(options: OpencodeProxyOptions): WebSocketServer {
const baseUrl = options.opencodeUrl.replace(/\/+$/, "");
const maxRequestMs = options.maxRequestMs ?? DEFAULT_MAX_REQUEST_MS;
const authHeader = options.password
? "Basic " + Buffer.from(`${options.username ?? "opencode"}:${options.password}`).toString("base64")
: undefined;
const server = new WebSocketServer({ host: options.host, port: options.port });
server.on("connection", (ws) => {
ws.on("message", (data) => {
void handleFrame(ws, data);
});
ws.on("error", () => {
/* connection-level errors just drop the socket; nothing to clean up */
});
});
async function handleFrame(ws: WebSocket, data: unknown): Promise<void> {
const frame = parseHttpRequestFrame(parseJsonFrame(data));
if (!frame) {
return;
}
const headers: Record<string, string> = {
"Content-Type": "application/json",
Accept: "application/json",
};
if (authHeader) {
headers.Authorization = authHeader;
}
try {
const res = await fetch(baseUrl + frame.path, {
method: frame.method,
headers,
body: frame.method === "POST" ? frame.body : undefined,
signal: AbortSignal.timeout(maxRequestMs),
});
const body = await res.text();
send(ws, { type: "http-response", id: frame.id, status: res.status, body });
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
send(ws, { type: "http-response", id: frame.id, status: 0, error: message });
}
}
return server;
}
function send(ws: WebSocket, frame: HttpResponseFrame): void {
if (ws.readyState === ws.OPEN) {
ws.send(JSON.stringify(frame));
}
}