128 lines
3.7 KiB
TypeScript
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);
|
|
}
|