diff --git a/src/server/routes/mcp_route.ts b/src/server/routes/mcp_route.ts index 98c5112..f66db60 100644 --- a/src/server/routes/mcp_route.ts +++ b/src/server/routes/mcp_route.ts @@ -9,9 +9,9 @@ const PING_INTERVAL_MS = 25_000; // Store session & clients // ===================== type Client = { - id: string; - send: (data: string) => void; - close: () => void; + id: string; + send: (data: string) => void; + close: () => void; }; const sessions = new Map>(); @@ -20,125 +20,199 @@ const sessions = new Map>(); // Helper Functions // ===================== function isAuthorized(headers: Headers) { - return headers.get("x-api-key") === API_KEY; + return headers.get("x-api-key") === API_KEY; } function formatSSE(event: string, data: any, id?: string) { - const payload = typeof data === "string" ? data : JSON.stringify(data); - return [ - id ? `id: ${id}` : "", - event ? `event: ${event}` : "", - ...payload.split("\n").map((line) => `data: ${line}`), - "", - ].join("\n"); + const payload = typeof data === "string" ? data : JSON.stringify(data); + return [ + id ? `id: ${id}` : "", + event ? `event: ${event}` : "", + ...payload.split("\n").map((line) => `data: ${line}`), + "", + ].join("\n"); } function broadcast(sessionId: string, event: string, data: any) { - const clients = sessions.get(sessionId); - if (!clients) return 0; - const messageId = uuidv4(); - const message = formatSSE(event, data, messageId); + const clients = sessions.get(sessionId); + if (!clients) return 0; + const messageId = uuidv4(); + const message = formatSSE(event, data, messageId); - for (const client of clients) { - try { - client.send(message); - } catch { - clients.delete(client); + for (const client of clients) { + try { + client.send(message); + } catch { + clients.delete(client); + } } - } - return clients.size; + return clients.size; } +// ===================== +// Tools Definition +// ===================== +type Tool = { + name: string; + description: string; + run: (input?: any) => Promise; +}; + +// contoh tools sederhana (bisa dikembangkan) +const tools: Tool[] = [ + { + name: "ping", + description: "Mengembalikan timestamp saat ini dari server.", + run: async () => ({ pong: Date.now() }), + }, + { + name: "uuid", + description: "Menghasilkan UUID v4 unik.", + run: async () => ({ uuid: uuidv4() }), + }, + { + name: "echo", + description: "Mengembalikan data yang dikirim.", + run: async (input) => ({ echo: input }), + }, +]; + // ===================== // Server Initialization // ===================== export const MCPRoute = new Elysia() - .get("/mcp/:sessionId", ({ params, set }) => { - const { sessionId } = params; + // ===================== + // SSE Stream + // ===================== + .get("/mcp/:sessionId", ({ params, set }) => { + const { sessionId } = params; - set.headers["Content-Type"] = "text/event-stream; charset=utf-8"; - set.headers["Cache-Control"] = "no-cache"; - set.headers["Connection"] = "keep-alive"; - set.headers["Access-Control-Allow-Origin"] = "*"; + set.headers["Content-Type"] = "text/event-stream; charset=utf-8"; + set.headers["Cache-Control"] = "no-cache"; + set.headers["Connection"] = "keep-alive"; + set.headers["Access-Control-Allow-Origin"] = "*"; - // Create a readable stream for SSE - const stream = new TransformStream(); - const writer = stream.writable.getWriter(); + const stream = new TransformStream(); + const writer = stream.writable.getWriter(); - const client: Client = { - id: uuidv4(), - send: (data) => writer.write(new TextEncoder().encode(data + "\n")), - close: () => { - writer.close(); - const set = sessions.get(sessionId); - if (set) { - set.delete(client); - if (set.size === 0) sessions.delete(sessionId); + const client: Client = { + id: uuidv4(), + send: (data) => writer.write(new TextEncoder().encode(data + "\n")), + close: () => { + writer.close(); + const set = sessions.get(sessionId); + if (set) { + set.delete(client); + if (set.size === 0) sessions.delete(sessionId); + } + }, + }; + + if (!sessions.has(sessionId)) sessions.set(sessionId, new Set()); + sessions.get(sessionId)!.add(client); + + client.send(formatSSE("connected", { sessionId, id: client.id })); + + const ping = setInterval(() => { + client.send(formatSSE("ping", { ts: Date.now() })); + }, PING_INTERVAL_MS); + + const readable = stream.readable; + const abort = new AbortController(); + + abort.signal.addEventListener("abort", () => { + clearInterval(ping); + client.close(); + }); + + return new Response(readable, { + headers: set.headers as HeadersInit, + status: 200, + }); + }) + + // ===================== + // MCP Session Status + // ===================== + .get("/mcp/:sessionId/status", ({ params, set }) => { + set.headers["Access-Control-Allow-Origin"] = "*"; + const clients = sessions.get(params.sessionId); + return { + sessionId: params.sessionId, + connected: clients?.size ?? 0, + }; + }) + + // ===================== + // MCP Broadcast + // ===================== + .post("/mcp/:sessionId", async ({ params, request, set }) => { + set.headers["Access-Control-Allow-Origin"] = "*"; + if (!isAuthorized(request.headers)) { + return new Response("Unauthorized", { status: 401 }); } - }, - }; - if (!sessions.has(sessionId)) sessions.set(sessionId, new Set()); - sessions.get(sessionId)!.add(client); + const body = await request.json(); + const event = body.event ?? "message"; + const data = body.data ?? body; - // Send "connected" event - client.send(formatSSE("connected", { sessionId, id: client.id })); + const sentTo = broadcast(params.sessionId, event, data); + return { ok: true, sentTo }; + }) - // Keepalive ping - const ping = setInterval(() => { - client.send(formatSSE("ping", { ts: Date.now() })); - }, PING_INTERVAL_MS); + // ===================== + // Delete /mcp/:sessionId + // ===================== + .delete("/mcp/:sessionId", ({ params, request, set }) => { + set.headers["Access-Control-Allow-Origin"] = "*"; + if (!isAuthorized(request.headers)) { + return new Response("Unauthorized", { status: 401 }); + } - const readable = stream.readable; - const abort = new AbortController(); + const clients = sessions.get(params.sessionId); + if (clients) { + for (const c of clients) c.close(); + sessions.delete(params.sessionId); + } + return { ok: true }; + }) - abort.signal.addEventListener("abort", () => { - clearInterval(ping); - client.close(); + // ===================== + // Tools Introspection + // ===================== + .get("/mcp/tools", ({ set }) => { + set.headers["Access-Control-Allow-Origin"] = "*"; + return tools.map(({ name, description }) => ({ name, description })); + }) + + // ===================== + // Run Tool + // ===================== + .post("/mcp/tools/:toolName", async ({ params, request, set }) => { + set.headers["Access-Control-Allow-Origin"] = "*"; + if (!isAuthorized(request.headers)) { + return new Response("Unauthorized", { status: 401 }); + } + + const tool = tools.find((t) => t.name === params.toolName); + if (!tool) { + return new Response( + JSON.stringify({ error: `Tool '${params.toolName}' not found` }), + { status: 404 } + ); + } + + const body = await request.json().catch(() => ({})); + const result = await tool.run(body); + return { ok: true, tool: tool.name, result }; + }) + + // ===================== + // CORS preflight + // ===================== + .options("/mcp/:sessionId", ({ set }) => { + set.headers["Access-Control-Allow-Origin"] = "*"; + set.headers["Access-Control-Allow-Methods"] = "GET,POST,DELETE,OPTIONS"; + set.headers["Access-Control-Allow-Headers"] = "Content-Type,X-API-Key"; + return new Response(null, { status: 204 }); }); - - return new Response(readable, { - headers: set.headers as HeadersInit, - status: 200, - }); - }) - .get("/mcp/:sessionId/status", ({ params, set }) => { - set.headers["Access-Control-Allow-Origin"] = "*"; - const clients = sessions.get(params.sessionId); - return { - sessionId: params.sessionId, - connected: clients?.size ?? 0, - }; - }) - .post("/mcp/:sessionId", async ({ params, request, set }) => { - set.headers["Access-Control-Allow-Origin"] = "*"; - if (!isAuthorized(request.headers)) { - return new Response("Unauthorized", { status: 401 }); - } - - const body = await request.json(); - const event = body.event ?? "message"; - const data = body.data ?? body; - - const sentTo = broadcast(params.sessionId, event, data); - return { ok: true, sentTo }; - }) - .delete("/mcp/:sessionId", ({ params, request, set }) => { - set.headers["Access-Control-Allow-Origin"] = "*"; - if (!isAuthorized(request.headers)) { - return new Response("Unauthorized", { status: 401 }); - } - - const clients = sessions.get(params.sessionId); - if (clients) { - for (const c of clients) c.close(); - sessions.delete(params.sessionId); - } - return { ok: true }; - }) - .options("/mcp/:sessionId", ({ set }) => { - set.headers["Access-Control-Allow-Origin"] = "*"; - set.headers["Access-Control-Allow-Methods"] = "GET,POST,DELETE,OPTIONS"; - set.headers["Access-Control-Allow-Headers"] = "Content-Type,X-API-Key"; - return new Response(null, { status: 204 }); - });