From addd41389a930ef54db30e60a3a4fc910bbbe55c Mon Sep 17 00:00:00 2001 From: amaliadwiy Date: Mon, 27 Oct 2025 15:14:51 +0800 Subject: [PATCH 1/4] mcp pengaduan Deskripsi: - database pengaduan - seeder pengaduan No Issues --- bun.lock | 52 ++++++++++++++------------- package.json | 10 +++--- prisma/schema.prisma | 86 ++++++++++++++++++++++++++++++++------------ prisma/seed.ts | 31 ++++++++++++++-- src/index.tsx | 2 +- 5 files changed, 126 insertions(+), 55 deletions(-) diff --git a/bun.lock b/bun.lock index 3c811dc..b406d07 100644 --- a/bun.lock +++ b/bun.lock @@ -13,14 +13,14 @@ "@mantine/form": "^8.3.5", "@mantine/hooks": "^8.3.5", "@mantine/notifications": "^8.3.5", - "@modelcontextprotocol/sdk": "^1.20.1", - "@prisma/client": "^6.17.1", + "@modelcontextprotocol/sdk": "^1.20.2", + "@prisma/client": "^6.18.0", "@tabler/icons-react": "^3.35.0", "@types/jwt-decode": "^3.1.0", "@types/lodash": "^4.17.20", "@types/uuid": "^11.0.0", "add": "^2.0.6", - "elysia": "^1.4.12", + "elysia": "^1.4.13", "jwt-decode": "^4.0.0", "lodash": "^4.17.21", "react": "^19.2.0", @@ -35,11 +35,11 @@ "@types/react": "^19.2.2", "@types/react-dom": "^19.2.2", "biome": "^0.3.3", - "oxlint": "^1.23.0", + "oxlint": "^1.24.0", "postcss": "^8.5.6", "postcss-preset-mantine": "^1.18.0", "postcss-simple-vars": "^7.0.1", - "prisma": "^6.17.1", + "prisma": "^6.18.0", }, }, }, @@ -78,37 +78,37 @@ "@mantine/store": ["@mantine/store@8.3.5", "", { "peerDependencies": { "react": "^18.x || ^19.x" } }, "sha512-qN4fFsDMy86IV9oh1gZlDTv41RAsO0grjx90FGyT5QCv7NTgcavwxB74GBkhp45W8xn+Ms/awKy+6NxnmLmW1w=="], - "@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.20.1", "", { "dependencies": { "ajv": "^6.12.6", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.0.1", "express-rate-limit": "^7.5.0", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.23.8", "zod-to-json-schema": "^3.24.1" } }, "sha512-j/P+yuxXfgxb+mW7OEoRCM3G47zCTDqUPivJo/VzpjbG8I9csTXtOprCf5FfOfHK4whOJny0aHuBEON+kS7CCA=="], + "@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.20.2", "", { "dependencies": { "ajv": "^6.12.6", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.0.1", "express-rate-limit": "^7.5.0", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.23.8", "zod-to-json-schema": "^3.24.1" } }, "sha512-6rqTdFt67AAAzln3NOKsXRmv5ZzPkgbfaebKBqUbts7vK1GZudqnrun5a8d3M/h955cam9RHZ6Jb4Y1XhnmFPg=="], - "@oxlint/darwin-arm64": ["@oxlint/darwin-arm64@1.23.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-sbxoftgEMKmZQO7O4wHR9Rs7MfiHa2UH2x4QJDoc4LXqSCsI4lUIJbFQ05vX+zOUbt7CQMPdxEzExd4DqeKY2w=="], + "@oxlint/darwin-arm64": ["@oxlint/darwin-arm64@1.24.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-1Kd2+Ai1ttskhbJR+DNU4Y4YEDyP/cd50nWt2rAe2aE78dMOalaVGps3s8UnJkXpDL9ZqkgOHVDE5Doj2lxatw=="], - "@oxlint/darwin-x64": ["@oxlint/darwin-x64@1.23.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-PjByWr1TlwHQiOqEc8CPyXCT4wnujSK3n9l1m4un0Eh0uLJEDG5WM9tyDWOGuakC0Ika9/SMp0HDRg3ySchRRA=="], + "@oxlint/darwin-x64": ["@oxlint/darwin-x64@1.24.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-/R9VbnuTp7bLIBh6ucDHjx0po0wLQODLqzy+L/Frn5z4ifMVdE63DB+LHO8QAj+WEQleQq3u/MMms7RFPulCLA=="], - "@oxlint/linux-arm64-gnu": ["@oxlint/linux-arm64-gnu@1.23.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-sWlCwQ6xKeKC08qU3SfozqpRGCLJiO/onPYFJKEHbjzHkFp+OubOacFaT4ePcka28jCU1TvQ7Gi5BVQRncr0Xg=="], + "@oxlint/linux-arm64-gnu": ["@oxlint/linux-arm64-gnu@1.24.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-fA90bIQ1b44eNg0uULlTonqsADVIBnMz169mav6IhfZL9V6DpBCUWrV+8tEQCxbDvYC0WY1guBpPo2QWUnC/Dw=="], - "@oxlint/linux-arm64-musl": ["@oxlint/linux-arm64-musl@1.23.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-MPkmSiezuVgjMbzDSkRhENdnb038JOI+OTpBrOho2crbCAuqSRvyFwkMRhncJGZskzo1yeKxrKXB8T83ofmSXw=="], + "@oxlint/linux-arm64-musl": ["@oxlint/linux-arm64-musl@1.24.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-p7Bv9FTQ1lf4Z7OiIFwiy+cY2fxN6IJc0+2gJ4z2fpaQ0J2rQQcKdJ5RLQTxf+tAu7hyqjc6bf61EAGa9lb/GA=="], - "@oxlint/linux-x64-gnu": ["@oxlint/linux-x64-gnu@1.23.0", "", { "os": "linux", "cpu": "x64" }, "sha512-F6H9wmLfjBoNqtsgyg3P9abLnkVjNbCAnISKdRtDl7HvkMs4s/eU8np9+tSnqPeKOTBhkS+h/VSWgPGZTqIWQA=="], + "@oxlint/linux-x64-gnu": ["@oxlint/linux-x64-gnu@1.24.0", "", { "os": "linux", "cpu": "x64" }, "sha512-wIQOpTONiJ9pYPnLEq7UFuml8mpmSFTfUveNbT2rw9iXfj2nLMf7NIqGnUYQdvnnOi+maag9uei/WImXIm9LQQ=="], - "@oxlint/linux-x64-musl": ["@oxlint/linux-x64-musl@1.23.0", "", { "os": "linux", "cpu": "x64" }, "sha512-Xra0Cow35mAku8mbUbviPRalTU4Ct6MXQ1Eue8GmN4HFkjosrNa5qfy7QkJBqzjiI+JdnHxPXwackGn92/XOQw=="], + "@oxlint/linux-x64-musl": ["@oxlint/linux-x64-musl@1.24.0", "", { "os": "linux", "cpu": "x64" }, "sha512-HxcDX/SpTH7yC/Rn2MinjSHZmNpn79yJkBid792DWjP9bo0CnlNXOXMPXsbm+WqptvqQ9yUPCxf7KascUvxLyQ=="], - "@oxlint/win32-arm64": ["@oxlint/win32-arm64@1.23.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-FR+I+uGD3eFzTfBw87QRr+Y1jBYil3TqPM0wkSvuf3gOJTEXAfSkh9QHCgQqrseW3HDW7YJJ8ty1+sU31H/N4g=="], + "@oxlint/win32-arm64": ["@oxlint/win32-arm64@1.24.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-P1KtZ/xL+TcNTTmOtEsVrpqAdmpu2UCRAILjoqQyrYvI/CW6SdvoJfMBTntKOZaB52Peq2BHTgsYovON8q4FfQ=="], - "@oxlint/win32-x64": ["@oxlint/win32-x64@1.23.0", "", { "os": "win32", "cpu": "x64" }, "sha512-/oX0b26YIC1OgS5B+G8Ux1Vs/PIjOP4CBRzsPpYr0T+RoboJ3ZuV32bztLRggJKQqIlozcqiRo9fl/UMOMp8kQ=="], + "@oxlint/win32-x64": ["@oxlint/win32-x64@1.24.0", "", { "os": "win32", "cpu": "x64" }, "sha512-JMbMm7i1esFl12fRdOQwoeEeufWXxihOme8pZpI6jrwWK1kCIANMb5agI5Lkjf5vToQOP3DLXYc29aDm16fw6g=="], - "@prisma/client": ["@prisma/client@6.17.1", "", { "peerDependencies": { "prisma": "*", "typescript": ">=5.1.0" }, "optionalPeers": ["prisma", "typescript"] }, "sha512-zL58jbLzYamjnNnmNA51IOZdbk5ci03KviXCuB0Tydc9btH2kDWsi1pQm2VecviRTM7jGia0OPPkgpGnT3nKvw=="], + "@prisma/client": ["@prisma/client@6.18.0", "", { "peerDependencies": { "prisma": "*", "typescript": ">=5.1.0" }, "optionalPeers": ["prisma", "typescript"] }, "sha512-jnL2I9gDnPnw4A+4h5SuNn8Gc+1mL1Z79U/3I9eE2gbxJG1oSA+62ByPW4xkeDgwE0fqMzzpAZ7IHxYnLZ4iQA=="], - "@prisma/config": ["@prisma/config@6.17.1", "", { "dependencies": { "c12": "3.1.0", "deepmerge-ts": "7.1.5", "effect": "3.16.12", "empathic": "2.0.0" } }, "sha512-fs8wY6DsvOCzuiyWVckrVs1LOcbY4LZNz8ki4uUIQ28jCCzojTGqdLhN2Jl5lDnC1yI8/gNIKpsWDM8pLhOdwA=="], + "@prisma/config": ["@prisma/config@6.18.0", "", { "dependencies": { "c12": "3.1.0", "deepmerge-ts": "7.1.5", "effect": "3.18.4", "empathic": "2.0.0" } }, "sha512-rgFzspCpwsE+q3OF/xkp0fI2SJ3PfNe9LLMmuSVbAZ4nN66WfBiKqJKo/hLz3ysxiPQZf8h1SMf2ilqPMeWATQ=="], - "@prisma/debug": ["@prisma/debug@6.17.1", "", {}, "sha512-Vf7Tt5Wh9XcndpbmeotuqOMLWPTjEKCsgojxXP2oxE1/xYe7PtnP76hsouG9vis6fctX+TxgmwxTuYi/+xc7dQ=="], + "@prisma/debug": ["@prisma/debug@6.18.0", "", {}, "sha512-PMVPMmxPj0ps1VY75DIrT430MoOyQx9hmm174k6cmLZpcI95rAPXOQ+pp8ANQkJtNyLVDxnxVJ0QLbrm/ViBcg=="], - "@prisma/engines": ["@prisma/engines@6.17.1", "", { "dependencies": { "@prisma/debug": "6.17.1", "@prisma/engines-version": "6.17.1-1.272a37d34178c2894197e17273bf937f25acdeac", "@prisma/fetch-engine": "6.17.1", "@prisma/get-platform": "6.17.1" } }, "sha512-D95Ik3GYZkqZ8lSR4EyFOJ/tR33FcYRP8kK61o+WMsyD10UfJwd7+YielflHfKwiGodcqKqoraWw8ElAgMDbPw=="], + "@prisma/engines": ["@prisma/engines@6.18.0", "", { "dependencies": { "@prisma/debug": "6.18.0", "@prisma/engines-version": "6.18.0-8.34b5a692b7bd79939a9a2c3ef97d816e749cda2f", "@prisma/fetch-engine": "6.18.0", "@prisma/get-platform": "6.18.0" } }, "sha512-i5RzjGF/ex6AFgqEe2o1IW8iIxJGYVQJVRau13kHPYEL1Ck8Zvwuzamqed/1iIljs5C7L+Opiz5TzSsUebkriA=="], - "@prisma/engines-version": ["@prisma/engines-version@6.17.1-1.272a37d34178c2894197e17273bf937f25acdeac", "", {}, "sha512-17140E3huOuD9lMdJ9+SF/juOf3WR3sTJMVyyenzqUPbuH+89nPhSWcrY+Mf7tmSs6HvaO+7S+HkELinn6bhdg=="], + "@prisma/engines-version": ["@prisma/engines-version@6.18.0-8.34b5a692b7bd79939a9a2c3ef97d816e749cda2f", "", {}, "sha512-T7Af4QsJQnSgWN1zBbX+Cha5t4qjHRxoeoWpK4JugJzG/ipmmDMY5S+O0N1ET6sCBNVkf6lz+Y+ZNO9+wFU8pQ=="], - "@prisma/fetch-engine": ["@prisma/fetch-engine@6.17.1", "", { "dependencies": { "@prisma/debug": "6.17.1", "@prisma/engines-version": "6.17.1-1.272a37d34178c2894197e17273bf937f25acdeac", "@prisma/get-platform": "6.17.1" } }, "sha512-AYZiHOs184qkDMiTeshyJCtyL4yERkjfTkJiSJdYuSfc24m94lTNL5+GFinZ6vVz+ktX4NJzHKn1zIFzGTWrWg=="], + "@prisma/fetch-engine": ["@prisma/fetch-engine@6.18.0", "", { "dependencies": { "@prisma/debug": "6.18.0", "@prisma/engines-version": "6.18.0-8.34b5a692b7bd79939a9a2c3ef97d816e749cda2f", "@prisma/get-platform": "6.18.0" } }, "sha512-TdaBvTtBwP3IoqVYoGIYpD4mWlk0pJpjTJjir/xLeNWlwog7Sl3bD2J0jJ8+5+q/6RBg+acb9drsv5W6lqae7A=="], - "@prisma/get-platform": ["@prisma/get-platform@6.17.1", "", { "dependencies": { "@prisma/debug": "6.17.1" } }, "sha512-AKEn6fsfz0r482S5KRDFlIGEaq9wLNcgalD1adL+fPcFFblIKs1sD81kY/utrHdqKuVC6E1XSRpegDK3ZLL4Qg=="], + "@prisma/get-platform": ["@prisma/get-platform@6.18.0", "", { "dependencies": { "@prisma/debug": "6.18.0" } }, "sha512-uXNJCJGhxTCXo2B25Ta91Rk1/Nmlqg9p7G9GKh8TPhxvAyXCvMNQoogj4JLEUy+3ku8g59cpyQIKFhqY2xO2bg=="], "@scalar/openapi-types": ["@scalar/openapi-types@0.1.1", "", {}, "sha512-NMy3QNk6ytcCoPUGJH0t4NNr36OWXgZhA3ormr3TvhX1NDgoF95wFyodGVH8xiHeUyn2/FxtETm8UBLbB5xEmg=="], @@ -272,9 +272,9 @@ "ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="], - "effect": ["effect@3.16.12", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "fast-check": "^3.23.1" } }, "sha512-N39iBk0K71F9nb442TLbTkjl24FLUzuvx2i1I2RsEAQsdAdUTuUoW0vlfUXgkMTUOnYqKnWcFfqw4hK4Pw27hg=="], + "effect": ["effect@3.18.4", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "fast-check": "^3.23.1" } }, "sha512-b1LXQJLe9D11wfnOKAk3PKxuqYshQ0Heez+y5pnkd3jLj1yx9QhM72zZ9uUrOQyNvrs2GZZd/3maL0ZV18YuDA=="], - "elysia": ["elysia@1.4.12", "", { "dependencies": { "cookie": "^1.0.2", "exact-mirror": "0.2.2", "fast-decode-uri-component": "^1.0.1" }, "peerDependencies": { "@sinclair/typebox": ">= 0.34.0 < 1", "file-type": ">= 20.0.0", "openapi-types": ">= 12.0.0", "typescript": ">= 5.0.0" }, "optionalPeers": ["typescript"] }, "sha512-wbd0BkrobsjWSloIfYeF3f7G7rR0UWMa6tuLUhf6ZvwjiCEX3FVfhDsM+KaqqRRxkZpPDw42q4yIZlBLyE32ww=="], + "elysia": ["elysia@1.4.13", "", { "dependencies": { "cookie": "^1.0.2", "exact-mirror": "0.2.2", "fast-decode-uri-component": "^1.0.1", "memoirist": "^0.4.0" }, "peerDependencies": { "@sinclair/typebox": ">= 0.34.0 < 1", "@types/bun": ">= 1.2.0", "file-type": ">= 20.0.0", "openapi-types": ">= 12.0.0", "typescript": ">= 5.0.0" }, "optionalPeers": ["@types/bun", "typescript"] }, "sha512-6QaWQEm7QN1UCo1TPpEjaRJPHUmnM7R29y6LY224frDGk5PrpAnWmdHkoZxkcv+JRWp1j2ROr2IHbxHbG/jRjw=="], "empathic": ["empathic@2.0.0", "", {}, "sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA=="], @@ -434,6 +434,8 @@ "media-typer": ["media-typer@1.1.0", "", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="], + "memoirist": ["memoirist@0.4.0", "", {}, "sha512-zxTgA0mSYELa66DimuNQDvyLq36AwDlTuVRbnQtB+VuTcKWm5Qc4z3WkSpgsFWHNhexqkIooqpv4hdcqrX5Nmg=="], + "merge-descriptors": ["merge-descriptors@2.0.0", "", {}, "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g=="], "mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="], @@ -476,7 +478,7 @@ "os-homedir": ["os-homedir@1.0.2", "", {}, "sha512-B5JU3cabzk8c67mRRd3ECmROafjYMXbuzlwtqdM8IbS8ktlTix8aFGb2bAGKrSRIlnfKwovGUUr72JUPyOb6kQ=="], - "oxlint": ["oxlint@1.23.0", "", { "optionalDependencies": { "@oxlint/darwin-arm64": "1.23.0", "@oxlint/darwin-x64": "1.23.0", "@oxlint/linux-arm64-gnu": "1.23.0", "@oxlint/linux-arm64-musl": "1.23.0", "@oxlint/linux-x64-gnu": "1.23.0", "@oxlint/linux-x64-musl": "1.23.0", "@oxlint/win32-arm64": "1.23.0", "@oxlint/win32-x64": "1.23.0" }, "peerDependencies": { "oxlint-tsgolint": ">=0.2.0" }, "optionalPeers": ["oxlint-tsgolint"], "bin": { "oxlint": "bin/oxlint", "oxc_language_server": "bin/oxc_language_server" } }, "sha512-cLVdSE7Bza8npm+PffU0oufs15+M5uSMbQn0k2fJCayWU0xqQ3dyA3w9tEk8lgNOk1j1VJEdYctz64Vik8VG1w=="], + "oxlint": ["oxlint@1.24.0", "", { "optionalDependencies": { "@oxlint/darwin-arm64": "1.24.0", "@oxlint/darwin-x64": "1.24.0", "@oxlint/linux-arm64-gnu": "1.24.0", "@oxlint/linux-arm64-musl": "1.24.0", "@oxlint/linux-x64-gnu": "1.24.0", "@oxlint/linux-x64-musl": "1.24.0", "@oxlint/win32-arm64": "1.24.0", "@oxlint/win32-x64": "1.24.0" }, "peerDependencies": { "oxlint-tsgolint": ">=0.2.0" }, "optionalPeers": ["oxlint-tsgolint"], "bin": { "oxlint": "bin/oxlint", "oxc_language_server": "bin/oxc_language_server" } }, "sha512-swXlnHT7ywcCApkctIbgOSjDYHwMa12yMU0iXevfDuHlYkRUcbQrUv6nhM5v6B0+Be3zTBMNDGPAMQv0oznzRQ=="], "parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="], @@ -514,7 +516,7 @@ "postcss-simple-vars": ["postcss-simple-vars@7.0.1", "", { "peerDependencies": { "postcss": "^8.2.1" } }, "sha512-5GLLXaS8qmzHMOjVxqkk1TZPf1jMqesiI7qLhnlyERalG0sMbHIbJqrcnrpmZdKCLglHnRHoEBB61RtGTsj++A=="], - "prisma": ["prisma@6.17.1", "", { "dependencies": { "@prisma/config": "6.17.1", "@prisma/engines": "6.17.1" }, "peerDependencies": { "typescript": ">=5.1.0" }, "optionalPeers": ["typescript"], "bin": { "prisma": "build/index.js" } }, "sha512-ac6h0sM1Tg3zu8NInY+qhP/S9KhENVaw9n1BrGKQVFu05JT5yT5Qqqmb8tMRIE3ZXvVj4xcRA5yfrsy4X7Yy5g=="], + "prisma": ["prisma@6.18.0", "", { "dependencies": { "@prisma/config": "6.18.0", "@prisma/engines": "6.18.0" }, "peerDependencies": { "typescript": ">=5.1.0" }, "optionalPeers": ["typescript"], "bin": { "prisma": "build/index.js" } }, "sha512-bXWy3vTk8mnRmT+SLyZBQoC2vtV9Z8u7OHvEu+aULYxwiop/CPiFZ+F56KsNRNf35jw+8wcu8pmLsjxpBxAO9g=="], "prop-types": ["prop-types@15.8.1", "", { "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", "react-is": "^16.13.1" } }, "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg=="], diff --git a/package.json b/package.json index 2a02df3..d6577e3 100644 --- a/package.json +++ b/package.json @@ -20,14 +20,14 @@ "@mantine/form": "^8.3.5", "@mantine/hooks": "^8.3.5", "@mantine/notifications": "^8.3.5", - "@modelcontextprotocol/sdk": "^1.20.1", - "@prisma/client": "^6.17.1", + "@modelcontextprotocol/sdk": "^1.20.2", + "@prisma/client": "^6.18.0", "@tabler/icons-react": "^3.35.0", "@types/jwt-decode": "^3.1.0", "@types/lodash": "^4.17.20", "@types/uuid": "^11.0.0", "add": "^2.0.6", - "elysia": "^1.4.12", + "elysia": "^1.4.13", "jwt-decode": "^4.0.0", "lodash": "^4.17.21", "react": "^19.2.0", @@ -42,10 +42,10 @@ "@types/react": "^19.2.2", "@types/react-dom": "^19.2.2", "biome": "^0.3.3", - "oxlint": "^1.23.0", + "oxlint": "^1.24.0", "postcss": "^8.5.6", "postcss-preset-mantine": "^1.18.0", "postcss-simple-vars": "^7.0.1", - "prisma": "^6.17.1" + "prisma": "^6.18.0" } } diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 04793a5..6f92e9a 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -8,15 +8,26 @@ datasource db { url = env("DATABASE_URL") } -model User { +model Role { id String @id @default(cuid()) - name String? - email String? @unique - password String? - phone String? @unique + name String createdAt DateTime @default(now()) updatedAt DateTime @updatedAt - ApiKey ApiKey[] + User User[] +} + +model User { + id String @id @default(cuid()) + Role Role? @relation(fields: [roleId], references: [id]) + roleId String? + name String? + email String? @unique + password String? + phone String? @unique + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + ApiKey ApiKey[] + HistoryPengaduan HistoryPengaduan[] } model ApiKey { @@ -39,24 +50,55 @@ model Credential { updatedAt DateTime @updatedAt } -model Pengaduan { - id String @id @default(cuid()) - jenis_pengaduan String - name String? - phone String? - detail String? - status StatusPengaduan @default(diterima) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt +model CategoryPengaduan { + id String @id @default(cuid()) + name String + isActive Boolean @default(true) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + Pengaduan Pengaduan[] } -model PengaduanSampah { - id String @id @default(cuid()) - judul String - deskripsi String - status StatusPengaduan @default(diterima) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt +model Pengaduan { + id String @id @default(cuid()) + CategoryPengaduan CategoryPengaduan @relation(fields: [idCategory], references: [id]) + idCategory String + Warga Warga @relation(fields: [idWarga], references: [id]) + idWarga String + noPengaduan String + title String? + phone String? + detail String? + location String? + image String? + status StatusPengaduan @default(diterima) + isActive Boolean @default(true) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + HistoryPengaduan HistoryPengaduan[] +} + +model HistoryPengaduan { + id String @id @default(cuid()) + Pengaduan Pengaduan @relation(fields: [idPengaduan], references: [id]) + idPengaduan String + User User @relation(fields: [idUser], references: [id]) + idUser String + deskripsi String? + status StatusPengaduan @default(diterima) + image String? + order Int + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} + +model Warga { + id String @id @default(cuid()) + name String? + phone String? @unique + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + Pengaduan Pengaduan[] } enum StatusPengaduan { diff --git a/prisma/seed.ts b/prisma/seed.ts index 4deac9c..9421739 100644 --- a/prisma/seed.ts +++ b/prisma/seed.ts @@ -1,24 +1,51 @@ import { prisma } from "@/server/lib/prisma"; +const role = [ + { + id: "developer", + name: "developer" + }, + { + id: "admin", + name: "admin" + }, + { + id: "pelaksana", + name: "pelaksana" + } +] + const user = [ { name: "Bip", email: "bip@bip.com", password: "bip", + roleId: "developer" } ]; -; (async () => { +(async () => { for (const u of user) { await prisma.user.upsert({ where: { email: u.email }, create: u, - update: 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({ + where: { id: r.id }, + create: r, + update: r + }) + + console.log(`✅ Role ${r.name} seeded successfully`) + } + })().catch((e) => { console.error(e) diff --git a/src/index.tsx b/src/index.tsx index e05dfdb..ab645fd 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -52,7 +52,7 @@ const app = new Elysia() }, ) .use(MCPRoute) - // .get("/*", html) + .get("/*", html) .onRequest(({ set }) => { set.headers["Access-Control-Allow-Origin"] = "*"; set.headers["Access-Control-Allow-Methods"] = "GET, POST, OPTIONS"; From 7587b3ac540626c66cb2bbe9a32e55e72ecccf85 Mon Sep 17 00:00:00 2001 From: amaliadwiy Date: Mon, 27 Oct 2025 17:40:56 +0800 Subject: [PATCH 2/4] upd: mcp pengaduan Deskripsi: - database pengaduan - api categori pengaduan > create list update hapus - api pengaduan > create pengaduan dan history, update status dan create history No Issues --- prisma/schema.prisma | 12 +- src/pages/list/list_pengaduan_sampah.tsx | 13 -- src/server/lib/no-pengaduan.ts | 23 +++ src/server/routes/aduan_route.ts | 22 +-- src/server/routes/darmasaba_route.ts | 30 ++-- src/server/routes/pengaduan_route.ts | 203 +++++++++++++++++++++++ 6 files changed, 257 insertions(+), 46 deletions(-) delete mode 100644 src/pages/list/list_pengaduan_sampah.tsx create mode 100644 src/server/lib/no-pengaduan.ts create mode 100644 src/server/routes/pengaduan_route.ts diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 6f92e9a..858f99f 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -67,11 +67,11 @@ model Pengaduan { idWarga String noPengaduan String title String? - phone String? detail String? location String? image String? - status StatusPengaduan @default(diterima) + keterangan String? + status StatusPengaduan @default(antrian) isActive Boolean @default(true) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@ -82,12 +82,10 @@ model HistoryPengaduan { id String @id @default(cuid()) Pengaduan Pengaduan @relation(fields: [idPengaduan], references: [id]) idPengaduan String - User User @relation(fields: [idUser], references: [id]) - idUser String + User User? @relation(fields: [idUser], references: [id]) + idUser String? deskripsi String? - status StatusPengaduan @default(diterima) - image String? - order Int + status StatusPengaduan @default(antrian) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt } diff --git a/src/pages/list/list_pengaduan_sampah.tsx b/src/pages/list/list_pengaduan_sampah.tsx deleted file mode 100644 index e1fc389..0000000 --- a/src/pages/list/list_pengaduan_sampah.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import apiFetch from "@/lib/apiFetch" -import useSWR from "swr" - -export default function ListPengaduanSampah() { - // const { data, error, isLoading, mutate } = useSWR("/", apiFetch.api.aduan["list-aduan-sampah"].get) - - - return ( -
-

List Pengaduan Sampah

-
- ) -} \ No newline at end of file diff --git a/src/server/lib/no-pengaduan.ts b/src/server/lib/no-pengaduan.ts new file mode 100644 index 0000000..634e5e7 --- /dev/null +++ b/src/server/lib/no-pengaduan.ts @@ -0,0 +1,23 @@ +import { prisma } from "./prisma" + +export const generateNoPengaduan = async () => { + const date = new Date() + const year = String(date.getFullYear()).slice(-2) // ambil 2 digit terakhir + const month = String(date.getMonth() + 1).padStart(2, "0") + const day = String(date.getDate()).padStart(2, "0") + + const prefix = `PGD-${day}${month}${year}` + + const count = await prisma.pengaduan.count({ + where: { + noPengaduan: { + contains: prefix + } + } + }) + + // pastikan nomor urut selalu 3 digit + const number = String(count + 1).padStart(3, "0") + + return `${prefix}-${number}` +} diff --git a/src/server/routes/aduan_route.ts b/src/server/routes/aduan_route.ts index a9d6d32..e652600 100644 --- a/src/server/routes/aduan_route.ts +++ b/src/server/routes/aduan_route.ts @@ -23,12 +23,12 @@ const AduanRoute = new Elysia({ }) .post("/aduan-sampah", async (ctx) => { const { judul, deskripsi } = ctx.body; - await prisma.pengaduanSampah.create({ - data: { - judul, - deskripsi, - } - }) + // await prisma.pengaduanSampah.create({ + // data: { + // judul, + // deskripsi, + // } + // }) return { success: true, data: "berhasil membuat aduan sampah" @@ -44,11 +44,11 @@ const AduanRoute = new Elysia({ } }) .get("/list-aduan-sampah", async () => { - const data = await prisma.pengaduanSampah.findMany() - return { - success: true, - data: data - } + // const data = await prisma.pengaduanSampah.findMany() + // return { + // success: true, + // data: data + // } }, { detail: { summary: "list aduan sampah", diff --git a/src/server/routes/darmasaba_route.ts b/src/server/routes/darmasaba_route.ts index c7827f6..ab3b3ea 100644 --- a/src/server/routes/darmasaba_route.ts +++ b/src/server/routes/darmasaba_route.ts @@ -194,14 +194,14 @@ const DarmasabaRoute = new Elysia({ .post("/buat-pengaduan", async ({ body }) => { const { jenis_laporan, name, phone, detail } = body - await prisma.pengaduan.create({ - data: { - jenis_laporan, - detail, - name, - phone - } - }) + // await prisma.pengaduan.create({ + // data: { + // jenis_laporan, + // detail, + // name, + // phone + // } + // }) return ` ${JSON.stringify(body)} @@ -223,14 +223,14 @@ const DarmasabaRoute = new Elysia({ .post("/status-pengaduan", async ({ body }) => { const { name, phone } = body - const pengaduan = await prisma.pengaduan.findMany({ - where: { - name, - phone - } - }) + // const pengaduan = await prisma.pengaduan.findMany({ + // where: { + // name, + // phone + // } + // }) - return pengaduan + // return pengaduan }, { body: t.Object({ diff --git a/src/server/routes/pengaduan_route.ts b/src/server/routes/pengaduan_route.ts new file mode 100644 index 0000000..2772ad5 --- /dev/null +++ b/src/server/routes/pengaduan_route.ts @@ -0,0 +1,203 @@ +import Elysia, { t } from "elysia" +import type { StatusPengaduan } from "generated/prisma" +import { generateNoPengaduan } from "../lib/no-pengaduan" +import { prisma } from "../lib/prisma" + +const PengaduanRoute = new Elysia({ + prefix: "pengaduan", + tags: ["pengaduan"], +}) + + // --- KATEGORI PENGADUAN --- + .get("/category", async () => { + const data = await prisma.categoryPengaduan.findMany({ + where: { + isActive: true + } + }) + return data + }, { + detail: { + summary: "get kategori pengaduan", + description: `tool untuk mendapatkan kategori pengaduan` + } + }) + .post("/category/create", async ({ body }) => { + const { name } = body + + await prisma.categoryPengaduan.create({ + data: { + name, + } + }) + + return ` + ${JSON.stringify(body)} + + kategori pengaduan sudah dibuat` + }, { + body: t.Object({ + name: t.String({ minLength: 1, error: "name harus diisi" }), + }), + detail: { + summary: "buat kategori pengaduan", + description: `tool untuk membuat kategori pengaduan` + } + }) + .post("/category/update", async ({ body }) => { + const { id, name } = body + + await prisma.categoryPengaduan.update({ + where: { + id, + }, + data: { + name + } + }) + + return ` + ${JSON.stringify(body)} + + kategori pengaduan sudah diperbarui` + }, { + body: t.Object({ + id: t.String({ minLength: 1, error: "id harus diisi" }), + name: t.String({ minLength: 1, error: "name harus diisi" }), + }), + detail: { + summary: "update kategori pengaduan", + description: `tool untuk update kategori pengaduan` + } + }) + .post("/category/delete", async ({ body }) => { + const { id } = body + + await prisma.categoryPengaduan.update({ + where: { + id, + }, + data: { + isActive: false + } + }) + + return ` + ${JSON.stringify(body)} + + kategori pengaduan sudah dihapus` + }, { + body: t.Object({ + id: t.String({ minLength: 1, error: "id harus diisi" }), + }), + detail: { + summary: "delete kategori pengaduan", + description: `tool untuk delete kategori pengaduan` + } + }) + + + + // --- PENGADUAN --- + .post("/create", async ({ body }) => { + const { title, detail, location, image, idCategory, idWarga } = body + const noPengaduan = await generateNoPengaduan() + + const pengaduan = await prisma.pengaduan.create({ + data: { + title, + detail, + idCategory, + idWarga, + location, + image, + noPengaduan, + }, + select: { + id: true, + } + }) + + if (!pengaduan.id) { + throw new Error("gagal membuat pengaduan") + } + + await prisma.historyPengaduan.create({ + data: { + idPengaduan: pengaduan.id, + deskripsi: "Pengaduan dibuat", + } + }) + + return ` + ${JSON.stringify(body)} + + pengaduan sudah dibuat` + }, { + body: t.Object({ + 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" }), + idCategory: t.String({ minLength: 1, error: "idCategory harus diisi" }), + idWarga: t.String({ minLength: 1, error: "idWarga harus diisi" }), + }), + detail: { + summary: "buat pengaduan", + description: `tool untuk membuat pengaduan` + } + }) + .post("/update-status", async ({ body }) => { + const { id, status, keterangan } = body + let deskripsi = "" + + const pengaduan = await prisma.pengaduan.update({ + where: { + id, + }, + data: { + status: status as StatusPengaduan, + keterangan, + } + }) + + if (!pengaduan) { + throw new Error("gagal membuat pengaduan") + } + + if(status === "diterima") { + deskripsi = "Pengaduan diterima oleh admin" + } else if(status === "dikerjakan") { + deskripsi = "Pengaduan dikerjakan oleh petugas" + } else if(status === "ditolak") { + deskripsi = "Pengaduan ditolak dengan keterangan " + keterangan + } else if(status === "selesai") { + deskripsi = "Pengaduan selesai" + } + + await prisma.historyPengaduan.create({ + data: { + idPengaduan: pengaduan.id, + deskripsi, + status: status as StatusPengaduan, + idUser: "" + } + }) + + return ` + ${JSON.stringify(body)} + + status pengaduan sudah diupdate` + }, { + body: t.Object({ + id: t.String({ minLength: 1, error: "id harus diisi" }), + status: t.String({ minLength: 1, error: "status harus diisi" }), + keterangan: t.Any() + }), + + detail: { + summary: "update status pengaduan", + description: `tool untuk update status pengaduan` + } + }) +export default PengaduanRoute From bf0083e678b7f4b58903278795e69f5ffd794a79 Mon Sep 17 00:00:00 2001 From: amaliadwiy Date: Tue, 28 Oct 2025 14:23:40 +0800 Subject: [PATCH 3/4] update api pengaduan --- src/index.tsx | 4 +- src/server/routes/pengaduan_route.ts | 100 +++++++++++++++++++++++++-- 2 files changed, 96 insertions(+), 8 deletions(-) diff --git a/src/index.tsx b/src/index.tsx index ab645fd..dfb6f62 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -13,6 +13,7 @@ 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"], @@ -32,7 +33,8 @@ const Api = new Elysia({ .use(CredentialRoute) .use(UserRoute) .use(LayananRoute) - .use(AduanRoute); + .use(AduanRoute) + .use(PengaduanRoute); const app = new Elysia() .use(Api) diff --git a/src/server/routes/pengaduan_route.ts b/src/server/routes/pengaduan_route.ts index 2772ad5..f8cce8c 100644 --- a/src/server/routes/pengaduan_route.ts +++ b/src/server/routes/pengaduan_route.ts @@ -148,7 +148,7 @@ const PengaduanRoute = new Elysia({ } }) .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 +165,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 +180,7 @@ const PengaduanRoute = new Elysia({ idPengaduan: pengaduan.id, deskripsi, status: status as StatusPengaduan, - idUser: "" + idUser, } }) @@ -192,7 +192,8 @@ 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: { @@ -200,4 +201,89 @@ const PengaduanRoute = new Elysia({ 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: "get detail pengaduan", + description: `tool untuk mendapatkan detail pengaduan` + } + }) export default PengaduanRoute From 6a9ce5431190f25f0b5892b4f6c7b25187bf2e3e Mon Sep 17 00:00:00 2001 From: amaliadwiy Date: Tue, 28 Oct 2025 15:00:57 +0800 Subject: [PATCH 4/4] 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