From 6a9ce5431190f25f0b5892b4f6c7b25187bf2e3e Mon Sep 17 00:00:00 2001 From: amaliadwiy Date: Tue, 28 Oct 2025 15:00:57 +0800 Subject: [PATCH] update --- src/server/lib/mcp_tool_convert.ts | 105 +++++ src/server/routes/mcp_route.ts | 329 +++++----------- tools.json | 612 +++++++++++++++++++++++++++++ 3 files changed, 815 insertions(+), 231 deletions(-) create mode 100644 src/server/lib/mcp_tool_convert.ts create mode 100644 tools.json diff --git a/src/server/lib/mcp_tool_convert.ts b/src/server/lib/mcp_tool_convert.ts new file mode 100644 index 0000000..614f420 --- /dev/null +++ b/src/server/lib/mcp_tool_convert.ts @@ -0,0 +1,105 @@ +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): 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(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)); + +export async function getMcpTools(){ + const data = await fetch(`${process.env.BUN_PUBLIC_BASE_URL}/docs/json`); + const openApiJson = await data.json(); + const tools = convertOpenApiToMcpTools(openApiJson); + return tools; +} + +if (import.meta.main) { + const tools = await getMcpTools(); + Bun.write("./tools.json", JSON.stringify(tools, null, 2)); +} diff --git a/src/server/routes/mcp_route.ts b/src/server/routes/mcp_route.ts index 842013b..8fd4086 100644 --- a/src/server/routes/mcp_route.ts +++ b/src/server/routes/mcp_route.ts @@ -1,102 +1,8 @@ import { Elysia } from "elysia"; -import { v4 as uuidv4 } from "uuid"; +import { getMcpTools } from "../lib/mcp_tool_convert"; +// 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; - required?: string[]; - additionalProperties?: boolean; - $schema?: string; - }; - run: (input?: any) => Promise; -}; - -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}`); - } - }, - }, -]; +var tools = [] as any[]; // ===================== // MCP Protocol Types @@ -119,16 +25,50 @@ type JSONRPCResponse = { }; }; -type JSONRPCNotification = { - jsonrpc: "2.0"; - method: string; - params?: any; -}; +// ===================== +// Tool Executor +// ===================== +export async function executeTool( + tool: any, + args: Record = {}, + 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 { const { id, method, params } = request; switch (method) { @@ -138,13 +78,8 @@ function handleMCPRequest(request: JSONRPCRequest): JSONRPCResponse { id, result: { protocolVersion: "2024-11-05", - capabilities: { - tools: {}, - }, - serverInfo: { - name: "elysia-mcp-server", - version: "1.0.0", - }, + capabilities: { tools: {} }, + serverInfo: { name: "elysia-mcp-server", version: "1.0.0" }, }, }; @@ -153,15 +88,16 @@ function handleMCPRequest(request: JSONRPCRequest): JSONRPCResponse { jsonrpc: "2.0", id, result: { - tools: tools.map(({ name, description, inputSchema }) => ({ + tools: tools.map(({ name, description, inputSchema, ["x-props"]: x }) => ({ name, description, inputSchema, + "x-props": x, })), }, }; - case "tools/call": + case "tools/call": { const toolName = params?.name; const tool = tools.find((t) => t.name === toolName); @@ -169,18 +105,14 @@ function handleMCPRequest(request: JSONRPCRequest): JSONRPCResponse { return { jsonrpc: "2.0", id, - error: { - code: -32601, - message: `Tool '${toolName}' not found`, - }, + error: { code: -32601, message: `Tool '${toolName}' not found` }, }; } try { - // Note: This is synchronous for simplicity - // In real implementation, you'd need to handle async properly - let result: any; - tool.run(params?.arguments || {}).then((r) => (result = r)); + const baseUrl = + process.env.BUN_PUBLIC_BASE_URL || "http://localhost:3000"; + const result = await executeTool(tool, params?.arguments || {}, baseUrl); return { jsonrpc: "2.0", @@ -189,7 +121,7 @@ function handleMCPRequest(request: JSONRPCRequest): JSONRPCResponse { content: [ { type: "text", - text: JSON.stringify(result || { pending: true }), + text: JSON.stringify(result, null, 2), }, ], }, @@ -198,111 +130,48 @@ function handleMCPRequest(request: JSONRPCRequest): JSONRPCResponse { return { jsonrpc: "2.0", id, - error: { - code: -32603, - message: error.message, - }, + error: { code: -32603, message: error.message }, }; } + } case "ping": - return { - jsonrpc: "2.0", - id, - result: {}, - }; + return { jsonrpc: "2.0", id, result: {} }; default: return { jsonrpc: "2.0", id, - error: { - code: -32601, - message: `Method '${method}' not found`, - }, + error: { code: -32601, message: `Method '${method}' not found` }, }; } } -async function handleMCPRequestAsync(request: JSONRPCRequest): Promise { - 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() - // ===================== - // MCP HTTP Streamable Endpoint - // ===================== - .post("/mcp/:sessionId", async ({ params, request, set }) => { +export const MCPRoute = new Elysia({ + tags: ["MCP"] +}) + .post("/mcp", async ({ request, set }) => { + if (!tools.length) { + tools = await getMcpTools(); + } set.headers["Content-Type"] = "application/json"; set.headers["Access-Control-Allow-Origin"] = "*"; - // Optional: Check authorization - // if (!isAuthorized(request.headers)) { - // set.status = 401; - // return { error: "Unauthorized" }; - // } - try { const body = await request.json(); - // Handle single request if (!Array.isArray(body)) { - const response = await handleMCPRequestAsync(body as JSONRPCRequest); - return response; + const res = await handleMCPRequestAsync(body); + return res; } - // Handle batch requests - const responses = await Promise.all( - body.map((req) => handleMCPRequestAsync(req as JSONRPCRequest)) + const results = await Promise.all( + body.map((req) => handleMCPRequestAsync(req)) ); - return responses; + return results; } catch (error: any) { set.status = 400; return { @@ -317,60 +186,58 @@ export const MCPRoute = new Elysia() } }) - // ===================== - // Simple tools list endpoint (for debugging) - // ===================== - .get("/mcp/:sessionId/tools", ({ set }) => { + // Tools list (debug) + .get("/mcp/tools", async ({ set }) => { + if (!tools.length) { + tools = await getMcpTools(); + } set.headers["Access-Control-Allow-Origin"] = "*"; return { - data: tools.map(({ name, description, inputSchema }) => ({ + tools: tools.map(({ name, description, inputSchema, ["x-props"]: x }) => ({ name, - value: name, description, inputSchema, + "x-props": x, })), }; }) - // ===================== - // Session Status - // ===================== - .get("/mcp/:sessionId/status", ({ params, set }) => { + // MCP status + .get("/mcp/status", ({ set }) => { set.headers["Access-Control-Allow-Origin"] = "*"; - return { - sessionId: params.sessionId, - status: "active", - timestamp: Date.now(), - }; + return { status: "active", timestamp: Date.now() }; }) - // ===================== - // Health Check - // ===================== + // 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(); + tools = _tools; return { - status: "ok", - timestamp: Date.now(), + success: true, + message: "MCP initialized", tools: tools.length, }; }) - // ===================== - // CORS preflight - // ===================== - .options("/mcp/:sessionId", ({ set }) => { + // 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.headers["Access-Control-Allow-Headers"] = + "Content-Type,Authorization,X-API-Key"; set.status = 204; return ""; }) - - .options("/mcp/:sessionId/tools", ({ set }) => { + .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.headers["Access-Control-Allow-Headers"] = + "Content-Type,Authorization,X-API-Key"; set.status = 204; return ""; - }); \ No newline at end of file + }); diff --git a/tools.json b/tools.json new file mode 100644 index 0000000..4195020 --- /dev/null +++ b/tools.json @@ -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/", + "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//", + "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/", + "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#" + } + } +] \ No newline at end of file