Compare commits
23 Commits
join
...
amalia/29-
| Author | SHA1 | Date | |
|---|---|---|---|
| 06feeae9a5 | |||
| b102643675 | |||
|
|
b2f8dc3714 | ||
| 578ad51726 | |||
|
|
8a3eaa2193 | ||
|
|
cae9ed7282 | ||
|
|
2003364bff | ||
|
|
5dc83dbd35 | ||
|
|
9c96031574 | ||
|
|
841fca55d1 | ||
|
|
e009e27d47 | ||
|
|
b52da1c4bd | ||
|
|
3edcc52e74 | ||
|
|
17bd04e389 | ||
|
|
69377a3491 | ||
|
|
3e2245da29 | ||
|
|
65b24ab031 | ||
| 78b1c0ee2d | |||
| 7cc49655b4 | |||
| 6a9ce54311 | |||
| bf0083e678 | |||
|
|
fb5a859ebc | ||
|
|
e0fdb88c32 |
3
bun.lock
3
bun.lock
@@ -4,6 +4,7 @@
|
||||
"": {
|
||||
"name": "bun-react-template",
|
||||
"dependencies": {
|
||||
"@elysiajs/bearer": "^1.4.1",
|
||||
"@elysiajs/cors": "^1.4.0",
|
||||
"@elysiajs/eden": "^1.4.4",
|
||||
"@elysiajs/jwt": "^1.4.0",
|
||||
@@ -48,6 +49,8 @@
|
||||
|
||||
"@borewit/text-codec": ["@borewit/text-codec@0.1.1", "", {}, "sha512-5L/uBxmjaCIX5h8Z+uu+kA9BQLkc/Wl06UGR5ajNRxu+/XjonB5i8JpgFMrPj3LXTCPA0pv8yxUvbUi+QthGGA=="],
|
||||
|
||||
"@elysiajs/bearer": ["@elysiajs/bearer@1.4.1", "", { "peerDependencies": { "elysia": ">= 1.4.0" } }, "sha512-vLLSMEVsLKp/8p/eoAbXZdXKRs1jEQO4OkrfcKM2x8FkiK2aKNcFgLID45bH+6rYbCf8Ihg0NKw59zxMLl43OQ=="],
|
||||
|
||||
"@elysiajs/cors": ["@elysiajs/cors@1.4.0", "", { "peerDependencies": { "elysia": ">= 1.4.0" } }, "sha512-pb0SCzBfFbFSYA/U40HHO7R+YrcXBJXOWgL20eSViK33ol1e20ru2/KUaZYo5IMUn63yaTJI/bQERuQ+77ND8g=="],
|
||||
|
||||
"@elysiajs/eden": ["@elysiajs/eden@1.4.4", "", { "peerDependencies": { "elysia": ">= 1.4.0-exp.0" } }, "sha512-/LVqflmgUcCiXb8rz1iRq9Rx3SWfIV/EkoNqDFGMx+TvOyo8QHAygFXAVQz7RHs+jk6n6mEgpI6KlKBANoErsQ=="],
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
"lint": "bunx oxlint src"
|
||||
},
|
||||
"dependencies": {
|
||||
"@elysiajs/bearer": "^1.4.1",
|
||||
"@elysiajs/cors": "^1.4.0",
|
||||
"@elysiajs/eden": "^1.4.4",
|
||||
"@elysiajs/jwt": "^1.4.0",
|
||||
|
||||
@@ -24,6 +24,7 @@ model User {
|
||||
email String? @unique
|
||||
password String?
|
||||
phone String? @unique
|
||||
isActive Boolean @default(true)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
ApiKey ApiKey[]
|
||||
@@ -82,7 +83,7 @@ model HistoryPengaduan {
|
||||
id String @id @default(cuid())
|
||||
Pengaduan Pengaduan @relation(fields: [idPengaduan], references: [id])
|
||||
idPengaduan String
|
||||
User User? @relation(fields: [idUser], references: [id])
|
||||
User User? @relation(fields: [idUser], references: [id])
|
||||
idUser String?
|
||||
deskripsi String?
|
||||
status StatusPengaduan @default(antrian)
|
||||
@@ -94,6 +95,7 @@ model Warga {
|
||||
id String @id @default(cuid())
|
||||
name String?
|
||||
phone String? @unique
|
||||
isActive Boolean @default(true)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
Pengaduan Pengaduan[]
|
||||
|
||||
@@ -1,5 +1,28 @@
|
||||
import { prisma } from "@/server/lib/prisma";
|
||||
|
||||
const category = [
|
||||
{
|
||||
id: "lainnya",
|
||||
name: "Lainnya"
|
||||
},
|
||||
{
|
||||
id: "kebersihan",
|
||||
name: "Kebersihan"
|
||||
},
|
||||
{
|
||||
id: "keamanan",
|
||||
name: "Keamanan"
|
||||
},
|
||||
{
|
||||
id: "pelayanan",
|
||||
name: "Pelayanan"
|
||||
},
|
||||
{
|
||||
id: "infrastruktur",
|
||||
name: "Infrastruktur"
|
||||
},
|
||||
]
|
||||
|
||||
const role = [
|
||||
{
|
||||
id: "developer",
|
||||
@@ -17,6 +40,7 @@ const role = [
|
||||
|
||||
const user = [
|
||||
{
|
||||
id: "bip",
|
||||
name: "Bip",
|
||||
email: "bip@bip.com",
|
||||
password: "bip",
|
||||
@@ -25,16 +49,6 @@ const user = [
|
||||
];
|
||||
|
||||
(async () => {
|
||||
for (const u of user) {
|
||||
await prisma.user.upsert({
|
||||
where: { email: u.email },
|
||||
create: u,
|
||||
update: u
|
||||
})
|
||||
|
||||
console.log(`✅ User ${u.email} seeded successfully`)
|
||||
}
|
||||
|
||||
for (const r of role) {
|
||||
console.log(`Seeding role ${r.name}`)
|
||||
await prisma.role.upsert({
|
||||
@@ -46,6 +60,28 @@ const user = [
|
||||
console.log(`✅ Role ${r.name} seeded successfully`)
|
||||
}
|
||||
|
||||
for (const u of user) {
|
||||
await prisma.user.upsert({
|
||||
where: { email: u.email },
|
||||
create: u,
|
||||
update: u
|
||||
})
|
||||
|
||||
console.log(`✅ User ${u.email} seeded successfully`)
|
||||
}
|
||||
|
||||
for (const c of category) {
|
||||
await prisma.categoryPengaduan.upsert({
|
||||
where: { id: c.id },
|
||||
create: c,
|
||||
update: c
|
||||
})
|
||||
|
||||
console.log(`✅ Category ${c.name} seeded successfully`)
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
})().catch((e) => {
|
||||
console.error(e)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import Swagger from "@elysiajs/swagger";
|
||||
import Elysia from "elysia";
|
||||
import html from "./index.html";
|
||||
import apiAuth from "./server/middlewares/apiAuth";
|
||||
import { apiAuth } from "./server/middlewares/apiAuth";
|
||||
import ApiKeyRoute from "./server/routes/apikey_route";
|
||||
import Auth from "./server/routes/auth_route";
|
||||
import CredentialRoute from "./server/routes/credential_route";
|
||||
@@ -10,9 +10,8 @@ import { convertOpenApiToMcp } from "./server/lib/mcp-converter";
|
||||
import UserRoute from "./server/routes/user_route";
|
||||
import LayananRoute from "./server/routes/layanan_route";
|
||||
import AduanRoute from "./server/routes/aduan_route";
|
||||
|
||||
import { cors } from "@elysiajs/cors";
|
||||
import { MCPRoute } from "./server/routes/mcp_route";
|
||||
import PengaduanRoute from "./server/routes/pengaduan_route";
|
||||
|
||||
const Docs = new Elysia({
|
||||
tags: ["docs"],
|
||||
@@ -26,6 +25,7 @@ const Api = new Elysia({
|
||||
prefix: "/api",
|
||||
tags: ["api"],
|
||||
})
|
||||
.use(PengaduanRoute)
|
||||
.use(apiAuth)
|
||||
.use(ApiKeyRoute)
|
||||
.use(DarmasabaRoute)
|
||||
|
||||
108
src/server/lib/mcp_tool_convert.ts
Normal file
108
src/server/lib/mcp_tool_convert.ts
Normal file
@@ -0,0 +1,108 @@
|
||||
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()).
|
||||
* Hanya menyertakan endpoint yang memiliki tag berisi "mcp".
|
||||
*/
|
||||
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<any>(methods as any)) {
|
||||
const tags: string[] = Array.isArray(operation.tags) ? operation.tags : [];
|
||||
|
||||
// ✅ exclude semua yang tidak punya tag atau tag-nya tidak mengandung "mcp"
|
||||
if (!tags.length || !tags.some(t => t.toLowerCase().includes("mcp"))) continue;
|
||||
|
||||
const rawName = _.snakeCase(operation.operationId || `${method}_${path}`) || "unnamed_tool";
|
||||
const name = cleanToolName(rawName);
|
||||
|
||||
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: tags[0],
|
||||
deprecated: operation.deprecated || false,
|
||||
summary: operation.summary,
|
||||
},
|
||||
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, "");
|
||||
}
|
||||
|
||||
/**
|
||||
* Ambil OpenAPI JSON dari endpoint dan konversi ke tools MCP
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
// === CLI Mode ===
|
||||
if (import.meta.main) {
|
||||
const tools = await getMcpTools();
|
||||
await Bun.write("./tools.json", JSON.stringify(tools, null, 2));
|
||||
}
|
||||
@@ -5,7 +5,11 @@ import { prisma } from '../lib/prisma'
|
||||
|
||||
const secret = process.env.JWT_SECRET
|
||||
|
||||
export default function apiAuth(app: Elysia) {
|
||||
if (!secret) {
|
||||
throw new Error('JWT_SECRET is not defined')
|
||||
}
|
||||
|
||||
export function apiAuth(app: Elysia) {
|
||||
if (!secret) {
|
||||
throw new Error('JWT_SECRET is not defined')
|
||||
}
|
||||
@@ -16,37 +20,63 @@ export default function apiAuth(app: Elysia) {
|
||||
secret,
|
||||
})
|
||||
)
|
||||
.derive(async ({ cookie, headers, jwt }) => {
|
||||
.derive(async ({ cookie, headers, jwt, request }) => {
|
||||
let token: string | undefined
|
||||
|
||||
if (cookie?.token?.value) {
|
||||
token = cookie.token.value as any
|
||||
}
|
||||
if (headers['x-token']?.startsWith('Bearer ')) {
|
||||
token = (headers['x-token'] as string).slice(7)
|
||||
}
|
||||
if (headers['authorization']?.startsWith('Bearer ')) {
|
||||
token = (headers['authorization'] as string).slice(7)
|
||||
}
|
||||
// 🔸 Ambil token dari Cookie
|
||||
if (cookie?.token?.value) token = cookie.token.value as string
|
||||
|
||||
let user: null | Awaited<ReturnType<typeof prisma.user.findUnique>> = null
|
||||
if (token) {
|
||||
try {
|
||||
const decoded = (await jwt.verify(token)) as JWTPayloadSpec
|
||||
if (decoded.sub) {
|
||||
user = await prisma.user.findUnique({
|
||||
where: { id: decoded.sub as string },
|
||||
})
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('[SERVER][apiAuth] Invalid token', err)
|
||||
// 🔸 Ambil token dari Header (case-insensitive)
|
||||
const possibleHeaders = [
|
||||
'authorization',
|
||||
'Authorization',
|
||||
'x-token',
|
||||
'X-Token',
|
||||
]
|
||||
|
||||
for (const key of possibleHeaders) {
|
||||
const value = headers[key]
|
||||
if (typeof value === 'string') {
|
||||
token = value.startsWith('Bearer ') ? value.slice(7) : value
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return { user }
|
||||
// 🔸 Tidak ada token
|
||||
if (!token) {
|
||||
console.warn(`[AUTH] No token found for ${request.method} ${request.url}`)
|
||||
return { user: null }
|
||||
}
|
||||
|
||||
// 🔸 Verifikasi token
|
||||
try {
|
||||
const decoded = (await jwt.verify(token)) as JWTPayloadSpec
|
||||
|
||||
if (!decoded?.sub) {
|
||||
console.warn('[AUTH] Token missing sub field:', decoded)
|
||||
return { user: null }
|
||||
}
|
||||
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { id: decoded.sub as string },
|
||||
})
|
||||
|
||||
if (!user) {
|
||||
console.warn('[AUTH] User not found for sub:', decoded.sub)
|
||||
return { user: null }
|
||||
}
|
||||
|
||||
return { user }
|
||||
} catch (err) {
|
||||
console.warn('[AUTH] Invalid JWT token:', err)
|
||||
return { user: null }
|
||||
}
|
||||
})
|
||||
.onBeforeHandle(({ user, set }) => {
|
||||
.onBeforeHandle(({ user, set, request }) => {
|
||||
if (!user) {
|
||||
console.warn(
|
||||
`[AUTH] Unauthorized access: ${request.method} ${request.url}`
|
||||
)
|
||||
set.status = 401
|
||||
return { error: 'Unauthorized' }
|
||||
}
|
||||
|
||||
@@ -96,6 +96,7 @@ const LayananRoute = new Elysia({
|
||||
detail: {
|
||||
summary: "Create Layanan KTP/KK",
|
||||
description: "Create a new service request for KTP or KK.",
|
||||
tags: ["mcp"],
|
||||
},
|
||||
}
|
||||
)
|
||||
@@ -131,6 +132,7 @@ const LayananRoute = new Elysia({
|
||||
detail: {
|
||||
summary: "Cek Status KTP",
|
||||
description: "Retrieve the current status of a KTP/KK request by unique ID.",
|
||||
tags: ["mcp"],
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
@@ -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<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}`);
|
||||
}
|
||||
},
|
||||
},
|
||||
];
|
||||
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<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;
|
||||
|
||||
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<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()
|
||||
// =====================
|
||||
// MCP HTTP Streamable Endpoint
|
||||
// =====================
|
||||
.post("/mcp/:sessionId", async ({ params, request, set }) => {
|
||||
export const MCPRoute = new Elysia({
|
||||
tags: ["MCP Server"]
|
||||
})
|
||||
.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 "";
|
||||
});
|
||||
});
|
||||
|
||||
@@ -18,8 +18,9 @@ const PengaduanRoute = new Elysia({
|
||||
return data
|
||||
}, {
|
||||
detail: {
|
||||
summary: "get kategori pengaduan",
|
||||
description: `tool untuk mendapatkan kategori pengaduan`
|
||||
summary: "List Kategori Pengaduan",
|
||||
description: `tool untuk mendapatkan list kategori pengaduan`,
|
||||
tags: ["mcp"]
|
||||
}
|
||||
})
|
||||
.post("/category/create", async ({ body }) => {
|
||||
@@ -100,15 +101,67 @@ const PengaduanRoute = new Elysia({
|
||||
|
||||
// --- PENGADUAN ---
|
||||
.post("/create", async ({ body }) => {
|
||||
const { title, detail, location, image, idCategory, idWarga } = body
|
||||
const { title, detail, location, image, idCategory, idWarga, phone } = body
|
||||
const noPengaduan = await generateNoPengaduan()
|
||||
let idCategoryFix = idCategory
|
||||
let idWargaFix = idWarga
|
||||
const category = await prisma.categoryPengaduan.findUnique({
|
||||
where: {
|
||||
id: idCategory,
|
||||
}
|
||||
})
|
||||
|
||||
if (!category) {
|
||||
const cariCategory = await prisma.categoryPengaduan.findFirst({
|
||||
where: {
|
||||
name: idCategory,
|
||||
}
|
||||
})
|
||||
|
||||
if (!cariCategory) {
|
||||
idCategoryFix = "lainnya"
|
||||
} else {
|
||||
idCategoryFix = cariCategory.id
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const warga = await prisma.warga.findUnique({
|
||||
where: {
|
||||
id: idWarga,
|
||||
}
|
||||
})
|
||||
|
||||
if (!warga) {
|
||||
const cariWarga = await prisma.warga.findFirst({
|
||||
where: {
|
||||
phone,
|
||||
}
|
||||
})
|
||||
|
||||
if (!cariWarga) {
|
||||
const wargaCreate = await prisma.warga.create({
|
||||
data: {
|
||||
name: idWarga,
|
||||
phone,
|
||||
},
|
||||
select: {
|
||||
id: true
|
||||
}
|
||||
})
|
||||
idWargaFix = wargaCreate.id
|
||||
} else {
|
||||
idWargaFix = cariWarga.id
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const pengaduan = await prisma.pengaduan.create({
|
||||
data: {
|
||||
title,
|
||||
detail,
|
||||
idCategory,
|
||||
idWarga,
|
||||
idCategory: idCategoryFix,
|
||||
idWarga: idWargaFix,
|
||||
location,
|
||||
image,
|
||||
noPengaduan,
|
||||
@@ -138,17 +191,19 @@ const PengaduanRoute = new Elysia({
|
||||
title: t.String({ minLength: 1, error: "title harus diisi" }),
|
||||
detail: t.String({ minLength: 1, error: "detail harus diisi" }),
|
||||
location: t.String({ minLength: 1, error: "location harus diisi" }),
|
||||
image: t.String({ minLength: 1, error: "image harus diisi" }),
|
||||
image: t.Any(),
|
||||
idCategory: t.String({ minLength: 1, error: "idCategory harus diisi" }),
|
||||
idWarga: t.String({ minLength: 1, error: "idWarga harus diisi" }),
|
||||
phone: t.String({ minLength: 1, error: "phone harus diisi" }),
|
||||
}),
|
||||
detail: {
|
||||
summary: "buat pengaduan",
|
||||
description: `tool untuk membuat pengaduan`
|
||||
summary: "Create Pengaduan Warga",
|
||||
description: `tool untuk membuat pengaduan warga`,
|
||||
tags: ["mcp"]
|
||||
}
|
||||
})
|
||||
.post("/update-status", async ({ body }) => {
|
||||
const { id, status, keterangan } = body
|
||||
const { id, status, keterangan, idUser } = body
|
||||
let deskripsi = ""
|
||||
|
||||
const pengaduan = await prisma.pengaduan.update({
|
||||
@@ -165,13 +220,13 @@ const PengaduanRoute = new Elysia({
|
||||
throw new Error("gagal membuat pengaduan")
|
||||
}
|
||||
|
||||
if(status === "diterima") {
|
||||
if (status === "diterima") {
|
||||
deskripsi = "Pengaduan diterima oleh admin"
|
||||
} else if(status === "dikerjakan") {
|
||||
} else if (status === "dikerjakan") {
|
||||
deskripsi = "Pengaduan dikerjakan oleh petugas"
|
||||
} else if(status === "ditolak") {
|
||||
} else if (status === "ditolak") {
|
||||
deskripsi = "Pengaduan ditolak dengan keterangan " + keterangan
|
||||
} else if(status === "selesai") {
|
||||
} else if (status === "selesai") {
|
||||
deskripsi = "Pengaduan selesai"
|
||||
}
|
||||
|
||||
@@ -180,7 +235,7 @@ const PengaduanRoute = new Elysia({
|
||||
idPengaduan: pengaduan.id,
|
||||
deskripsi,
|
||||
status: status as StatusPengaduan,
|
||||
idUser: ""
|
||||
idUser,
|
||||
}
|
||||
})
|
||||
|
||||
@@ -192,12 +247,171 @@ const PengaduanRoute = new Elysia({
|
||||
body: t.Object({
|
||||
id: t.String({ minLength: 1, error: "id harus diisi" }),
|
||||
status: t.String({ minLength: 1, error: "status harus diisi" }),
|
||||
keterangan: t.Any()
|
||||
keterangan: t.Any(),
|
||||
idUser: t.String({ minLength: 1, error: "idUser harus diisi" }),
|
||||
}),
|
||||
|
||||
detail: {
|
||||
summary: "update status pengaduan",
|
||||
summary: "Update status pengaduan",
|
||||
description: `tool untuk update status pengaduan`
|
||||
}
|
||||
})
|
||||
.get("/detail", async ({ query }) => {
|
||||
const { id } = query
|
||||
const data = await prisma.pengaduan.findUnique({
|
||||
where: {
|
||||
id,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
noPengaduan: true,
|
||||
title: true,
|
||||
detail: true,
|
||||
location: true,
|
||||
image: true,
|
||||
idCategory: true,
|
||||
idWarga: true,
|
||||
status: true,
|
||||
keterangan: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
CategoryPengaduan: {
|
||||
select: {
|
||||
name: true
|
||||
}
|
||||
},
|
||||
Warga: {
|
||||
select: {
|
||||
name: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const dataHistory = await prisma.historyPengaduan.findMany({
|
||||
where: {
|
||||
idPengaduan: id,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
deskripsi: true,
|
||||
status: true,
|
||||
createdAt: true,
|
||||
idUser: true,
|
||||
User: {
|
||||
select: {
|
||||
name: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const dataHistoryFix = dataHistory.map((item) => {
|
||||
return {
|
||||
id: item.id,
|
||||
deskripsi: item.deskripsi,
|
||||
status: item.status,
|
||||
createdAt: item.createdAt,
|
||||
idUser: item.idUser,
|
||||
nameUser: item.User?.name,
|
||||
}
|
||||
})
|
||||
|
||||
const datafix = {
|
||||
id: data?.id,
|
||||
noPengaduan: data?.noPengaduan,
|
||||
title: data?.title,
|
||||
detail: data?.detail,
|
||||
location: data?.location,
|
||||
image: data?.image,
|
||||
CategoryPengaduan: data?.CategoryPengaduan.name,
|
||||
idWarga: data?.idWarga,
|
||||
nameWarga: data?.Warga?.name,
|
||||
status: data?.status,
|
||||
keterangan: data?.keterangan,
|
||||
createdAt: data?.createdAt,
|
||||
updatedAt: data?.updatedAt,
|
||||
history: dataHistoryFix,
|
||||
}
|
||||
|
||||
return datafix
|
||||
}, {
|
||||
detail: {
|
||||
summary: "Detail Pengaduan Warga",
|
||||
description: `tool untuk mendapatkan detail pengaduan warga / history pengaduan / mengecek status pengaduan`,
|
||||
tags: ["mcp"]
|
||||
}
|
||||
})
|
||||
.get("/", async ({ query }) => {
|
||||
const { take, page, search } = query
|
||||
const skip = !page ? 0 : (Number(page) - 1) * (!take ? 10 : Number(take))
|
||||
|
||||
const data = await prisma.pengaduan.findMany({
|
||||
skip,
|
||||
take: !take ? 10 : Number(take),
|
||||
orderBy: {
|
||||
createdAt: "asc"
|
||||
},
|
||||
where: {
|
||||
isActive: true,
|
||||
OR: [
|
||||
{
|
||||
title: {
|
||||
contains: search ?? "",
|
||||
mode: "insensitive"
|
||||
},
|
||||
},
|
||||
{
|
||||
noPengaduan: {
|
||||
contains: search ?? "",
|
||||
mode: "insensitive"
|
||||
},
|
||||
},
|
||||
{
|
||||
detail: {
|
||||
contains: search ?? "",
|
||||
mode: "insensitive"
|
||||
},
|
||||
}
|
||||
],
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
noPengaduan: true,
|
||||
title: true,
|
||||
detail: true,
|
||||
location: true,
|
||||
status: true,
|
||||
createdAt: true,
|
||||
CategoryPengaduan: {
|
||||
select: {
|
||||
name: true
|
||||
}
|
||||
},
|
||||
Warga: {
|
||||
select: {
|
||||
name: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const dataFix = data.map((item) => {
|
||||
return {
|
||||
noPengaduan: item.noPengaduan,
|
||||
title: item.title,
|
||||
detail: item.detail,
|
||||
status: item.status,
|
||||
createdAt: item.createdAt.toLocaleDateString("id-ID", { day: "numeric", month: "long", year: "numeric" }),
|
||||
}
|
||||
})
|
||||
|
||||
return dataFix
|
||||
}, {
|
||||
detail: {
|
||||
summary: "List Pengaduan Warga",
|
||||
description: `tool untuk mendapatkan list pengaduan warga`,
|
||||
tags: ["mcp"]
|
||||
}
|
||||
})
|
||||
export default PengaduanRoute
|
||||
|
||||
612
tools.json
Normal file
612
tools.json
Normal 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#"
|
||||
}
|
||||
}
|
||||
]
|
||||
8
x.sh
8
x.sh
@@ -1,6 +1,2 @@
|
||||
# curl -N -v -X GET "https://cld-dkr-prod-jenna-mcp.wibudev.com/mcp/test-session-id"
|
||||
|
||||
curl -X POST http://localhost:3000/mcp/test-room \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "X-API-Key: super-secret-key" \
|
||||
-d '{"event":"notice","data":{"msg":"hello world"}}'
|
||||
TOKEN=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJob3N0Iiwic3ViIjoiYmlwIiwicGF5bG9hZCI6IntcIm5hbWVcIjpcImplbm5hLW1jcFwiLFwiZGVzY3JpcHRpb25cIjpcInVudHVrIGplbm5hIG1jcFwiLFwiZXhwaXJlZEF0XCI6XCIyMDQ4LTA2LTI4XCJ9IiwiZXhwIjoyNDc2OTE1MjAwLCJpYXQiOjE3NjE2NDA1NDN9.EY4P246r3GBHo3yJgG0c5hvgG7p1z2x0KNL2fWBMpk8
|
||||
curl http://localhost:3000/api/pengaduan/category
|
||||
176
xx.ts
176
xx.ts
@@ -1,127 +1,65 @@
|
||||
import { readdirSync, statSync, writeFileSync } from "fs";
|
||||
import _ from "lodash";
|
||||
import { basename, extname, join, relative } from "path";
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { Elysia } from 'elysia'
|
||||
import jwt, { type JWTPayloadSpec } from '@elysiajs/jwt'
|
||||
import bearer from '@elysiajs/bearer'
|
||||
import { prisma } from '../lib/prisma'
|
||||
|
||||
const PAGES_DIR = join(process.cwd(), "src/pages");
|
||||
const OUTPUT_FILE = join(process.cwd(), "src/AppRoutes.tsx");
|
||||
// =========================================================
|
||||
// JWT Secret Validation
|
||||
// =========================================================
|
||||
const secret = process.env.JWT_SECRET
|
||||
if (!secret) throw new Error('JWT_SECRET environment variable is missing')
|
||||
|
||||
// 🧩 Ubah nama file ke nama komponen (PascalCase)
|
||||
const toComponentName = (fileName: string) =>
|
||||
fileName
|
||||
.replace(/_/g, " ")
|
||||
.replace(/\b\w/g, (c) => c.toUpperCase())
|
||||
.replace(/\s/g, "");
|
||||
// =========================================================
|
||||
// Auth Middleware Plugin
|
||||
// =========================================================
|
||||
export default function apiAuth(app: Elysia) {
|
||||
if (!secret) throw new Error('JWT_SECRET environment variable is missing')
|
||||
return app
|
||||
// Register Bearer and JWT plugins
|
||||
.use(bearer()) // ✅ Extracts Bearer token automatically (case-insensitive)
|
||||
.use(
|
||||
jwt({
|
||||
name: 'jwt',
|
||||
secret,
|
||||
})
|
||||
)
|
||||
|
||||
// 🧩 Ubah nama file ke path route
|
||||
function toRoutePath(name: string): string {
|
||||
if (name.toLowerCase() === "home") return "/";
|
||||
if (name.toLowerCase() === "login") return "/login";
|
||||
if (name.toLowerCase() === "notfound") return "/*";
|
||||
if (name.endsWith("_page")) return name.replace("_page", "").toLowerCase();
|
||||
if (name.startsWith("form_")) return name.replace("form_", "").toLowerCase();
|
||||
return name.toLowerCase();
|
||||
}
|
||||
// Derive user from JWT or cookie
|
||||
.derive(async ({ bearer, cookie, jwt }) => {
|
||||
// Normalize token type to string or undefined
|
||||
const token =
|
||||
(typeof bearer === 'string' ? bearer : undefined) ??
|
||||
(typeof cookie?.token?.value === 'string' ? cookie.token.value : undefined)
|
||||
|
||||
// 🧭 Scan folder pages secara rekursif
|
||||
function scan(dir: string): any[] {
|
||||
const items = readdirSync(dir);
|
||||
const routes: any[] = [];
|
||||
let user: Awaited<ReturnType<typeof prisma.user.findUnique>> | null = null
|
||||
|
||||
for (const item of items) {
|
||||
const full = join(dir, item);
|
||||
const stat = statSync(full);
|
||||
if (token) {
|
||||
try {
|
||||
const decoded = (await jwt.verify(token)) as JWTPayloadSpec
|
||||
|
||||
if (stat.isDirectory()) {
|
||||
routes.push({
|
||||
name: item,
|
||||
path: item.toLowerCase(),
|
||||
children: scan(full),
|
||||
});
|
||||
} else if (extname(item) === ".tsx") {
|
||||
routes.push({
|
||||
name: basename(item, ".tsx"),
|
||||
filePath: relative(join(process.cwd(), "src"), full).replace(/\\/g, "/"),
|
||||
});
|
||||
if (decoded?.sub && typeof decoded.sub === 'string') {
|
||||
user = await prisma.user.findUnique({
|
||||
where: { id: decoded.sub },
|
||||
})
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('[SERVER][apiAuth] Invalid token:', (err as Error).message)
|
||||
}
|
||||
}
|
||||
return routes;
|
||||
}
|
||||
|
||||
return { user }
|
||||
})
|
||||
|
||||
// Protect all routes by default
|
||||
.onBeforeHandle(({ user, set, request }) => {
|
||||
// Whitelist public routes if needed
|
||||
const publicPaths = ['/auth/login', '/auth/register', '/public']
|
||||
if (publicPaths.some((path) => request.url.includes(path))) return
|
||||
|
||||
if (!user) {
|
||||
set.status = 401
|
||||
return { error: 'Unauthorized' }
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 🏗️ Generate <Route> JSX dari struktur folder
|
||||
function generateJSX(routes: any[], parentPath = ""): string {
|
||||
let jsx = "";
|
||||
|
||||
for (const route of routes) {
|
||||
if (route.children) {
|
||||
// cari layout di folder
|
||||
const layout = route.children.find((r: any) => r.name.endsWith("_layout"));
|
||||
if (layout) {
|
||||
const LayoutComponent = toComponentName(layout.name.replace("_layout", "Layout"));
|
||||
const nested = route.children.filter((r: any) => r !== layout);
|
||||
const nestedRoutes = generateJSX(nested, `${parentPath}/${route.path}`);
|
||||
jsx += `
|
||||
<Route path="${parentPath}/${route.path}" element={<${LayoutComponent} />}>
|
||||
${nestedRoutes}
|
||||
</Route>
|
||||
`;
|
||||
} else {
|
||||
jsx += generateJSX(route.children, `${parentPath}/${route.path}`);
|
||||
}
|
||||
} else {
|
||||
const Component = toComponentName(route.name);
|
||||
const routePath = toRoutePath(route.name);
|
||||
|
||||
// Hapus duplikasi segmen
|
||||
const fullPath =
|
||||
routePath.startsWith("/")
|
||||
? routePath
|
||||
: `${parentPath}/${_.kebabCase(routePath)}`.replace(/\/+/g, "/");
|
||||
|
||||
jsx += `<Route path="${fullPath}" element={<${Component} />} />\n`;
|
||||
}
|
||||
}
|
||||
return jsx;
|
||||
}
|
||||
|
||||
// 🧾 Generate import otomatis
|
||||
function generateImports(routes: any[]): string {
|
||||
const imports = new Set<string>();
|
||||
|
||||
function collect(rs: any[]) {
|
||||
for (const r of rs) {
|
||||
if (r.children) collect(r.children);
|
||||
else {
|
||||
const Comp = toComponentName(r.name);
|
||||
imports.add(`import ${Comp} from "./${r.filePath.replace(/\.tsx$/, "")}";`);
|
||||
}
|
||||
}
|
||||
}
|
||||
collect(routes);
|
||||
return Array.from(imports).join("\n");
|
||||
}
|
||||
|
||||
// 🧠 Main generator
|
||||
const allRoutes = scan(PAGES_DIR);
|
||||
const imports = generateImports(allRoutes);
|
||||
const jsxRoutes = generateJSX(allRoutes);
|
||||
|
||||
const finalCode = `
|
||||
// ⚡ Auto-generated by generateRoutes.ts — DO NOT EDIT MANUALLY
|
||||
import { BrowserRouter, Routes, Route } from "react-router-dom";
|
||||
import ProtectedRoute from "./components/ProtectedRoute";
|
||||
${imports}
|
||||
|
||||
export default function AppRoutes() {
|
||||
return (
|
||||
<BrowserRouter>
|
||||
<Routes>
|
||||
${jsxRoutes}
|
||||
</Routes>
|
||||
</BrowserRouter>
|
||||
);
|
||||
}
|
||||
`;
|
||||
|
||||
writeFileSync(OUTPUT_FILE, finalCode);
|
||||
console.log("✅ Routes generated successfully → src/AppRoutes.generated.tsx");
|
||||
Bun.spawnSync(["bunx", "prettier", "--write", "src/**/*.tsx"])
|
||||
|
||||
Reference in New Issue
Block a user