diff --git a/src/server/routes/mcp_route.ts b/src/server/routes/mcp_route.ts index bf760b8..b1d5cf6 100644 --- a/src/server/routes/mcp_route.ts +++ b/src/server/routes/mcp_route.ts @@ -1,150 +1,168 @@ +// src/server/routes/mcp_route.ts import { Elysia, t } from "elysia"; +function now() { + return new Date().toISOString(); +} + export const MCPRoute = new Elysia({ - prefix: "/mcp-server", - tags: ["mcp-server"], + prefix: "/mcp-server", + tags: ["mcp-server"], }) - // ✅ 1. GET untuk handshake n8n - Mengembalikan protocol info - .get("/mcp", ({ set }) => { - set.headers["Content-Type"] = "application/json"; - return { +// ---------- GET: handshake (n8n mengharapkan GET terlebih dahulu) ---------- +.get("/mcp", ({ set, headers }) => { + // disable upstream buffering for nginx (X-Accel-Buffering) and proxies + set.headers["Content-Type"] = "application/json"; + set.headers["Cache-Control"] = "no-cache, no-transform"; + set.headers["X-Accel-Buffering"] = "no"; + set.headers["Connection"] = "keep-alive"; + + // If client explicitly accepts SSE (some clients use SSE fallback), return SSE style + const accept = (headers["accept"] || "").toString(); + if (accept.includes("text/event-stream")) { + // Return a minimal SSE handshake (keep connection open) + const sseStream = new ReadableStream({ + start(controller) { + // send an initial comment to avoid some proxies closing immediately + controller.enqueue(": mcp-sse\n\n"); + // also send a first data event containing protocol info + controller.enqueue( + `data: ${JSON.stringify({ jsonrpc: "2.0", result: { - protocolVersion: "2024-11-05", - capabilities: { - tools: {}, - resources: {} - }, - serverInfo: { - name: "tentang-darmasaba-mcp", - version: "1.0.0" - } - } - }; - }) - - // ✅ 2. POST untuk komunikasi JSON-RPC (non-streaming untuk kompatibilitas) - .post("/mcp", async ({ body, set }) => { - const { id, method, params } = body as any; - - set.headers["Content-Type"] = "application/json"; - - // Initialize response - if (method === "initialize") { - return { - jsonrpc: "2.0", - id, - result: { - protocolVersion: "2024-11-05", - capabilities: { - tools: {}, - resources: {} - }, - serverInfo: { - name: "tentang-darmasaba-mcp", - version: "1.0.0" - } - } - }; - } - - // List tools - if (method === "tools/list") { - return { - jsonrpc: "2.0", - id, - result: { - tools: [ - { - name: "sayHello", - description: "Greets user with a friendly message", - inputSchema: { - type: "object", - properties: { - name: { - type: "string", - description: "Name of the person to greet" - } - }, - required: ["name"] - } - }, - { - name: "getTentangDarmasaba", - description: "Get information about Tentang Darmasaba", - inputSchema: { - type: "object", - properties: {} - } - } - ] - } - }; - } - - // Call tool - if (method === "tools/call") { - const toolName = params?.name; - const args = params?.arguments || {}; - - if (toolName === "sayHello") { - return { - jsonrpc: "2.0", - id, - result: { - content: [ - { - type: "text", - text: `Hello ${args.name || "User"}! Welcome to Tentang Darmasaba MCP Server! 👋` - } - ] - } - }; - } - - if (toolName === "getTentangDarmasaba") { - return { - jsonrpc: "2.0", - id, - result: { - content: [ - { - type: "text", - text: "Tentang Darmasaba adalah platform untuk belajar tentang Darmasaba. Server MCP ini menyediakan tools untuk berinteraksi dengan sistem." - } - ] - } - }; - } - - // Tool not found - return { - jsonrpc: "2.0", - id, - error: { - code: -32602, - message: `Tool '${toolName}' not found` - } - }; - } - - // Method not found - return { - jsonrpc: "2.0", - id, - error: { - code: -32601, - message: `Method '${method}' not found` - } - }; - }, { - body: t.Object({ - jsonrpc: t.String(), - method: t.String(), - params: t.Optional(t.Any()), - id: t.Union([t.String(), t.Number()]), - }), + protocol: "2024-11-05", + capabilities: { "tools/list": true, "tools/call": true }, + }, + })}\n\n` + ); + // keep stream open; do NOT close here (client expects streaming) + }, }); -export default MCPRoute; \ No newline at end of file + return new Response(sseStream, { + headers: { + "Content-Type": "text/event-stream; charset=utf-8", + "Cache-Control": "no-cache, no-transform", + "X-Accel-Buffering": "no", + Connection: "keep-alive", + }, + }); + } + + // Default: return JSON handshake (n8n expects this on GET) + return { + jsonrpc: "2.0", + result: { + protocol: "2024-11-05", + capabilities: { + "tools/list": true, + "tools/call": true, + }, + }, + }; +}) + +// ---------- POST: HTTP Streamable transport (chunked) ---------- +.post( + "/mcp", + ({ body, set, headers }) => { + const { id, method, params } = body as any; + + // Important response headers to help proxies and n8n + set.headers["Content-Type"] = "application/json; charset=utf-8"; + set.headers["Transfer-Encoding"] = "chunked"; + set.headers["Connection"] = "keep-alive"; + set.headers["Cache-Control"] = "no-cache, no-transform"; + set.headers["X-Accel-Buffering"] = "no"; // nginx: disable buffering + // optional helpful header for some proxies + set.headers["X-Content-Type-Options"] = "nosniff"; + + const stream = new ReadableStream({ + async start(controller) { + // send a tiny initial chunk ASAP so client recognizes streaming + controller.enqueue(JSON.stringify({ jsonrpc: "2.0", id, result: { _ping: now() } }) + "\n"); + + // mcp/version might also be called via POST; respond immediately + if (method === "mcp/version") { + controller.enqueue( + JSON.stringify({ + jsonrpc: "2.0", + id, + result: { + protocol: "2024-11-05", + capabilities: { "tools/list": true, "tools/call": true }, + }, + }) + "\n" + ); + // keep a short delay then close; n8n will proceed + await Bun.sleep(50); + controller.close(); + return; + } + + // tools/list + if (method === "tools/list") { + controller.enqueue( + JSON.stringify({ + jsonrpc: "2.0", + id, + result: [ + { + name: "sayHello", + description: "Greets user", + inputSchema: { type: "object", properties: { name: { type: "string" } } }, + }, + ], + }) + "\n" + ); + await Bun.sleep(50); + controller.close(); + return; + } + + // tools/call standard format: params: { name: "toolName", arguments: { ... } } + if (method === "tools/call" && params?.name === "sayHello") { + // stage: processing + controller.enqueue(JSON.stringify({ jsonrpc: "2.0", id, result: { status: "Processing..." } }) + "\n"); + await Bun.sleep(300); + + // final result + controller.enqueue( + JSON.stringify({ + jsonrpc: "2.0", + id, + result: { message: `Hello ${params?.arguments?.name || "User"} 👋` }, + }) + "\n" + ); + await Bun.sleep(50); + controller.close(); + return; + } + + // fallback: method not found + controller.enqueue( + JSON.stringify({ + jsonrpc: "2.0", + id, + error: { code: -32601, message: `Method '${method}' not found` }, + }) + "\n" + ); + await Bun.sleep(50); + controller.close(); + }, + }); + + return new Response(stream); + }, + { + body: t.Object({ + jsonrpc: t.Optional(t.String()), + id: t.Optional(t.Union([t.String(), t.Number()])), + method: t.String(), + params: t.Optional(t.Record(t.String(), t.Any())), + }), + } +); + +export default MCPRoute;