diff --git a/src/index.tsx b/src/index.tsx index 18bcbc3..c8258ad 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -36,7 +36,6 @@ const app = new Elysia() .use(Api) .use(Docs) .use(Auth) - .use(MCPRoute) .get( "/.well-known/mcp.json", async () => { @@ -50,8 +49,8 @@ const app = new Elysia() }, }, ) - // .use(McpRoute) - .get("*", html) + .use(MCPRoute) + .get("/*", html) .listen(3000, () => { console.log("Server running at http://localhost:3000"); }); diff --git a/src/server/routes/mcp_route.ts b/src/server/routes/mcp_route.ts index d4a6f35..fbc8534 100644 --- a/src/server/routes/mcp_route.ts +++ b/src/server/routes/mcp_route.ts @@ -1,165 +1,228 @@ // src/routes/mcp_route.ts import { Elysia, t } from "elysia"; +function createStream(handler: (controller: ReadableStreamDefaultController) => Promise) { + return new ReadableStream({ + async start(controller) { + try { + controller.enqueue(":\n\n"); // Heartbeat awal + await handler(controller); + } catch (error) { + controller.enqueue( + JSON.stringify({ + jsonrpc: "2.0", + error: { code: -32000, message: "Internal Server Error" }, + }) + "\n" + ); + } finally { + try { + controller.close(); + } catch {} + } + }, + cancel() { + console.log("🔴 Client closed the connection"); + }, + }); +} + export const MCPRoute = new Elysia({ prefix: "/mcp", + tags: ["mcp"], }) - // POST endpoint untuk MCP protocol (standard) - .post("/", async ({ body, set }) => { - const { id, method, params } = body as any; + /** + * ✅ GET /mcp/:id — streaming response untuk n8n + curl + */ + .get("/:id", ({ params, query, set }) => { + const id = params.id ?? 1; + const method = query.method as string | undefined; - set.headers["Content-Type"] = "application/json"; + set.headers["Content-Type"] = "application/json; charset=utf-8"; + set.headers["Cache-Control"] = "no-cache, no-transform"; + set.headers["Connection"] = "keep-alive"; + set.headers["Transfer-Encoding"] = "chunked"; + set.headers["X-Accel-Buffering"] = "no"; - try { - // 1. Initialize - Version handshake - if (method === "initialize") { - return { - jsonrpc: "2.0", - id, - result: { - protocolVersion: "2024-11-05", - capabilities: { - tools: { - listChanged: false - } + const stream = createStream(async (controller) => { + // Jika tidak ada method → status server + if (!method) { + controller.enqueue( + JSON.stringify({ + jsonrpc: "2.0", + id, + result: { + status: "MCP Server Ready", + message: "Use ?method=mcp/version or ?method=tools/list", + availableMethods: ["mcp/version", "tools/list", "tools/sayHello"], }, - serverInfo: { - name: "elysia-mcp-server", - version: "1.0.0" - } - } - }; + }) + "\n" + ); + return; } - // 2. List available tools + // mcp/version + 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" + ); + return; + } + + // tools/list if (method === "tools/list") { - return { - jsonrpc: "2.0", - id, - result: { - tools: [ + controller.enqueue( + JSON.stringify({ + jsonrpc: "2.0", + id, + result: [ { name: "sayHello", - description: "Greets a user by name", + description: "Greets a user", inputSchema: { type: "object", - properties: { - name: { - type: "string", - description: "Name of the person to greet" - } - }, - required: ["name"] - } + properties: { name: { type: "string", description: "Your name" } }, + required: ["name"], + }, }, - { - name: "getCurrentTime", - description: "Returns the current server time", - inputSchema: { - type: "object", - properties: {} - } - } - ] - } - }; + ], + }) + "\n" + ); + return; } - // 3. Call tool - Execute the requested tool - if (method === "tools/call") { - const toolName = params?.name; - const toolArgs = params?.arguments || {}; - - if (toolName === "sayHello") { - const userName = toolArgs.name || "User"; - return { + // tools/sayHello (progress + result) + if (method === "tools/sayHello") { + controller.enqueue( + JSON.stringify({ jsonrpc: "2.0", id, result: { status: "Processing..." } }) + "\n" + ); + await Bun.sleep(500); + controller.enqueue( + JSON.stringify({ jsonrpc: "2.0", id, - result: { - content: [ - { - type: "text", - text: `Hello, ${userName}! Welcome to the MCP server.` - } - ] - } - }; - } + result: { message: `Hello ${query?.name || "User"}` }, + }) + "\n" + ); + return; + } - if (toolName === "getCurrentTime") { - const currentTime = new Date().toISOString(); - return { - jsonrpc: "2.0", - id, - result: { - content: [ - { - type: "text", - text: `Current server time: ${currentTime}` - } - ] - } - }; - } - - // Tool not found - return { + // Unknown method + controller.enqueue( + JSON.stringify({ jsonrpc: "2.0", id, - error: { - code: -32602, - message: `Tool "${toolName}" not found` - } - }; - } + error: { code: -32601, message: `Method "${method}" not found` }, + }) + "\n" + ); + }); - // Method not found - return { - jsonrpc: "2.0", - id, - error: { - code: -32601, - message: `Method "${method}" not found` - } - }; + return new Response(stream); + }) + /** + * ✅ POST /mcp — JSON-RPC versi body + */ + .post( + "/", + async ({ body, set }) => { + const { id, method, params } = body as any; - } catch (error: any) { - return { - jsonrpc: "2.0", - id, - error: { - code: -32603, - message: error.message || "Internal error" + set.headers["Content-Type"] = "application/json; charset=utf-8"; + set.headers["Connection"] = "keep-alive"; + set.headers["Transfer-Encoding"] = "chunked"; + set.headers["X-Accel-Buffering"] = "no"; + + const stream = createStream(async (controller) => { + if (!method) { + controller.enqueue( + JSON.stringify({ + jsonrpc: "2.0", + id, + error: { code: -32600, message: "Method required" }, + }) + "\n" + ); + return; } - }; + + 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" + ); + return; + } + + if (method === "tools/list") { + controller.enqueue( + JSON.stringify({ + jsonrpc: "2.0", + id, + result: [ + { + name: "sayHello", + description: "Greets a user", + inputSchema: { + type: "object", + properties: { name: { type: "string" } }, + required: ["name"], + }, + }, + ], + }) + "\n" + ); + return; + } + + if (method === "tools/sayHello") { + controller.enqueue( + JSON.stringify({ + jsonrpc: "2.0", + id, + result: { status: "Processing..." }, + }) + "\n" + ); + await Bun.sleep(500); + controller.enqueue( + JSON.stringify({ + jsonrpc: "2.0", + id, + result: { message: `Hello ${params?.name || "User"}` }, + }) + "\n" + ); + return; + } + + controller.enqueue( + JSON.stringify({ + jsonrpc: "2.0", + id, + error: { code: -32601, message: `Method "${method}" not found` }, + }) + "\n" + ); + }); + + return new Response(stream); + }, + { + body: t.Object({ + jsonrpc: t.Optional(t.String()), + method: t.String(), + params: t.Optional(t.Record(t.String(), t.Any())), + id: t.Optional(t.Union([t.String(), t.Number()])), + }), } - }, { - body: t.Object({ - jsonrpc: t.String(), - method: t.String(), - params: t.Optional(t.Any()), - id: t.Union([t.String(), t.Number()]) - }) - }) - - // GET endpoint untuk testing / health check - .get("/", () => { - return { - status: "ok", - message: "MCP Server is running", - endpoints: { - main: "POST /mcp", - methods: ["initialize", "tools/list", "tools/call"] - } - }; - }) - - // Health check endpoint - .get("/health", () => { - return { - status: "healthy", - timestamp: new Date().toISOString() - }; - }); + ); -export default MCPRoute; \ No newline at end of file +export default MCPRoute; diff --git a/x.sh b/x.sh index ef4151d..9f0d711 100644 --- a/x.sh +++ b/x.sh @@ -1,2 +1,2 @@ -curl -N https://cid-dkr-prod-jenna-mcp.wibudev.com/mcp-server/sse +curl -N -v -X GET "https://n8n.wibudev.com/mcp/123?method=mcp/version"