251 lines
7.0 KiB
Plaintext
251 lines
7.0 KiB
Plaintext
import { Elysia } from "elysia";
|
|
import { getMcpTools } from "../lib/mcp_tool_convert";
|
|
|
|
var tools = [] as any[];
|
|
const OPENAPI_URL = process.env.BUN_PUBLIC_BASE_URL + "/docs/json";
|
|
const FILTER_TAG = "mcp";
|
|
|
|
if (!process.env.BUN_PUBLIC_BASE_URL) {
|
|
throw new Error("BUN_PUBLIC_BASE_URL environment variable is not set");
|
|
}
|
|
|
|
// =====================
|
|
// MCP Protocol Types
|
|
// =====================
|
|
type JSONRPCRequest = {
|
|
jsonrpc: "2.0";
|
|
id: string | number;
|
|
method: string;
|
|
params?: any;
|
|
};
|
|
|
|
type JSONRPCResponse = {
|
|
jsonrpc: "2.0";
|
|
id: string | number;
|
|
result?: any;
|
|
error?: {
|
|
code: number;
|
|
message: string;
|
|
data?: any;
|
|
};
|
|
};
|
|
|
|
// =====================
|
|
// Tool Executor
|
|
// =====================
|
|
export async function executeTool(
|
|
tool: any,
|
|
args: Record<string, any> = {},
|
|
baseUrl: string
|
|
) {
|
|
const x = tool["x-props"] || {};
|
|
|
|
const method = (x.method || "GET").toUpperCase();
|
|
const path = x.path || `/${tool.name}`;
|
|
const url = `${baseUrl}${path}`;
|
|
|
|
const opts: RequestInit = {
|
|
method,
|
|
headers: { "Content-Type": "application/json" },
|
|
};
|
|
|
|
if (["POST", "PUT", "PATCH", "DELETE"].includes(method)) {
|
|
opts.body = JSON.stringify(args || {});
|
|
}
|
|
|
|
const res = await fetch(url, opts);
|
|
const contentType = res.headers.get("content-type") || "";
|
|
const data = contentType.includes("application/json")
|
|
? await res.json()
|
|
: await res.text();
|
|
|
|
return {
|
|
success: res.ok,
|
|
status: res.status,
|
|
method,
|
|
path,
|
|
data,
|
|
};
|
|
}
|
|
|
|
// =====================
|
|
// MCP Handler (Async)
|
|
// =====================
|
|
async function handleMCPRequestAsync(
|
|
request: JSONRPCRequest
|
|
): Promise<JSONRPCResponse> {
|
|
const { id, method, params } = request;
|
|
|
|
switch (method) {
|
|
case "initialize":
|
|
return {
|
|
jsonrpc: "2.0",
|
|
id,
|
|
result: {
|
|
protocolVersion: "2024-11-05",
|
|
capabilities: { tools: {} },
|
|
serverInfo: { name: "elysia-mcp-server", version: "1.0.0" },
|
|
},
|
|
};
|
|
|
|
case "tools/list":
|
|
return {
|
|
jsonrpc: "2.0",
|
|
id,
|
|
result: {
|
|
tools: tools.map(({ name, description, inputSchema, ["x-props"]: x }) => ({
|
|
name,
|
|
description,
|
|
inputSchema,
|
|
"x-props": x,
|
|
})),
|
|
},
|
|
};
|
|
|
|
case "tools/call": {
|
|
const toolName = params?.name;
|
|
const tool = tools.find((t) => t.name === toolName);
|
|
|
|
if (!tool) {
|
|
return {
|
|
jsonrpc: "2.0",
|
|
id,
|
|
error: { code: -32601, message: `Tool '${toolName}' not found` },
|
|
};
|
|
}
|
|
|
|
try {
|
|
const baseUrl =
|
|
process.env.BUN_PUBLIC_BASE_URL || "http://localhost:3000";
|
|
const result = await executeTool(tool, params?.arguments || {}, baseUrl);
|
|
const data = result.data.data;
|
|
const isObject = typeof data === "object" && data !== null;
|
|
|
|
return {
|
|
jsonrpc: "2.0",
|
|
id,
|
|
result: {
|
|
content: [
|
|
isObject
|
|
? { type: "json", data: data }
|
|
: { type: "text", text: JSON.stringify(data || result.data || result) },
|
|
],
|
|
},
|
|
};
|
|
} catch (error: any) {
|
|
return {
|
|
jsonrpc: "2.0",
|
|
id,
|
|
error: { code: -32603, message: error.message },
|
|
};
|
|
}
|
|
}
|
|
|
|
case "ping":
|
|
return { jsonrpc: "2.0", id, result: {} };
|
|
|
|
default:
|
|
return {
|
|
jsonrpc: "2.0",
|
|
id,
|
|
error: { code: -32601, message: `Method '${method}' not found` },
|
|
};
|
|
}
|
|
}
|
|
|
|
// =====================
|
|
// Elysia MCP Server
|
|
// =====================
|
|
export const MCPRoute = new Elysia({
|
|
tags: ["MCP Server"]
|
|
})
|
|
.post("/mcp", async ({ request, set }) => {
|
|
if (!tools.length) {
|
|
tools = await getMcpTools(OPENAPI_URL, FILTER_TAG);
|
|
}
|
|
set.headers["Content-Type"] = "application/json";
|
|
set.headers["Access-Control-Allow-Origin"] = "*";
|
|
|
|
try {
|
|
const body = await request.json();
|
|
|
|
if (!Array.isArray(body)) {
|
|
const res = await handleMCPRequestAsync(body);
|
|
return res;
|
|
}
|
|
|
|
const results = await Promise.all(
|
|
body.map((req) => handleMCPRequestAsync(req))
|
|
);
|
|
return results;
|
|
} catch (error: any) {
|
|
set.status = 400;
|
|
return {
|
|
jsonrpc: "2.0",
|
|
id: null,
|
|
error: {
|
|
code: -32700,
|
|
message: "Parse error",
|
|
data: error.message,
|
|
},
|
|
};
|
|
}
|
|
})
|
|
|
|
// Tools list (debug)
|
|
.get("/mcp/tools", async ({ set }) => {
|
|
if (!tools.length) {
|
|
|
|
tools = await getMcpTools(OPENAPI_URL, FILTER_TAG);
|
|
}
|
|
set.headers["Access-Control-Allow-Origin"] = "*";
|
|
return {
|
|
tools: tools.map(({ name, description, inputSchema, ["x-props"]: x }) => ({
|
|
name,
|
|
description,
|
|
inputSchema,
|
|
"x-props": x,
|
|
})),
|
|
};
|
|
})
|
|
|
|
// MCP status
|
|
.get("/mcp/status", ({ set }) => {
|
|
set.headers["Access-Control-Allow-Origin"] = "*";
|
|
return { status: "active", timestamp: Date.now() };
|
|
})
|
|
|
|
// Health check
|
|
.get("/health", ({ set }) => {
|
|
set.headers["Access-Control-Allow-Origin"] = "*";
|
|
return { status: "ok", timestamp: Date.now(), tools: tools.length };
|
|
})
|
|
.get("/mcp/init", async ({ set }) => {
|
|
|
|
const _tools = await getMcpTools(OPENAPI_URL, FILTER_TAG);
|
|
tools = _tools;
|
|
return {
|
|
success: true,
|
|
message: "MCP initialized",
|
|
tools: tools.length,
|
|
};
|
|
})
|
|
|
|
// CORS
|
|
.options("/mcp", ({ set }) => {
|
|
set.headers["Access-Control-Allow-Origin"] = "*";
|
|
set.headers["Access-Control-Allow-Methods"] = "GET,POST,OPTIONS";
|
|
set.headers["Access-Control-Allow-Headers"] =
|
|
"Content-Type,Authorization,X-API-Key";
|
|
set.status = 204;
|
|
return "";
|
|
})
|
|
.options("/mcp/tools", ({ set }) => {
|
|
set.headers["Access-Control-Allow-Origin"] = "*";
|
|
set.headers["Access-Control-Allow-Methods"] = "GET,OPTIONS";
|
|
set.headers["Access-Control-Allow-Headers"] =
|
|
"Content-Type,Authorization,X-API-Key";
|
|
set.status = 204;
|
|
return "";
|
|
});
|