tambahan
This commit is contained in:
@@ -1,150 +1,168 @@
|
|||||||
|
// src/server/routes/mcp_route.ts
|
||||||
import { Elysia, t } from "elysia";
|
import { Elysia, t } from "elysia";
|
||||||
|
|
||||||
|
function now() {
|
||||||
|
return new Date().toISOString();
|
||||||
|
}
|
||||||
|
|
||||||
export const MCPRoute = new Elysia({
|
export const MCPRoute = new Elysia({
|
||||||
prefix: "/mcp-server",
|
prefix: "/mcp-server",
|
||||||
tags: ["mcp-server"],
|
tags: ["mcp-server"],
|
||||||
})
|
})
|
||||||
|
|
||||||
// ✅ 1. GET untuk handshake n8n - Mengembalikan protocol info
|
// ---------- GET: handshake (n8n mengharapkan GET terlebih dahulu) ----------
|
||||||
.get("/mcp", ({ set }) => {
|
.get("/mcp", ({ set, headers }) => {
|
||||||
set.headers["Content-Type"] = "application/json";
|
// disable upstream buffering for nginx (X-Accel-Buffering) and proxies
|
||||||
return {
|
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",
|
jsonrpc: "2.0",
|
||||||
result: {
|
result: {
|
||||||
protocolVersion: "2024-11-05",
|
protocol: "2024-11-05",
|
||||||
capabilities: {
|
capabilities: { "tools/list": true, "tools/call": true },
|
||||||
tools: {},
|
},
|
||||||
resources: {}
|
})}\n\n`
|
||||||
},
|
);
|
||||||
serverInfo: {
|
// keep stream open; do NOT close here (client expects streaming)
|
||||||
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()]),
|
|
||||||
}),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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;
|
export default MCPRoute;
|
||||||
Reference in New Issue
Block a user