cc-libs/tools/mcp-bridge/src/mcp-server.ts

128 lines
3.7 KiB
TypeScript

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<unknown> {
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<void> {
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<unknown> {
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<string> {
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<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}