tambahan
This commit is contained in:
@@ -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;
|
||||
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;
|
||||
|
||||
Reference in New Issue
Block a user