import { createServer, type IncomingMessage, type Server, type ServerResponse } from "node:http"; import type { LinkRegistry } from "./link-server.js"; type JsonRpcRequest = { jsonrpc?: unknown; id?: unknown; method?: unknown; params?: unknown; }; export function startMcpServer(options: { host: string; port: number; probeTimeoutMs: number; registry: LinkRegistry; }): Server { const server = createServer((req, res) => { void handleRequest(req, res, options.registry, options.probeTimeoutMs); }); server.listen(options.port, options.host); return server; } export async function handleMcpRequest(body: unknown, registry: LinkRegistry, probeTimeoutMs: number): Promise { if (Array.isArray(body)) { return Promise.all(body.map((item) => handleSingleRequest(item as JsonRpcRequest, registry, probeTimeoutMs))); } return handleSingleRequest(body as JsonRpcRequest, registry, probeTimeoutMs); } async function handleRequest( req: IncomingMessage, res: ServerResponse, registry: LinkRegistry, probeTimeoutMs: number, ): Promise { if (req.method === "GET" && req.url === "/health") { writeJson(res, 200, { ok: true, computers: registry.count() }); return; } if (req.method !== "POST") { writeJson(res, 404, { error: "not found" }); return; } const raw = await readBody(req); let body: unknown; try { body = JSON.parse(raw) as unknown; } catch { writeJson(res, 400, jsonRpcError(null, -32700, "Parse error")); return; } writeJson(res, 200, await handleMcpRequest(body, registry, probeTimeoutMs)); } async function handleSingleRequest(request: JsonRpcRequest, registry: LinkRegistry, probeTimeoutMs: number): Promise { const id = request && "id" in request ? request.id : null; if (!request || request.jsonrpc !== "2.0" || typeof request.method !== "string") { return jsonRpcError(id, -32600, "Invalid Request"); } if (request.method === "initialize") { return jsonRpcResult(id, { protocolVersion: "2024-11-05", capabilities: { tools: {} }, serverInfo: { name: "mcp-bridge", version: "0.1.0" }, }); } if (request.method === "notifications/initialized") { return null; } if (request.method === "tools/list") { return jsonRpcResult(id, { tools: [ { name: "probe-computers", description: "Probe all linked ComputerCraft computers.", inputSchema: { type: "object", properties: {}, additionalProperties: false }, }, ], }); } if (request.method === "tools/call") { const params = isRecord(request.params) ? request.params : {}; if (params.name !== "probe-computers") { return jsonRpcError(id, -32602, "Unknown tool"); } const text = await registry.probeComputers(probeTimeoutMs); return jsonRpcResult(id, { content: [{ type: "text", text }] }); } return jsonRpcError(id, -32601, "Method not found"); } function jsonRpcResult(id: unknown, result: unknown): unknown { return { jsonrpc: "2.0", id, result }; } function jsonRpcError(id: unknown, code: number, message: string): unknown { return { jsonrpc: "2.0", id, error: { code, message } }; } function writeJson(res: ServerResponse, statusCode: number, body: unknown): void { res.writeHead(statusCode, { "content-type": "application/json" }); res.end(JSON.stringify(body)); } async function readBody(req: IncomingMessage): Promise { const chunks: Buffer[] = []; for await (const chunk of req) { chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)); } return Buffer.concat(chunks).toString("utf8"); } function isRecord(value: unknown): value is Record { return typeof value === "object" && value !== null && !Array.isArray(value); }