Compare commits

...

19 Commits

Author SHA1 Message Date
2aaa44cf14 Merge pull request 'upd : dashboard admin' (#18) from amalia/10-nov-25 into main
Reviewed-on: http://wibugit.wibudev.com/wibu/jenna-mcp/pulls/18
2025-11-11 10:15:42 +08:00
fbf00a55da upd : dashboard admin
Deskripsi:
- tampilan detail pengaduan

No Issues
2025-11-10 17:47:32 +08:00
bipproduction
03955743ca tambah route texs 2025-11-10 17:12:50 +08:00
bipproduction
cdd7c6fa2b tambah route texs 2025-11-10 17:08:10 +08:00
bipproduction
c51dcfdad4 tambah route texs 2025-11-10 16:59:26 +08:00
bipproduction
e68fe87e9e tambah route texs 2025-11-10 16:55:38 +08:00
bipproduction
fca77c6bd8 tambah route texs 2025-11-10 16:53:00 +08:00
aa89a10aa8 Merge pull request 'return upload base64' (#17) from amalia/10-nov-25 into main
Reviewed-on: http://wibugit.wibudev.com/wibu/jenna-mcp/pulls/17
2025-11-10 14:11:17 +08:00
21af3e3310 return upload base64 2025-11-10 14:10:22 +08:00
08faa9f6b0 Merge pull request 'upd: json return' (#16) from amalia/10-nov-25 into main
Reviewed-on: http://wibugit.wibudev.com/wibu/jenna-mcp/pulls/16
2025-11-10 11:36:27 +08:00
b101c63f8d upd: json return 2025-11-10 11:35:51 +08:00
41820ff2b3 Merge pull request 'upload base64 fix' (#15) from amalia/10-nov-25 into main
Reviewed-on: http://wibugit.wibudev.com/wibu/jenna-mcp/pulls/15
2025-11-10 11:25:28 +08:00
9c045f32ea upload base64 fix 2025-11-10 11:24:50 +08:00
6dd8dcd06e Merge pull request 'upd: upload base64' (#14) from amalia/10-nov-25 into main
Reviewed-on: http://wibugit.wibudev.com/wibu/jenna-mcp/pulls/14
2025-11-10 10:54:52 +08:00
f79629e97e upd: upload base64 2025-11-10 10:54:22 +08:00
b52bb57fbc Merge pull request 'upload base64' (#13) from amalia/10-nov-25 into main
Reviewed-on: http://wibugit.wibudev.com/wibu/jenna-mcp/pulls/13
2025-11-10 10:35:00 +08:00
401f8f13a2 upload base64 2025-11-10 10:34:30 +08:00
7b0d4e5d30 Merge pull request 'amalia/07-nov-25' (#12) from amalia/07-nov-25 into main
Reviewed-on: http://wibugit.wibudev.com/wibu/jenna-mcp/pulls/12
2025-11-07 17:35:13 +08:00
0ac649345d Merge pull request 'amalia/07-nov-25' (#11) from amalia/07-nov-25 into main
Reviewed-on: http://wibugit.wibudev.com/wibu/jenna-mcp/pulls/11
2025-11-07 12:07:58 +08:00
12 changed files with 469 additions and 176 deletions

View File

@@ -19,6 +19,7 @@ import CredentialPage from "./pages/scr/dashboard/credential/credential_page";
import DashboardHome from "./pages/scr/dashboard/dashboard_home";
import ListPelayananPage from "./pages/scr/dashboard/pelayanan-surat/list_pelayanan_page";
import ListPage from "./pages/scr/dashboard/pengaduan/list_page";
import DetailPage from "./pages/scr/dashboard/pengaduan/detail_page";
import ApikeyPage from "./pages/scr/dashboard/apikey/apikey_page";
import DashboardLayout from "./pages/scr/dashboard/dashboard_layout";
import ScrLayout from "./pages/scr/scr_layout";
@@ -102,6 +103,10 @@ export default function AppRoutes() {
path="/scr/dashboard/pengaduan/list"
element={<ListPage />}
/>
<Route
path="/scr/dashboard/pengaduan/detail"
element={<DetailPage />}
/>
<Route
path="/scr/dashboard/apikey/apikey"
element={<ApikeyPage />}

View File

@@ -21,6 +21,7 @@ const clientRoutes = {
"/scr/dashboard/dashboard-home": "/scr/dashboard/dashboard-home",
"/scr/dashboard/pelayanan-surat/list-pelayanan": "/scr/dashboard/pelayanan-surat/list-pelayanan",
"/scr/dashboard/pengaduan/list": "/scr/dashboard/pengaduan/list",
"/scr/dashboard/pengaduan/detail": "/scr/dashboard/pengaduan/detail",
"/scr/dashboard/apikey/apikey": "/scr/dashboard/apikey/apikey",
"/dir/dir": "/dir/dir",
"/*": "/*"

View File

@@ -12,6 +12,7 @@ import LayananRoute from "./server/routes/layanan_route";
import { MCPRoute } from "./server/routes/mcp_route";
import PelayananRoute from "./server/routes/pelayanan_surat_route";
import PengaduanRoute from "./server/routes/pengaduan_route";
import TestRoute from "./server/routes/test";
import UserRoute from "./server/routes/user_route";
import cors from "@elysiajs/cors";
@@ -29,6 +30,7 @@ const Api = new Elysia({
})
.use(PengaduanRoute)
.use(PelayananRoute)
.use(TestRoute)
.use(apiAuth)
.use(ApiKeyRoute)
.use(DarmasabaRoute)

View File

@@ -0,0 +1,301 @@
import apiFetch from "@/lib/apiFetch";
import {
Anchor,
Badge,
Card,
Container,
Divider,
Flex,
Grid,
Group,
Stack,
Table,
Text,
Title
} from "@mantine/core";
import { useShallowEffect } from "@mantine/hooks";
import {
IconAlignJustified,
IconCategory,
IconFileCertificate,
IconInfoTriangle,
IconMapPin,
IconMessageReport,
IconPhotoScan,
IconUser
} from "@tabler/icons-react";
import { useState } from "react";
import { useLocation } from "react-router-dom";
import useSwr from "swr";
export default function DetailPengaduanPage() {
const { search } = useLocation();
const query = new URLSearchParams(search);
const id = query.get("id");
return (
<Container size="xl" py="xl" w={"100%"}>
<Grid>
<Grid.Col span={8}>
<Stack gap={"xl"}>
<DetailDataPengaduan />
<DetailDataHistori />
</Stack>
</Grid.Col>
<Grid.Col span={4}>
<DetailUserPengaduan />
</Grid.Col>
</Grid>
</Container>
);
}
function DetailDataPengaduan() {
return (
<Card
radius="md"
p="lg"
withBorder
style={{
background:
"linear-gradient(145deg, rgba(25,25,25,0.95), rgba(45,45,45,0.85))",
borderColor: "rgba(100,100,100,0.2)",
boxShadow: "0 0 20px rgba(0,255,200,0.08)",
}}
>
<Stack gap={"md"}>
<Flex align="center" justify="space-between">
<Group gap="xs">
<Title order={4} c="gray.2">
Pengaduan
</Title>
<Title order={4} c="dimmed">
#PGf-2345-33
</Title>
</Group>
<Badge
size="xl"
variant="light"
radius="sm"
color={"yellow"}
style={{ textTransform: "none" }}
>
antrian
</Badge>
</Flex>
<Divider my={0} />
<Grid>
<Grid.Col span={6}>
<Stack gap="md">
<Flex direction={"column"} justify="flex-start">
<Group gap="xs">
<IconAlignJustified size={20} />
<Text size="md">
Judul
</Text>
</Group>
<Text size="md" c={"white"}>Judul Pengaduan</Text>
</Flex>
<Flex direction={"column"} justify="flex-start">
<Group gap="xs">
<IconMapPin size={20} />
<Text size="md">
Lokasi
</Text>
</Group>
<Text size="md" c="white">fwef</Text>
</Flex>
</Stack>
</Grid.Col>
<Grid.Col span={6}>
<Stack gap="md">
<Flex direction={"column"} justify="flex-start">
<Group gap="xs">
<IconCategory size={20} />
<Text size="md">
Kategori
</Text>
</Group>
<Text size="md" c="white">fwef</Text>
</Flex>
<Flex direction={"column"} justify="flex-start">
<Group gap="xs">
<IconPhotoScan size={20} />
<Text size="md">
Gambar
</Text>
</Group>
<Anchor href="https://mantine.dev/" target="_blank">
Lihat Gambar
</Anchor>
</Flex>
</Stack>
</Grid.Col>
<Grid.Col span={12}>
<Stack gap="md">
<Flex direction={"column"} justify="flex-start">
<Group gap="xs">
<IconAlignJustified size={20} />
<Text size="md">
Detail
</Text>
</Group>
<Text size="md" c="white">
Lorem ipsum dolor sit, amet consectetur adipisicing elit. Illum, corporis iusto. Suscipit veritatis quas, non nobis fuga, laudantium accusantium tempora sint aliquid architecto totam esse eum excepturi nostrum fugiat ut.
</Text>
</Flex>
<Flex direction={"column"} justify="flex-start">
<Group gap="xs">
<IconInfoTriangle size={20} />
<Text size="md">
Keterangan
</Text>
</Group>
<Text size="md" c={"white"}>
Lorem ipsum dolor, sit amet consectetur adipisicing elit. At fugiat eligendi nesciunt dolore? Maiores a cumque vitae suscipit incidunt quos beatae modi, vel, id ullam quae voluptas, deserunt quas placeat.
</Text>
</Flex>
</Stack>
</Grid.Col>
</Grid>
</Stack>
</Card>
);
}
function DetailDataHistori() {
const elements = [
{ position: 6, mass: 12.011, symbol: 'C', name: 'Carbon' },
{ position: 7, mass: 14.007, symbol: 'N', name: 'Nitrogen' },
{ position: 39, mass: 88.906, symbol: 'Y', name: 'Yttrium' },
{ position: 56, mass: 137.33, symbol: 'Ba', name: 'Barium' },
{ position: 58, mass: 140.12, symbol: 'Ce', name: 'Cerium' },
];
const rows = elements.map((element) => (
<Table.Tr key={element.name}>
<Table.Td>{element.position}</Table.Td>
<Table.Td>{element.name}</Table.Td>
<Table.Td>{element.symbol}</Table.Td>
<Table.Td>{element.mass}</Table.Td>
</Table.Tr>
));
return (
<Card
radius="md"
p="lg"
withBorder
style={{
background:
"linear-gradient(145deg, rgba(25,25,25,0.95), rgba(45,45,45,0.85))",
borderColor: "rgba(100,100,100,0.2)",
boxShadow: "0 0 20px rgba(0,255,200,0.08)",
}}
>
<Stack gap="md">
<Flex align="center" justify="space-between">
<Title order={4} c="gray.2">
Histori Pengaduan
</Title>
</Flex>
<Divider my={0} />
<Table>
<Table.Thead>
<Table.Tr>
<Table.Th>Tanggal</Table.Th>
<Table.Th>Deskripsi</Table.Th>
<Table.Th>Status</Table.Th>
<Table.Th>User</Table.Th>
</Table.Tr>
</Table.Thead>
<Table.Tbody>{rows}</Table.Tbody>
</Table>
</Stack>
</Card>
)
}
function DetailUserPengaduan() {
const [page, setPage] = useState(1);
const [value, setValue] = useState("");
const { data, mutate, isLoading } = useSwr("/", () =>
apiFetch.api.pengaduan.list.get({
query: {
status,
search: value,
take: "",
page: "",
},
}),
);
useShallowEffect(() => {
mutate();
}, [status, value]);
const list = data?.data || [];
return (
<Card
radius="md"
p="lg"
withBorder
style={{
background:
"linear-gradient(145deg, rgba(25,25,25,0.95), rgba(45,45,45,0.85))",
borderColor: "rgba(100,100,100,0.2)",
boxShadow: "0 0 20px rgba(0,255,200,0.08)",
}}
>
<Stack gap="md">
<Flex align="center" justify="space-between">
<Flex direction={"column"}>
<Title order={4} c="gray.2">
Warga
</Title>
</Flex>
</Flex>
<Divider my={0} />
<Stack gap="md">
<Group justify="space-between" >
<Group gap="xs">
<IconUser size={20} />
<Text size="md">
Nama
</Text>
</Group>
<Text size="md" c={"white"}>Amalia Dwi Yustiani</Text>
</Group>
<Group justify="space-between" >
<Group gap="xs">
<IconMapPin size={20} />
<Text size="md">
Telepon
</Text>
</Group>
<Text size="md" c="white">08123456789</Text>
</Group>
<Group justify="space-between" >
<Group gap="xs">
<IconMessageReport size={20} />
<Text size="md">
Jumlah Pengaduan
</Text>
</Group>
<Text size="md" c="white">10</Text>
</Group>
<Group justify="space-between" >
<Group gap="xs">
<IconFileCertificate size={20} />
<Text size="md">
Jumlah Pelayanan Surat
</Text>
</Group>
<Text size="md" c="white">10</Text>
</Group>
</Stack>
</Stack>
</Card>
);
}

View File

@@ -115,7 +115,9 @@ type StatusKey =
| "ditolak"
| "selesai"
| "semua";
function ListPengaduan({ status }: { status: StatusKey }) {
const navigate = useNavigate();
const [page, setPage] = useState(1);
const [value, setValue] = useState("");
const { data, mutate, isLoading } = useSwr("/", () =>
@@ -196,6 +198,7 @@ function ListPengaduan({ status }: { status: StatusKey }) {
borderColor: "rgba(100,100,100,0.2)",
boxShadow: "0 0 20px rgba(0,255,200,0.08)",
}}
onClick={() => navigate(`/scr/dashboard/pengaduan/detail?id=${v.id}`)}
>
<Stack gap="md">
<Flex align="center" justify="space-between">

View File

@@ -0,0 +1,42 @@
export function mimeToExtension(mimeType: string): string {
const map: Record<string, string> = {
"image/jpeg": "jpg",
"image/png": "png",
"image/gif": "gif",
"image/webp": "webp",
"image/svg+xml": "svg",
"image/bmp": "bmp",
"image/tiff": "tiff",
"video/mp4": "mp4",
"video/webm": "webm",
"video/ogg": "ogv",
"video/quicktime": "mov",
"audio/mpeg": "mp3",
"audio/wav": "wav",
"audio/ogg": "ogg",
"audio/webm": "weba",
"application/pdf": "pdf",
"application/zip": "zip",
"application/x-zip-compressed": "zip",
"application/json": "json",
"application/javascript": "js",
"application/x-httpd-php": "php",
"application/msword": "doc",
"application/vnd.openxmlformats-officedocument.wordprocessingml.document": "docx",
"application/vnd.ms-excel": "xls",
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": "xlsx",
"application/vnd.ms-powerpoint": "ppt",
"application/vnd.openxmlformats-officedocument.presentationml.presentation": "pptx",
"text/plain": "txt",
"text/html": "html",
"text/css": "css",
"text/csv": "csv",
"text/xml": "xml",
};
return map[mimeType.toLowerCase()] || "bin"; // default jika tidak dikenal
}

View File

@@ -162,7 +162,7 @@ export async function uploadFile(config: Config, file: File): Promise<string> {
if (!res.ok) throw new Error(`Upload failed: ${text}`);
return `✅ Uploaded ${file.name} successfully`;
}
export async function uploadFileBase64(config: Config, base64File: { name: string; data: string }): Promise<string> {
export async function uploadFileBase64(config: Config, base64File: { name: string; data: string; }): Promise<string> {
const remoteName = path.basename(base64File.name);
// 1. Dapatkan upload link (pakai Authorization)

View File

@@ -38,10 +38,7 @@ const PelayananRoute = new Elysia({
}
})
return `
${JSON.stringify(body)}
kategori pelayanan surat sudah dibuat`
return { success: true, message: 'kategori pelayanan surat sudah dibuat' }
}, {
body: t.Object({
name: t.String({ minLength: 1, error: "name harus diisi" }),
@@ -67,10 +64,7 @@ const PelayananRoute = new Elysia({
}
})
return `
${JSON.stringify(body)}
kategori pelayanan surat sudah diperbarui`
return { success: true, message: 'kategori pelayanan surat sudah diperbarui' }
}, {
body: t.Object({
id: t.String({ minLength: 1, error: "id harus diisi" }),
@@ -95,10 +89,7 @@ const PelayananRoute = new Elysia({
}
})
return `
${JSON.stringify(body)}
kategori pelayanan surat sudah dihapus`
return { success: true, message: 'kategori pelayanan surat sudah dihapus' }
}, {
body: t.Object({
id: t.String({ minLength: 1, error: "id harus diisi" }),
@@ -252,10 +243,7 @@ const PelayananRoute = new Elysia({
}
})
return `
${JSON.stringify(body)}
pengaduan sudah dibuat`
return { success: true, message: 'pengajuan surat sudah dibuat' }
}, {
body: t.Object({
idCategory: t.String({ minLength: 1, error: "idCategory harus diisi" }),
@@ -300,10 +288,7 @@ const PelayananRoute = new Elysia({
}
})
return `
${JSON.stringify(body)}
pengajuan surat sudah diperbarui`
return { success: true, message: 'pengajuan surat sudah diperbarui' }
}, {
body: t.Object({
id: t.String({ minLength: 1, error: "id harus diisi" }),

View File

@@ -1,6 +1,8 @@
import Elysia, { t } from "elysia"
import type { StatusPengaduan } from "generated/prisma"
import { v4 as uuidv4 } from "uuid"
import { getLastUpdated } from "../lib/get-last-updated"
import { mimeToExtension } from "../lib/mimetypeToExtension"
import { generateNoPengaduan } from "../lib/no-pengaduan"
import { normalizePhoneNumber } from "../lib/normalizePhone"
import { prisma } from "../lib/prisma"
@@ -38,10 +40,7 @@ const PengaduanRoute = new Elysia({
}
})
return `
${JSON.stringify(body)}
kategori pengaduan sudah dibuat`
return { success: true, message: 'kategori pengaduan sudah dibuat' }
}, {
body: t.Object({
name: t.String({ minLength: 1, error: "name harus diisi" }),
@@ -63,10 +62,7 @@ const PengaduanRoute = new Elysia({
}
})
return `
${JSON.stringify(body)}
kategori pengaduan sudah diperbarui`
return { success: true, message: 'kategori pengaduan sudah diperbarui' }
}, {
body: t.Object({
id: t.String({ minLength: 1, error: "id harus diisi" }),
@@ -89,10 +85,7 @@ const PengaduanRoute = new Elysia({
}
})
return `
${JSON.stringify(body)}
kategori pengaduan sudah dihapus`
return { success: true, message: 'kategori pengaduan sudah dihapus' }
}, {
body: t.Object({
id: t.String({ minLength: 1, error: "id harus diisi" }),
@@ -189,10 +182,7 @@ const PengaduanRoute = new Elysia({
}
})
return `
${JSON.stringify(body)}
pengaduan sudah dibuat`
return { success: true, message: 'pengaduan sudah dibuat' }
}, {
body: t.Object({
title: t.String({ minLength: 1, error: "title harus diisi" }),
@@ -246,10 +236,7 @@ const PengaduanRoute = new Elysia({
}
})
return `
${JSON.stringify(body)}
status pengaduan sudah diupdate`
return { success: true, message: 'status pengaduan sudah diupdate' }
}, {
body: t.Object({
id: t.String({ minLength: 1, error: "id harus diisi" }),
@@ -463,31 +450,35 @@ const PengaduanRoute = new Elysia({
},
})
.post("/upload-base64", async ({ body }) => {
const { file } = body;
const { data, mimetype } = body;
const ext = mimeToExtension(mimetype)
const name = `${uuidv4()}.${ext}`
// Validasi file
if (!file) {
if (!data) {
return { success: false, message: "File tidak ditemukan" };
}
// Konversi file ke base64
const buffer = await file.arrayBuffer();
const base64String = Buffer.from(buffer).toString("base64");
// const buffer = await file.arrayBuffer();
// const base64String = Buffer.from(buffer).toString("base64");
// (Opsional) jika perlu dikirim ke Seafile sebagai base64
const result = await uploadFileBase64(defaultConfigSF, { name: file.name, data: base64String });
const result = await uploadFileBase64(defaultConfigSF, { name: name, data: data });
return {
success: true,
message: "Upload berhasil",
filename: file.name,
size: file.size,
base64Preview: base64String.slice(0, 100) + "...", // hanya preview
seafileResult: result
data: {
name,
mimetype,
ext,
}
};
}, {
body: t.Object({
file: t.File({ format: "binary" })
data: t.String(),
mimetype: t.String()
}),
detail: {
summary: "Upload File (Base64)",
@@ -572,6 +563,7 @@ const PengaduanRoute = new Elysia({
const dataFix = data.map((item) => {
return {
noPengaduan: item.noPengaduan,
id: item.id,
title: item.title,
detail: item.detail,
status: item.status,

53
src/server/routes/test.ts Normal file
View File

@@ -0,0 +1,53 @@
import Elysia, { t } from "elysia";
const TestRoute = new Elysia({
prefix: "test",
tags: ["mcp", "test"],
})
.get("/info-rapat-list", () => {
return {
success: true,
message: "data info rapat berhasil diambil",
data: [
{
judul: "Info Rapat",
tanggal: "2025-11-10",
deskripsi: "Info rapat",
gambar: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8/x8AAwMCAO+ip1sAAAAASUVORK5CYII="
}
]
}
}, {
detail: {
summary: "mendapatkan list rapat",
description: "mendapatkan list rapat dari database",
}
})
.post("/simpan-rapat", ({ body }) => {
if (!body.gambar) {
return {
success: false,
message: "gambar harus diisi",
}
}
return {
success: true,
message: "data info rapat berhasil diambil",
chunk: body.gambar.substring(22)
}
}, {
body: t.Object({
judul: t.String(),
tanggal: t.String(),
deskripsi: t.String(),
gambar: t.Required(t.String()),
}),
detail: {
summary: "simpan data rapat",
description: "simpan data rapat memerlukan base64 gambar",
}
})
export default TestRoute

View File

@@ -1,2 +1,5 @@
IMAGE_BASE64=$(base64 image.png | tr -d '\n')
curl -X POST http://localhost:3000/api/pengaduan/upload-base64 \
-F file=@package.json
-H "Content-Type: application/json" \
-d "{\"file\": \"$IMAGE_BASE64\"}"

154
x.ts
View File

@@ -1,133 +1,39 @@
/**
* src/utils/swagger-to-mcp.ts
*
* Auto-converter: Swagger (OpenAPI) → MCP manifest (real-time)
*
* - Fetch swagger JSON dynamically from process.env.BUN_PUBLIC_BASE_URL + "/docs/json"
* - Generate MCP manifest for AI discovery (/.well-known/mcp.json)
* - Can be used as Bun CLI or integrated in Elysia route
*/
import fs from "fs";
import { writeFileSync } from "fs"
// 1⃣ File yang mau diupload
const filePath = "image.png";
const apiUrl = "http://localhost:3000/api/pengaduan/upload-base64";
interface OpenAPI {
info: { title?: string; description?: string; version?: string }
paths: Record<string, any>
}
// 2⃣ Baca file dan ubah ke base64
const fileBuffer = fs.readFileSync(filePath);
const base64Data = fileBuffer.toString("base64");
interface McpManifest {
schema_version: string
name: string
description: string
version?: string
endpoints: Record<string, string>
capabilities: Record<string, any>
contact?: { email?: string }
}
// 3⃣ Buat payload JSON
const payload = {
data: base64Data,
mimetype: "image/png"
};
/**
* Convert OpenAPI JSON to MCP manifest format
*/
async function convertOpenApiToMcp(baseUrl: string): Promise<McpManifest> {
const res = await fetch(`${baseUrl}/docs/json`)
if (!res.ok) throw new Error(`Failed to fetch Swagger JSON from ${baseUrl}/docs/json`)
// 4⃣ Kirim ke server pakai fetch
async function uploadBase64() {
try {
const res = await fetch(apiUrl, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(payload),
});
const openapi: OpenAPI = await res.json()
const manifest: McpManifest = {
schema_version: "1.0",
name: openapi.info?.title ?? "MCP Server",
description: openapi.info?.description ?? "Auto-generated MCP manifest from Swagger",
version: openapi.info?.version ?? "0.0.0",
endpoints: {
openapi: `${baseUrl}/docs/json`,
mcp: `${baseUrl}/.well-known/mcp.json`
},
capabilities: {}
if (!res.ok) {
throw new Error(`Request failed: ${res.status} ${res.statusText}`);
}
for (const [path, methods] of Object.entries(openapi.paths || {})) {
for (const [method, def] of Object.entries<any>(methods)) {
const tags = def.tags || ["default"]
const tag = tags[0]
const operationId = def.operationId || `${method}_${path.replace(/[\/{}]/g, "_")}`
manifest.capabilities[tag] ??= {}
// Extract parameters and body schema
const params: Record<string, string> = {}
const required: string[] = []
if (Array.isArray(def.parameters)) {
for (const p of def.parameters) {
const type = p.schema?.type || "string"
params[p.name] = type
if (p.required) required.push(p.name)
}
}
const bodySchema = def.requestBody?.content?.["application/json"]?.schema
if (bodySchema?.properties) {
for (const [key, prop] of Object.entries<any>(bodySchema.properties)) {
params[key] = prop.type || "string"
}
if (Array.isArray(bodySchema.required))
required.push(...bodySchema.required)
}
// Generate example cURL
const sampleCurl = [
`curl -X ${method.toUpperCase()} ${baseUrl}${path}`,
Object.keys(params).length > 0
? ` -H 'Content-Type: application/json' -d '${JSON.stringify(
Object.fromEntries(Object.keys(params).map(k => [k, params[k] === "string" ? k : "value"]))
)}'`
: ""
]
.filter(Boolean)
.join(" \\\n")
manifest.capabilities[tag][operationId] = {
method: method.toUpperCase(),
path,
summary: def.summary || def.description || "",
parameters: Object.keys(params).length > 0 ? params : undefined,
required: required.length > 0 ? required : undefined,
command: sampleCurl
}
}
}
return manifest
const result = await res.json();
console.log("✅ Upload sukses:", result);
} catch (err) {
console.error("❌ Upload gagal:", err);
}
}
/**
* CLI entry
* bun run src/utils/swagger-to-mcp.ts
*/
if (import.meta.main) {
const baseUrl = process.env.BUN_PUBLIC_BASE_URL
if (!baseUrl) {
console.error("❌ Missing BUN_PUBLIC_BASE_URL environment variable.")
process.exit(1)
}
convertOpenApiToMcp(baseUrl)
.then(manifest => {
writeFileSync(".well-known/mcp.json", JSON.stringify(manifest, null, 2))
console.log("✅ Generated .well-known/mcp.json")
})
.catch(err => console.error("❌ Failed to convert Swagger → MCP:", err))
}
/**
* Optional: Elysia integration
* Automatically serve /.well-known/mcp.json
*/
// import Elysia from "elysia"
// new Elysia()
// .get("/.well-known/mcp.json", async () => {
// const baseUrl = process.env.BUN_PUBLIC_BASE_URL!
// return await convertOpenApiToMcp(baseUrl)
// })
// .listen(3000)
uploadBase64();