tambahan
This commit is contained in:
@@ -19,7 +19,7 @@ interface McpTool {
|
|||||||
* Convert OpenAPI 3.x JSON spec into MCP-compatible tool definitions (without run()).
|
* Convert OpenAPI 3.x JSON spec into MCP-compatible tool definitions (without run()).
|
||||||
* Each tool corresponds to an endpoint, with metadata stored under `x-props`.
|
* Each tool corresponds to an endpoint, with metadata stored under `x-props`.
|
||||||
*/
|
*/
|
||||||
export function convertOpenApiToMcpTools(openApiJson: any, baseUrl: string = ""): McpTool[] {
|
export function convertOpenApiToMcpTools(openApiJson: any): McpTool[] {
|
||||||
const tools: McpTool[] = [];
|
const tools: McpTool[] = [];
|
||||||
const paths = openApiJson.paths || {};
|
const paths = openApiJson.paths || {};
|
||||||
|
|
||||||
@@ -92,9 +92,14 @@ function cleanToolName(name: string): string {
|
|||||||
// const tools = convertOpenApiToMcpTools(openApiJson, "https://api.wibudev.com");
|
// const tools = convertOpenApiToMcpTools(openApiJson, "https://api.wibudev.com");
|
||||||
// console.log(JSON.stringify(tools, null, 2));
|
// console.log(JSON.stringify(tools, null, 2));
|
||||||
|
|
||||||
if (import.meta.main) {
|
export async function getMcpTools(){
|
||||||
const data = await fetch("http://localhost:3000/docs/json");
|
const data = await fetch(`${process.env.BUN_PUBLIC_BASE_URL}/docs/json`);
|
||||||
const openApiJson = await data.json();
|
const openApiJson = await data.json();
|
||||||
const tools = convertOpenApiToMcpTools(openApiJson, "http://localhost:3000");
|
const tools = convertOpenApiToMcpTools(openApiJson);
|
||||||
|
return tools;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (import.meta.main) {
|
||||||
|
const tools = await getMcpTools();
|
||||||
Bun.write("./tools.json", JSON.stringify(tools, null, 2));
|
Bun.write("./tools.json", JSON.stringify(tools, null, 2));
|
||||||
}
|
}
|
||||||
@@ -1,147 +1,150 @@
|
|||||||
import { Elysia } from "elysia";
|
import { Elysia } from "elysia";
|
||||||
import tools from "./../../../tools.json";
|
import { getMcpTools } from "../lib/mcp_tool_convert";
|
||||||
|
// import tools from "./../../../tools.json";
|
||||||
|
|
||||||
|
var tools = [] as any[];
|
||||||
|
|
||||||
// =====================
|
// =====================
|
||||||
// MCP Protocol Types
|
// MCP Protocol Types
|
||||||
// =====================
|
// =====================
|
||||||
type JSONRPCRequest = {
|
type JSONRPCRequest = {
|
||||||
jsonrpc: "2.0";
|
jsonrpc: "2.0";
|
||||||
id: string | number;
|
id: string | number;
|
||||||
method: string;
|
method: string;
|
||||||
params?: any;
|
params?: any;
|
||||||
};
|
};
|
||||||
|
|
||||||
type JSONRPCResponse = {
|
type JSONRPCResponse = {
|
||||||
jsonrpc: "2.0";
|
jsonrpc: "2.0";
|
||||||
id: string | number;
|
id: string | number;
|
||||||
result?: any;
|
result?: any;
|
||||||
error?: {
|
error?: {
|
||||||
code: number;
|
code: number;
|
||||||
message: string;
|
message: string;
|
||||||
data?: any;
|
data?: any;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
// =====================
|
// =====================
|
||||||
// Tool Executor
|
// Tool Executor
|
||||||
// =====================
|
// =====================
|
||||||
export async function executeTool(
|
export async function executeTool(
|
||||||
tool: any,
|
tool: any,
|
||||||
args: Record<string, any> = {},
|
args: Record<string, any> = {},
|
||||||
baseUrl: string
|
baseUrl: string
|
||||||
) {
|
) {
|
||||||
const x = tool["x-props"] || {};
|
const x = tool["x-props"] || {};
|
||||||
|
|
||||||
const method = (x.method || "GET").toUpperCase();
|
const method = (x.method || "GET").toUpperCase();
|
||||||
const path = x.path || `/${tool.name}`;
|
const path = x.path || `/${tool.name}`;
|
||||||
const url = `${baseUrl}${path}`;
|
const url = `${baseUrl}${path}`;
|
||||||
|
|
||||||
const opts: RequestInit = {
|
const opts: RequestInit = {
|
||||||
method,
|
method,
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
};
|
};
|
||||||
|
|
||||||
if (["POST", "PUT", "PATCH", "DELETE"].includes(method)) {
|
if (["POST", "PUT", "PATCH", "DELETE"].includes(method)) {
|
||||||
opts.body = JSON.stringify(args || {});
|
opts.body = JSON.stringify(args || {});
|
||||||
}
|
}
|
||||||
|
|
||||||
const res = await fetch(url, opts);
|
const res = await fetch(url, opts);
|
||||||
const contentType = res.headers.get("content-type") || "";
|
const contentType = res.headers.get("content-type") || "";
|
||||||
const data = contentType.includes("application/json")
|
const data = contentType.includes("application/json")
|
||||||
? await res.json()
|
? await res.json()
|
||||||
: await res.text();
|
: await res.text();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: res.ok,
|
success: res.ok,
|
||||||
status: res.status,
|
status: res.status,
|
||||||
method,
|
method,
|
||||||
path,
|
path,
|
||||||
data,
|
data,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// =====================
|
// =====================
|
||||||
// MCP Handler (Async)
|
// MCP Handler (Async)
|
||||||
// =====================
|
// =====================
|
||||||
async function handleMCPRequestAsync(
|
async function handleMCPRequestAsync(
|
||||||
request: JSONRPCRequest
|
request: JSONRPCRequest
|
||||||
): Promise<JSONRPCResponse> {
|
): Promise<JSONRPCResponse> {
|
||||||
const { id, method, params } = request;
|
const { id, method, params } = request;
|
||||||
|
|
||||||
switch (method) {
|
switch (method) {
|
||||||
case "initialize":
|
case "initialize":
|
||||||
return {
|
return {
|
||||||
jsonrpc: "2.0",
|
jsonrpc: "2.0",
|
||||||
id,
|
id,
|
||||||
result: {
|
result: {
|
||||||
protocolVersion: "2024-11-05",
|
protocolVersion: "2024-11-05",
|
||||||
capabilities: { tools: {} },
|
capabilities: { tools: {} },
|
||||||
serverInfo: { name: "elysia-mcp-server", version: "1.0.0" },
|
serverInfo: { name: "elysia-mcp-server", version: "1.0.0" },
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
case "tools/list":
|
case "tools/list":
|
||||||
return {
|
return {
|
||||||
jsonrpc: "2.0",
|
jsonrpc: "2.0",
|
||||||
id,
|
id,
|
||||||
result: {
|
result: {
|
||||||
tools: tools.map(({ name, description, inputSchema, ["x-props"]: x }) => ({
|
tools: tools.map(({ name, description, inputSchema, ["x-props"]: x }) => ({
|
||||||
name,
|
name,
|
||||||
description,
|
description,
|
||||||
inputSchema,
|
inputSchema,
|
||||||
"x-props": x,
|
"x-props": x,
|
||||||
})),
|
})),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
case "tools/call": {
|
case "tools/call": {
|
||||||
const toolName = params?.name;
|
const toolName = params?.name;
|
||||||
const tool = tools.find((t) => t.name === toolName);
|
const tool = tools.find((t) => t.name === toolName);
|
||||||
|
|
||||||
if (!tool) {
|
if (!tool) {
|
||||||
return {
|
return {
|
||||||
jsonrpc: "2.0",
|
jsonrpc: "2.0",
|
||||||
id,
|
id,
|
||||||
error: { code: -32601, message: `Tool '${toolName}' not found` },
|
error: { code: -32601, message: `Tool '${toolName}' not found` },
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const baseUrl =
|
const baseUrl =
|
||||||
process.env.BUN_PUBLIC_BASE_URL || "http://localhost:3000";
|
process.env.BUN_PUBLIC_BASE_URL || "http://localhost:3000";
|
||||||
const result = await executeTool(tool, params?.arguments || {}, baseUrl);
|
const result = await executeTool(tool, params?.arguments || {}, baseUrl);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
jsonrpc: "2.0",
|
jsonrpc: "2.0",
|
||||||
id,
|
id,
|
||||||
result: {
|
result: {
|
||||||
content: [
|
content: [
|
||||||
{
|
{
|
||||||
type: "text",
|
type: "text",
|
||||||
text: JSON.stringify(result, null, 2),
|
text: JSON.stringify(result, null, 2),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
return {
|
return {
|
||||||
jsonrpc: "2.0",
|
jsonrpc: "2.0",
|
||||||
id,
|
id,
|
||||||
error: { code: -32603, message: error.message },
|
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` },
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
case "ping":
|
|
||||||
return { jsonrpc: "2.0", id, result: {} };
|
|
||||||
|
|
||||||
default:
|
|
||||||
return {
|
|
||||||
jsonrpc: "2.0",
|
|
||||||
id,
|
|
||||||
error: { code: -32601, message: `Method '${method}' not found` },
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// =====================
|
// =====================
|
||||||
@@ -150,75 +153,91 @@ async function handleMCPRequestAsync(
|
|||||||
export const MCPRoute = new Elysia({
|
export const MCPRoute = new Elysia({
|
||||||
tags: ["MCP"]
|
tags: ["MCP"]
|
||||||
})
|
})
|
||||||
.post("/mcp", async ({ request, set }) => {
|
.post("/mcp", async ({ request, set }) => {
|
||||||
set.headers["Content-Type"] = "application/json";
|
if (!tools.length) {
|
||||||
set.headers["Access-Control-Allow-Origin"] = "*";
|
tools = await getMcpTools();
|
||||||
|
}
|
||||||
|
set.headers["Content-Type"] = "application/json";
|
||||||
|
set.headers["Access-Control-Allow-Origin"] = "*";
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const body = await request.json();
|
const body = await request.json();
|
||||||
|
|
||||||
if (!Array.isArray(body)) {
|
if (!Array.isArray(body)) {
|
||||||
const res = await handleMCPRequestAsync(body);
|
const res = await handleMCPRequestAsync(body);
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
const results = await Promise.all(
|
const results = await Promise.all(
|
||||||
body.map((req) => handleMCPRequestAsync(req))
|
body.map((req) => handleMCPRequestAsync(req))
|
||||||
);
|
);
|
||||||
return results;
|
return results;
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
set.status = 400;
|
set.status = 400;
|
||||||
return {
|
return {
|
||||||
jsonrpc: "2.0",
|
jsonrpc: "2.0",
|
||||||
id: null,
|
id: null,
|
||||||
error: {
|
error: {
|
||||||
code: -32700,
|
code: -32700,
|
||||||
message: "Parse error",
|
message: "Parse error",
|
||||||
data: error.message,
|
data: error.message,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// Tools list (debug)
|
// Tools list (debug)
|
||||||
.get("/mcp/tools", ({ set }) => {
|
.get("/mcp/tools", async ({ set }) => {
|
||||||
set.headers["Access-Control-Allow-Origin"] = "*";
|
if (!tools.length) {
|
||||||
return {
|
tools = await getMcpTools();
|
||||||
tools: tools.map(({ name, description, inputSchema, ["x-props"]: x }) => ({
|
}
|
||||||
name,
|
set.headers["Access-Control-Allow-Origin"] = "*";
|
||||||
description,
|
return {
|
||||||
inputSchema,
|
tools: tools.map(({ name, description, inputSchema, ["x-props"]: x }) => ({
|
||||||
"x-props": x,
|
name,
|
||||||
})),
|
description,
|
||||||
};
|
inputSchema,
|
||||||
})
|
"x-props": x,
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
})
|
||||||
|
|
||||||
// MCP status
|
// MCP status
|
||||||
.get("/mcp/status", ({ set }) => {
|
.get("/mcp/status", ({ set }) => {
|
||||||
set.headers["Access-Control-Allow-Origin"] = "*";
|
set.headers["Access-Control-Allow-Origin"] = "*";
|
||||||
return { status: "active", timestamp: Date.now() };
|
return { status: "active", timestamp: Date.now() };
|
||||||
})
|
})
|
||||||
|
|
||||||
// Health check
|
// Health check
|
||||||
.get("/health", ({ set }) => {
|
.get("/health", ({ set }) => {
|
||||||
set.headers["Access-Control-Allow-Origin"] = "*";
|
set.headers["Access-Control-Allow-Origin"] = "*";
|
||||||
return { status: "ok", timestamp: Date.now(), tools: tools.length };
|
return { status: "ok", timestamp: Date.now(), tools: tools.length };
|
||||||
})
|
})
|
||||||
|
.get("/mcp/init", async ({ set }) => {
|
||||||
|
|
||||||
// CORS
|
const _tools = await getMcpTools();
|
||||||
.options("/mcp", ({ set }) => {
|
tools = _tools;
|
||||||
set.headers["Access-Control-Allow-Origin"] = "*";
|
return {
|
||||||
set.headers["Access-Control-Allow-Methods"] = "GET,POST,OPTIONS";
|
success: true,
|
||||||
set.headers["Access-Control-Allow-Headers"] =
|
message: "MCP initialized",
|
||||||
"Content-Type,Authorization,X-API-Key";
|
tools: tools.length,
|
||||||
set.status = 204;
|
};
|
||||||
return "";
|
})
|
||||||
})
|
|
||||||
.options("/mcp/tools", ({ set }) => {
|
// CORS
|
||||||
set.headers["Access-Control-Allow-Origin"] = "*";
|
.options("/mcp", ({ set }) => {
|
||||||
set.headers["Access-Control-Allow-Methods"] = "GET,OPTIONS";
|
set.headers["Access-Control-Allow-Origin"] = "*";
|
||||||
set.headers["Access-Control-Allow-Headers"] =
|
set.headers["Access-Control-Allow-Methods"] = "GET,POST,OPTIONS";
|
||||||
"Content-Type,Authorization,X-API-Key";
|
set.headers["Access-Control-Allow-Headers"] =
|
||||||
set.status = 204;
|
"Content-Type,Authorization,X-API-Key";
|
||||||
return "";
|
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 "";
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user