This commit is contained in:
bipproduction
2025-10-28 14:05:53 +08:00
parent 4d1e78468f
commit e0fdb88c32
3 changed files with 923 additions and 363 deletions

View File

@@ -1,102 +1,5 @@
import { Elysia } from "elysia"; import { Elysia } from "elysia";
import { v4 as uuidv4 } from "uuid"; import tools from "./../../../tools.json";
// const API_KEY = process.env.MCP_API_KEY ?? "super-secret-key";
// const PORT = Number(process.env.PORT ?? 3000);
// // =====================
// // Helper Functions
// // =====================
// function isAuthorized(headers: Headers) {
// const authHeader = headers.get("authorization");
// if (authHeader?.startsWith("Bearer ")) {
// const token = authHeader.substring(7);
// return token === API_KEY;
// }
// return headers.get("x-api-key") === API_KEY;
// }
// =====================
// Tools Definition
// =====================
type Tool = {
name: string;
description: string;
inputSchema: {
type: string;
properties: Record<string, any>;
required?: string[];
additionalProperties?: boolean;
$schema?: string;
};
run: (input?: any) => Promise<any>;
};
const tools: Tool[] = [
{
name: "perbekal_darmasaba",
description: "Mengembalikan nama perbekal darmasaba",
inputSchema: {
type: "object",
properties: {},
additionalProperties: true,
$schema: "http://json-schema.org/draft-07/schema#",
},
run: async () => ({ perbekal_darmasaba: "malik kurosaki" }),
},
{
name: "uuid",
description: "Menghasilkan UUID v4 unik.",
inputSchema: {
type: "object",
properties: {},
additionalProperties: true,
$schema: "http://json-schema.org/draft-07/schema#",
},
run: async () => ({ uuid: uuidv4() }),
},
{
name: "echo",
description: "Mengembalikan data yang dikirim.",
inputSchema: {
type: "object",
properties: {
input: {
type: "string",
description: "Message to echo back",
},
},
required: ["input"],
additionalProperties: true,
$schema: "http://json-schema.org/draft-07/schema#",
},
run: async (input) => ({ echo: input }),
},
{
name: "Calculator",
description: "Useful for getting the result of a math expression. The input to this tool should be a valid mathematical expression that could be executed by a simple calculator.",
inputSchema: {
type: "object",
properties: {
input: {
type: "string",
},
},
required: ["input"],
additionalProperties: true,
$schema: "http://json-schema.org/draft-07/schema#",
},
run: async (input) => {
try {
// Simple math evaluation (be careful in production!)
const result = Function(`"use strict"; return (${input.input})`)();
return { result: String(result) };
} catch (error: any) {
throw new Error(`Invalid expression: ${error.message}`);
}
},
},
];
// ===================== // =====================
// MCP Protocol Types // MCP Protocol Types
@@ -119,16 +22,50 @@ type JSONRPCResponse = {
}; };
}; };
type JSONRPCNotification = { // =====================
jsonrpc: "2.0"; // Tool Executor
method: string; // =====================
params?: any; 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 // MCP Handler (Async)
// ===================== // =====================
function handleMCPRequest(request: JSONRPCRequest): JSONRPCResponse { async function handleMCPRequestAsync(
request: JSONRPCRequest
): Promise<JSONRPCResponse> {
const { id, method, params } = request; const { id, method, params } = request;
switch (method) { switch (method) {
@@ -138,13 +75,8 @@ function handleMCPRequest(request: JSONRPCRequest): JSONRPCResponse {
id, id,
result: { result: {
protocolVersion: "2024-11-05", protocolVersion: "2024-11-05",
capabilities: { capabilities: { tools: {} },
tools: {}, serverInfo: { name: "elysia-mcp-server", version: "1.0.0" },
},
serverInfo: {
name: "elysia-mcp-server",
version: "1.0.0",
},
}, },
}; };
@@ -153,15 +85,16 @@ function handleMCPRequest(request: JSONRPCRequest): JSONRPCResponse {
jsonrpc: "2.0", jsonrpc: "2.0",
id, id,
result: { result: {
tools: tools.map(({ name, description, inputSchema }) => ({ tools: tools.map(({ name, description, inputSchema, ["x-props"]: x }) => ({
name, name,
description, description,
inputSchema, inputSchema,
"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);
@@ -169,18 +102,14 @@ function handleMCPRequest(request: JSONRPCRequest): JSONRPCResponse {
return { return {
jsonrpc: "2.0", jsonrpc: "2.0",
id, id,
error: { error: { code: -32601, message: `Tool '${toolName}' not found` },
code: -32601,
message: `Tool '${toolName}' not found`,
},
}; };
} }
try { try {
// Note: This is synchronous for simplicity const baseUrl =
// In real implementation, you'd need to handle async properly process.env.BUN_PUBLIC_BASE_URL || "http://localhost:3000";
let result: any; const result = await executeTool(tool, params?.arguments || {}, baseUrl);
tool.run(params?.arguments || {}).then((r) => (result = r));
return { return {
jsonrpc: "2.0", jsonrpc: "2.0",
@@ -189,7 +118,7 @@ function handleMCPRequest(request: JSONRPCRequest): JSONRPCResponse {
content: [ content: [
{ {
type: "text", type: "text",
text: JSON.stringify(result || { pending: true }), text: JSON.stringify(result, null, 2),
}, },
], ],
}, },
@@ -198,111 +127,45 @@ function handleMCPRequest(request: JSONRPCRequest): JSONRPCResponse {
return { return {
jsonrpc: "2.0", jsonrpc: "2.0",
id, id,
error: { error: { code: -32603, message: error.message },
code: -32603,
message: error.message,
},
}; };
} }
}
case "ping": case "ping":
return { return { jsonrpc: "2.0", id, result: {} };
jsonrpc: "2.0",
id,
result: {},
};
default: default:
return { return {
jsonrpc: "2.0", jsonrpc: "2.0",
id, id,
error: { error: { code: -32601, message: `Method '${method}' not found` },
code: -32601,
message: `Method '${method}' not found`,
},
}; };
} }
} }
async function handleMCPRequestAsync(request: JSONRPCRequest): Promise<JSONRPCResponse> {
const { id, method, params } = request;
if (method === "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 result = await tool.run(params?.arguments || {});
return {
jsonrpc: "2.0",
id,
result: {
content: [
{
type: "text",
text: JSON.stringify(result),
},
],
},
};
} catch (error: any) {
return {
jsonrpc: "2.0",
id,
error: {
code: -32603,
message: error.message,
},
};
}
}
// For other methods, use sync handler
return handleMCPRequest(request);
}
// ===================== // =====================
// Server Initialization // Elysia MCP Server
// ===================== // =====================
export const MCPRoute = new Elysia() export const MCPRoute = new Elysia({
// ===================== tags: ["MCP"]
// MCP HTTP Streamable Endpoint })
// ===================== .post("/mcp", async ({ request, set }) => {
.post("/mcp/:sessionId", async ({ params, request, set }) => {
set.headers["Content-Type"] = "application/json"; set.headers["Content-Type"] = "application/json";
set.headers["Access-Control-Allow-Origin"] = "*"; set.headers["Access-Control-Allow-Origin"] = "*";
// Optional: Check authorization
// if (!isAuthorized(request.headers)) {
// set.status = 401;
// return { error: "Unauthorized" };
// }
try { try {
const body = await request.json(); const body = await request.json();
// Handle single request
if (!Array.isArray(body)) { if (!Array.isArray(body)) {
const response = await handleMCPRequestAsync(body as JSONRPCRequest); const res = await handleMCPRequestAsync(body);
return response; return res;
} }
// Handle batch requests const results = await Promise.all(
const responses = await Promise.all( body.map((req) => handleMCPRequestAsync(req))
body.map((req) => handleMCPRequestAsync(req as JSONRPCRequest))
); );
return responses; return results;
} catch (error: any) { } catch (error: any) {
set.status = 400; set.status = 400;
return { return {
@@ -317,60 +180,45 @@ export const MCPRoute = new Elysia()
} }
}) })
// ===================== // Tools list (debug)
// Simple tools list endpoint (for debugging) .get("/mcp/tools", ({ set }) => {
// =====================
.get("/mcp/:sessionId/tools", ({ set }) => {
set.headers["Access-Control-Allow-Origin"] = "*"; set.headers["Access-Control-Allow-Origin"] = "*";
return { return {
data: tools.map(({ name, description, inputSchema }) => ({ tools: tools.map(({ name, description, inputSchema, ["x-props"]: x }) => ({
name, name,
value: name,
description, description,
inputSchema, inputSchema,
"x-props": x,
})), })),
}; };
}) })
// ===================== // MCP status
// Session Status .get("/mcp/status", ({ set }) => {
// =====================
.get("/mcp/:sessionId/status", ({ params, set }) => {
set.headers["Access-Control-Allow-Origin"] = "*"; set.headers["Access-Control-Allow-Origin"] = "*";
return { return { status: "active", timestamp: Date.now() };
sessionId: params.sessionId,
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 { return { status: "ok", timestamp: Date.now(), tools: tools.length };
status: "ok",
timestamp: Date.now(),
tools: tools.length,
};
}) })
// ===================== // CORS
// CORS preflight .options("/mcp", ({ set }) => {
// =====================
.options("/mcp/:sessionId", ({ set }) => {
set.headers["Access-Control-Allow-Origin"] = "*"; set.headers["Access-Control-Allow-Origin"] = "*";
set.headers["Access-Control-Allow-Methods"] = "GET,POST,OPTIONS"; set.headers["Access-Control-Allow-Methods"] = "GET,POST,OPTIONS";
set.headers["Access-Control-Allow-Headers"] = "Content-Type,Authorization,X-API-Key"; set.headers["Access-Control-Allow-Headers"] =
"Content-Type,Authorization,X-API-Key";
set.status = 204; set.status = 204;
return ""; return "";
}) })
.options("/mcp/tools", ({ set }) => {
.options("/mcp/:sessionId/tools", ({ set }) => {
set.headers["Access-Control-Allow-Origin"] = "*"; set.headers["Access-Control-Allow-Origin"] = "*";
set.headers["Access-Control-Allow-Methods"] = "GET,OPTIONS"; set.headers["Access-Control-Allow-Methods"] = "GET,OPTIONS";
set.headers["Access-Control-Allow-Headers"] = "Content-Type,Authorization,X-API-Key"; set.headers["Access-Control-Allow-Headers"] =
"Content-Type,Authorization,X-API-Key";
set.status = 204; set.status = 204;
return ""; return "";
}); });

612
tools.json Normal file
View File

@@ -0,0 +1,612 @@
[
{
"name": "apikey_create",
"description": "create api key by user",
"x-props": {
"method": "POST",
"path": "/api/apikey/create",
"operationId": "postApiApikeyCreate",
"tag": "apikey",
"deprecated": false,
"summary": "create"
},
"inputSchema": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"description": {
"type": "string"
},
"expiredAt": {
"format": "date-time",
"type": "string"
}
},
"required": [
"name",
"description"
],
"additionalProperties": true,
"$schema": "http://json-schema.org/draft-07/schema#"
}
},
{
"name": "apikey_list",
"description": "get api key list by user",
"x-props": {
"method": "GET",
"path": "/api/apikey/list",
"operationId": "getApiApikeyList",
"tag": "apikey",
"deprecated": false,
"summary": "list"
},
"inputSchema": {
"type": "object",
"properties": {},
"additionalProperties": true,
"$schema": "http://json-schema.org/draft-07/schema#"
}
},
{
"name": "apikey_delete",
"description": "delete api key by id",
"x-props": {
"method": "DELETE",
"path": "/api/apikey/delete",
"operationId": "deleteApiApikeyDelete",
"tag": "apikey",
"deprecated": false,
"summary": "delete"
},
"inputSchema": {
"type": "object",
"properties": {
"id": {
"type": "string"
}
},
"required": [
"id"
],
"additionalProperties": true,
"$schema": "http://json-schema.org/draft-07/schema#"
}
},
{
"name": "darmasaba_repos",
"description": "get list of repositories",
"x-props": {
"method": "GET",
"path": "/api/darmasaba/repos",
"operationId": "getApiDarmasabaRepos",
"tag": "darmasaba",
"deprecated": false,
"summary": "repos"
},
"inputSchema": {
"type": "object",
"properties": {},
"additionalProperties": true,
"$schema": "http://json-schema.org/draft-07/schema#"
}
},
{
"name": "darmasaba_ls",
"description": "get list of dir in darmasaba",
"x-props": {
"method": "GET",
"path": "/api/darmasaba/ls",
"operationId": "getApiDarmasabaLs",
"tag": "darmasaba",
"deprecated": false,
"summary": "ls"
},
"inputSchema": {
"type": "object",
"properties": {},
"additionalProperties": true,
"$schema": "http://json-schema.org/draft-07/schema#"
}
},
{
"name": "darmasaba_ls_by_dir",
"description": "get list of files in darmasaba/<dir>",
"x-props": {
"method": "GET",
"path": "/api/darmasaba/ls/{dir}",
"operationId": "getApiDarmasabaLsByDir",
"tag": "darmasaba",
"deprecated": false,
"summary": "ls"
},
"inputSchema": {
"type": "object",
"properties": {},
"additionalProperties": true,
"$schema": "http://json-schema.org/draft-07/schema#"
}
},
{
"name": "darmasaba_file_by_dir_by_file_name",
"description": "get content of file in darmasaba/<dir>/<file_name>",
"x-props": {
"method": "GET",
"path": "/api/darmasaba/file/{dir}/{file_name}",
"operationId": "getApiDarmasabaFileByDirByFile_name",
"tag": "darmasaba",
"deprecated": false,
"summary": "file"
},
"inputSchema": {
"type": "object",
"properties": {},
"additionalProperties": true,
"$schema": "http://json-schema.org/draft-07/schema#"
}
},
{
"name": "darmasaba_list_pengetahuan_umum",
"description": "get list of files in darmasaba/pengetahuan-umum",
"x-props": {
"method": "GET",
"path": "/api/darmasaba/list-pengetahuan-umum",
"operationId": "getApiDarmasabaList-pengetahuan-umum",
"tag": "darmasaba",
"deprecated": false,
"summary": "list-pengetahuan-umum"
},
"inputSchema": {
"type": "object",
"properties": {},
"additionalProperties": true,
"$schema": "http://json-schema.org/draft-07/schema#"
}
},
{
"name": "darmasaba_pengetahuan_umum_by_file_name",
"description": "get content of file in darmasaba/pengetahuan-umum/<file_name>",
"x-props": {
"method": "GET",
"path": "/api/darmasaba/pengetahuan-umum/{file_name}",
"operationId": "getApiDarmasabaPengetahuan-umumByFile_name",
"tag": "darmasaba",
"deprecated": false,
"summary": "pengetahuan-umum"
},
"inputSchema": {
"type": "object",
"properties": {},
"additionalProperties": true,
"$schema": "http://json-schema.org/draft-07/schema#"
}
},
{
"name": "darmasaba_buat_pengaduan",
"description": "tool untuk membuat pengaduan atau pelaporan warga kepada desa darmasaba",
"x-props": {
"method": "POST",
"path": "/api/darmasaba/buat-pengaduan",
"operationId": "postApiDarmasabaBuat-pengaduan",
"tag": "darmasaba",
"deprecated": false,
"summary": "buat-pengaduan atau pelaporan"
},
"inputSchema": {
"type": "object",
"properties": {
"jenis_laporan": {
"minLength": 1,
"error": "jenis laporan harus diisi",
"type": "string"
},
"name": {
"minLength": 1,
"error": "name harus diisi",
"type": "string"
},
"phone": {
"minLength": 1,
"error": "phone harus diisi",
"type": "string"
},
"detail": {
"minLength": 1,
"error": "detail harus diisi",
"type": "string"
}
},
"required": [
"jenis_laporan",
"name",
"phone",
"detail"
],
"additionalProperties": true,
"$schema": "http://json-schema.org/draft-07/schema#"
}
},
{
"name": "darmasaba_status_pengaduan",
"description": "melikat status pengaduan dari user",
"x-props": {
"method": "POST",
"path": "/api/darmasaba/status-pengaduan",
"operationId": "postApiDarmasabaStatus-pengaduan",
"tag": "darmasaba",
"deprecated": false,
"summary": "lihat status pengaduan"
},
"inputSchema": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"phone": {
"type": "string"
}
},
"required": [
"name",
"phone"
],
"additionalProperties": true,
"$schema": "http://json-schema.org/draft-07/schema#"
}
},
{
"name": "credential_create",
"description": "create credential",
"x-props": {
"method": "POST",
"path": "/api/credential/create",
"operationId": "postApiCredentialCreate",
"tag": "credential",
"deprecated": false,
"summary": "create"
},
"inputSchema": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"value": {
"type": "string"
}
},
"required": [
"name",
"value"
],
"additionalProperties": true,
"$schema": "http://json-schema.org/draft-07/schema#"
}
},
{
"name": "credential_list",
"description": "get credential list",
"x-props": {
"method": "GET",
"path": "/api/credential/list",
"operationId": "getApiCredentialList",
"tag": "credential",
"deprecated": false,
"summary": "list"
},
"inputSchema": {
"type": "object",
"properties": {},
"additionalProperties": true,
"$schema": "http://json-schema.org/draft-07/schema#"
}
},
{
"name": "credential_rm",
"description": "delete credential by id",
"x-props": {
"method": "DELETE",
"path": "/api/credential/rm",
"operationId": "deleteApiCredentialRm",
"tag": "credential",
"deprecated": false,
"summary": "rm"
},
"inputSchema": {
"type": "object",
"properties": {
"id": {
"type": "string"
}
},
"required": [
"id"
],
"additionalProperties": true,
"$schema": "http://json-schema.org/draft-07/schema#"
}
},
{
"name": "user_find",
"description": "find user",
"x-props": {
"method": "GET",
"path": "/api/user/find",
"operationId": "getApiUserFind",
"tag": "user",
"deprecated": false,
"summary": "find"
},
"inputSchema": {
"type": "object",
"properties": {},
"additionalProperties": true,
"$schema": "http://json-schema.org/draft-07/schema#"
}
},
{
"name": "user_upsert",
"description": "upsert user",
"x-props": {
"method": "POST",
"path": "/api/user/upsert",
"operationId": "postApiUserUpsert",
"tag": "user",
"deprecated": false,
"summary": "upsert"
},
"inputSchema": {
"type": "object",
"properties": {
"name": {
"minLength": 1,
"error": "name is required",
"type": "string"
},
"phone": {
"minLength": 1,
"error": "phone is required",
"type": "string"
}
},
"required": [
"name",
"phone"
],
"additionalProperties": true,
"$schema": "http://json-schema.org/draft-07/schema#"
}
},
{
"name": "layanan_list",
"description": "Returns the list of all available public services.",
"x-props": {
"method": "GET",
"path": "/api/layanan/list",
"operationId": "getApiLayananList",
"tag": "layanan",
"deprecated": false,
"summary": "List Layanan"
},
"inputSchema": {
"type": "object",
"properties": {},
"additionalProperties": true,
"$schema": "http://json-schema.org/draft-07/schema#"
}
},
{
"name": "layanan_create_ktp",
"description": "Create a new service request for KTP or KK.",
"x-props": {
"method": "POST",
"path": "/api/layanan/create-ktp",
"operationId": "postApiLayananCreate-ktp",
"tag": "layanan",
"deprecated": false,
"summary": "Create Layanan KTP/KK"
},
"inputSchema": {
"type": "object",
"properties": {
"jenis": {
"anyOf": [
{
"const": "ktp",
"type": "string"
},
{
"const": "kk",
"type": "string"
}
]
},
"nama": {
"minLength": 3,
"description": "Nama pemohon layanan",
"type": "string"
},
"deskripsi": {
"minLength": 5,
"description": "Deskripsi singkat permohonan layanan",
"type": "string"
}
},
"required": [
"jenis",
"nama",
"deskripsi"
],
"additionalProperties": true,
"$schema": "http://json-schema.org/draft-07/schema#"
}
},
{
"name": "layanan_status_ktp",
"description": "Retrieve the current status of a KTP/KK request by unique ID.",
"x-props": {
"method": "POST",
"path": "/api/layanan/status-ktp",
"operationId": "postApiLayananStatus-ktp",
"tag": "layanan",
"deprecated": false,
"summary": "Cek Status KTP"
},
"inputSchema": {
"type": "object",
"properties": {
"uniqid": {
"description": "Unique ID layanan",
"type": "string"
}
},
"required": [
"uniqid"
],
"additionalProperties": true,
"$schema": "http://json-schema.org/draft-07/schema#"
}
},
{
"name": "aduan_create",
"description": "create aduan",
"x-props": {
"method": "POST",
"path": "/api/aduan/create",
"operationId": "postApiAduanCreate",
"tag": "aduan",
"deprecated": false,
"summary": "create"
},
"inputSchema": {
"type": "object",
"properties": {
"title": {
"type": "string"
},
"description": {
"type": "string"
}
},
"required": [
"title",
"description"
],
"additionalProperties": true,
"$schema": "http://json-schema.org/draft-07/schema#"
}
},
{
"name": "aduan_aduan_sampah",
"description": "tool untuk membuat aduan sampah liar",
"x-props": {
"method": "POST",
"path": "/api/aduan/aduan-sampah",
"operationId": "postApiAduanAduan-sampah",
"tag": "aduan",
"deprecated": false,
"summary": "aduan sampah"
},
"inputSchema": {
"type": "object",
"properties": {
"judul": {
"type": "string"
},
"deskripsi": {
"type": "string"
}
},
"required": [
"judul",
"deskripsi"
],
"additionalProperties": true,
"$schema": "http://json-schema.org/draft-07/schema#"
}
},
{
"name": "aduan_list_aduan_sampah",
"description": "tool untuk melihat list aduan sampah liar",
"x-props": {
"method": "GET",
"path": "/api/aduan/list-aduan-sampah",
"operationId": "getApiAduanList-aduan-sampah",
"tag": "aduan",
"deprecated": false,
"summary": "list aduan sampah"
},
"inputSchema": {
"type": "object",
"properties": {},
"additionalProperties": true,
"$schema": "http://json-schema.org/draft-07/schema#"
}
},
{
"name": "auth_login",
"description": "Login with phone; auto-register if not found",
"x-props": {
"method": "POST",
"path": "/auth/login",
"operationId": "postAuthLogin",
"tag": "auth",
"deprecated": false,
"summary": "login"
},
"inputSchema": {
"type": "object",
"properties": {
"email": {
"type": "string"
},
"password": {
"type": "string"
}
},
"required": [
"email",
"password"
],
"additionalProperties": true,
"$schema": "http://json-schema.org/draft-07/schema#"
}
},
{
"name": "auth_logout",
"description": "Logout (clear token cookie)",
"x-props": {
"method": "DELETE",
"path": "/auth/logout",
"operationId": "deleteAuthLogout",
"tag": "auth",
"deprecated": false,
"summary": "logout"
},
"inputSchema": {
"type": "object",
"properties": {},
"additionalProperties": true,
"$schema": "http://json-schema.org/draft-07/schema#"
}
},
{
"name": "health",
"description": "Execute GET /health",
"x-props": {
"method": "GET",
"path": "/health",
"operationId": "getHealth",
"deprecated": false
},
"inputSchema": {
"type": "object",
"properties": {},
"additionalProperties": true,
"$schema": "http://json-schema.org/draft-07/schema#"
}
}
]

100
xxx/tool_convert.ts Normal file
View File

@@ -0,0 +1,100 @@
import _ from "lodash";
import { v4 as uuidv4 } from "uuid";
interface McpTool {
name: string;
description: string;
inputSchema: any;
"x-props": {
method: string;
path: string;
operationId?: string;
tag?: string;
deprecated?: boolean;
summary?: string;
};
}
/**
* 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`.
*/
export function convertOpenApiToMcpTools(openApiJson: any, baseUrl: string = ""): McpTool[] {
const tools: McpTool[] = [];
const paths = openApiJson.paths || {};
for (const [path, methods] of Object.entries(paths)) {
// ✅ skip semua path internal MCP
if (path.startsWith("/mcp")) continue;
for (const [method, operation] of Object.entries<any>(methods as any)) {
const rawName = _.snakeCase(operation.operationId || `${method}_${path}`) || "unnamed_tool";
const name = cleanToolName(rawName);
const summary = operation.summary || `Execute ${method.toUpperCase()} ${path}`;
const description =
operation.description ||
operation.summary ||
`Execute ${method.toUpperCase()} ${path}`;
const schema =
operation.requestBody?.content?.["application/json"]?.schema || {
type: "object",
properties: {},
additionalProperties: true,
};
const tool: McpTool = {
name,
description,
"x-props": {
method: method.toUpperCase(),
path,
operationId: operation.operationId,
tag: Array.isArray(operation.tags) ? operation.tags[0] : undefined,
deprecated: operation.deprecated || false,
summary: operation.summary, // ✅ tambahkan summary ke metadata
},
inputSchema: {
...schema,
additionalProperties: true,
$schema: "http://json-schema.org/draft-07/schema#",
},
};
tools.push(tool);
}
}
return tools;
}
/**
* Bersihkan nama agar valid untuk digunakan sebagai tool name
* - hapus karakter spesial
* - ubah slash jadi underscore
* - hilangkan prefix umum (get_, post_, api_, dll)
* - rapikan underscore berganda
*/
function cleanToolName(name: string): string {
return name
.replace(/[{}]/g, "")
.replace(/[^a-zA-Z0-9_]/g, "_")
.replace(/_+/g, "_")
.replace(/^_|_$/g, "")
.replace(/^(get|post|put|delete|patch|api)_/i, "")
.replace(/^(get_|post_|put_|delete_|patch_|api_)+/gi, "")
.replace(/(^_|_$)/g, "");
}
// === Contoh Pemakaian ===
// import openApiJson from "./openapi.json";
// const tools = convertOpenApiToMcpTools(openApiJson, "https://api.wibudev.com");
// console.log(JSON.stringify(tools, null, 2));
if (import.meta.main) {
const data = await fetch("http://localhost:3000/docs/json");
const openApiJson = await data.json();
const tools = convertOpenApiToMcpTools(openApiJson, "http://localhost:3000");
Bun.write("./tools.json", JSON.stringify(tools, null, 2));
}