diff --git a/.well-known/mcp.json b/.well-known/mcp.json new file mode 100644 index 0000000..44a4d5b --- /dev/null +++ b/.well-known/mcp.json @@ -0,0 +1,149 @@ +{ + "schema_version": "1.0", + "name": "Elysia Documentation", + "description": "Development documentation", + "version": "0.0.0", + "endpoints": { + "openapi": "http://localhost:3000/docs/json", + "mcp": "http://localhost:3000/.well-known/mcp.json" + }, + "capabilities": { + "apikey": { + "postApiApikeyCreate": { + "method": "POST", + "path": "/api/apikey/create", + "summary": "create api key", + "parameters": { + "name": "string", + "description": "string", + "expiredAt": "string" + }, + "required": [ + "name", + "description" + ], + "command": "curl -X POST http://localhost:3000/api/apikey/create \\\n -H 'Content-Type: application/json' -d '{\"name\":\"name\",\"description\":\"description\",\"expiredAt\":\"expiredAt\"}'" + }, + "getApiApikeyList": { + "method": "GET", + "path": "/api/apikey/list", + "summary": "get api key list", + "command": "curl -X GET http://localhost:3000/api/apikey/list" + }, + "deleteApiApikeyDelete": { + "method": "DELETE", + "path": "/api/apikey/delete", + "summary": "delete api key", + "parameters": { + "id": "string" + }, + "required": [ + "id" + ], + "command": "curl -X DELETE http://localhost:3000/api/apikey/delete \\\n -H 'Content-Type: application/json' -d '{\"id\":\"id\"}'" + } + }, + "darmasaba": { + "getApiDarmasabaRepos": { + "method": "GET", + "path": "/api/darmasaba/repos", + "summary": "/repos", + "command": "curl -X GET http://localhost:3000/api/darmasaba/repos" + }, + "getApiDarmasabaLs": { + "method": "GET", + "path": "/api/darmasaba/ls", + "summary": "/ls", + "command": "curl -X GET http://localhost:3000/api/darmasaba/ls" + }, + "getApiDarmasabaLsByDir": { + "method": "GET", + "path": "/api/darmasaba/ls/{dir}", + "summary": "/ls/:dir", + "parameters": { + "dir": "string" + }, + "required": [ + "dir" + ], + "command": "curl -X GET http://localhost:3000/api/darmasaba/ls/{dir} \\\n -H 'Content-Type: application/json' -d '{\"dir\":\"dir\"}'" + }, + "getApiDarmasabaFileByDirByFile_name": { + "method": "GET", + "path": "/api/darmasaba/file/{dir}/{file_name}", + "summary": "/file/:dir/:file_name", + "parameters": { + "dir": "string", + "file_name": "string" + }, + "required": [ + "dir", + "file_name" + ], + "command": "curl -X GET http://localhost:3000/api/darmasaba/file/{dir}/{file_name} \\\n -H 'Content-Type: application/json' -d '{\"dir\":\"dir\",\"file_name\":\"file_name\"}'" + } + }, + "default": { + "getApiUserFind": { + "method": "GET", + "path": "/api/user/find", + "summary": "", + "command": "curl -X GET http://localhost:3000/api/user/find" + }, + "postApiCredentialCreate": { + "method": "POST", + "path": "/api/credential/create", + "summary": "", + "parameters": { + "name": "string", + "value": "string" + }, + "required": [ + "name", + "value" + ], + "command": "curl -X POST http://localhost:3000/api/credential/create \\\n -H 'Content-Type: application/json' -d '{\"name\":\"name\",\"value\":\"value\"}'" + }, + "getApiCredentialList": { + "method": "GET", + "path": "/api/credential/list", + "summary": "", + "command": "curl -X GET http://localhost:3000/api/credential/list" + }, + "deleteApiCredentialRm": { + "method": "DELETE", + "path": "/api/credential/rm", + "summary": "", + "parameters": { + "id": "string" + }, + "required": [ + "id" + ], + "command": "curl -X DELETE http://localhost:3000/api/credential/rm \\\n -H 'Content-Type: application/json' -d '{\"id\":\"id\"}'" + } + }, + "auth": { + "postAuthLogin": { + "method": "POST", + "path": "/auth/login", + "summary": "login", + "parameters": { + "email": "string", + "password": "string" + }, + "required": [ + "email", + "password" + ], + "command": "curl -X POST http://localhost:3000/auth/login \\\n -H 'Content-Type: application/json' -d '{\"email\":\"email\",\"password\":\"password\"}'" + }, + "deleteAuthLogout": { + "method": "DELETE", + "path": "/auth/logout", + "summary": "logout", + "command": "curl -X DELETE http://localhost:3000/auth/logout" + } + } + } +} \ No newline at end of file diff --git a/bun.lock b/bun.lock index 271afc5..8b87bab 100644 --- a/bun.lock +++ b/bun.lock @@ -21,6 +21,7 @@ "react-dom": "^19", "react-router-dom": "^7.9.3", "swr": "^2.3.6", + "valtio": "^2.1.8", }, "devDependencies": { "@types/bun": "latest", @@ -212,6 +213,8 @@ "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=="], + "proxy-compare": ["proxy-compare@3.0.1", "", {}, "sha512-V9plBAt3qjMlS1+nC8771KNf6oJ12gExvaxnNzN/9yVRLdTv/lc+oJlnSzrdYDAvBfTStPCoiaCOTmTs0adv7Q=="], + "pure-rand": ["pure-rand@6.1.0", "", {}, "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA=="], "rc9": ["rc9@2.1.2", "", { "dependencies": { "defu": "^6.1.4", "destr": "^2.0.3" } }, "sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg=="], @@ -276,6 +279,8 @@ "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], + "valtio": ["valtio@2.1.8", "", { "dependencies": { "proxy-compare": "^3.0.1" }, "peerDependencies": { "@types/react": ">=18.0.0", "react": ">=18.0.0" }, "optionalPeers": ["@types/react", "react"] }, "sha512-fjTPbJyKEmfVBZUOh3V0OtMHoFUGr4+4XpejjxhNJE/IS2l8rDbyJuzi3w/fZWBDyk7BJOpG+lmvTK5iiVhXuQ=="], + "zhead": ["zhead@2.2.4", "", {}, "sha512-8F0OI5dpWIA5IGG5NHUg9staDwz/ZPxZtvGVf01j7vHqSyZ0raHY+78atOVxRqb73AotX22uV1pXt3gYSstGag=="], "zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], diff --git a/mcp.json b/mcp.json new file mode 100644 index 0000000..d6ff80b --- /dev/null +++ b/mcp.json @@ -0,0 +1,72 @@ +{ + "name": "Elysia Documentation", + "description": "Development documentation", + "version": "0.0.0", + "capabilities": { + "postapiapikeycreate": { + "enabled": true, + "command": "curl -X POST http://localhost:3000/api/apikey/create", + "description": "create api key" + }, + "getapiapikeylist": { + "enabled": true, + "command": "curl -X GET http://localhost:3000/api/apikey/list", + "description": "get api key list" + }, + "deleteapiapikeydelete": { + "enabled": true, + "command": "curl -X DELETE http://localhost:3000/api/apikey/delete", + "description": "delete api key" + }, + "getapidarmasabarepos": { + "enabled": true, + "command": "curl -X GET http://localhost:3000/api/darmasaba/repos", + "description": "/repos" + }, + "getapidarmasabals": { + "enabled": true, + "command": "curl -X GET http://localhost:3000/api/darmasaba/ls", + "description": "/ls" + }, + "getapidarmasabalsbydir": { + "enabled": true, + "command": "curl -X GET http://localhost:3000/api/darmasaba/ls/{dir}", + "description": "/ls/:dir" + }, + "getapidarmasabafilebydirbyfile_name": { + "enabled": true, + "command": "curl -X GET http://localhost:3000/api/darmasaba/file/{dir}/{file_name}", + "description": "/file/:dir/:file_name" + }, + "getapiuserfind": { + "enabled": true, + "command": "curl -X GET http://localhost:3000/api/user/find", + "description": "GET /api/user/find" + }, + "postapicredentialcreate": { + "enabled": true, + "command": "curl -X POST http://localhost:3000/api/credential/create", + "description": "POST /api/credential/create" + }, + "getapicredentiallist": { + "enabled": true, + "command": "curl -X GET http://localhost:3000/api/credential/list", + "description": "GET /api/credential/list" + }, + "deleteapicredentialrm": { + "enabled": true, + "command": "curl -X DELETE http://localhost:3000/api/credential/rm", + "description": "DELETE /api/credential/rm" + }, + "postauthlogin": { + "enabled": true, + "command": "curl -X POST http://localhost:3000/auth/login", + "description": "login" + }, + "deleteauthlogout": { + "enabled": true, + "command": "curl -X DELETE http://localhost:3000/auth/logout", + "description": "logout" + } + } +} \ No newline at end of file diff --git a/package.json b/package.json index 8c24816..f32686a 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,8 @@ "react": "^19", "react-dom": "^19", "react-router-dom": "^7.9.3", - "swr": "^2.3.6" + "swr": "^2.3.6", + "valtio": "^2.1.8" }, "devDependencies": { "@types/bun": "latest", diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 8540516..53bfd2b 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -29,3 +29,11 @@ model ApiKey { createdAt DateTime @default(now()) updatedAt DateTime @updatedAt } + +model Credential { + id String @id @default(cuid()) + name String? + value String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} diff --git a/src/AppRoutes.tsx b/src/AppRoutes.tsx index 45bc1f4..6f5bf04 100644 --- a/src/AppRoutes.tsx +++ b/src/AppRoutes.tsx @@ -7,6 +7,7 @@ import ProtectedRoute from "./components/ProtectedRoute"; import Dashboard from "./pages/dashboard/dashboard_page"; import DashboardLayout from "./pages/dashboard/dashboard_layout"; import ApiKeyPage from "./pages/dashboard/apikey/apikey_page"; +import CredentialPage from "./pages/dashboard/credential/credential_page"; export default function AppRoutes() { return ( @@ -19,6 +20,7 @@ export default function AppRoutes() { } /> } /> } /> + } /> diff --git a/src/clientRoutes.ts b/src/clientRoutes.ts index cf19abe..147a844 100644 --- a/src/clientRoutes.ts +++ b/src/clientRoutes.ts @@ -5,6 +5,7 @@ const clientRoutes = { "/dashboard": "/dashboard", "/dashboard/landing": "/dashboard/landing", "/dashboard/apikey": "/dashboard/apikey", + "/dashboard/credential": "/dashboard/credential", "/*": "/*" } as const; diff --git a/src/index.tsx b/src/index.tsx index 2909cec..d22d428 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,12 +1,14 @@ -import Elysia, { t } from "elysia"; import Swagger from "@elysiajs/swagger"; -import html from "./index.html" -import Dashboard from "./server/routes/darmasaba"; -import apiAuth from "./server/middlewares/apiAuth"; -import Auth from "./server/routes/auth_route"; -import ApiKeyRoute from "./server/routes/apikey_route"; +import Elysia from "elysia"; import type { User } from "generated/prisma"; +import html from "./index.html"; +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"; +import DarmasabaRoute from "./server/routes/darmasaba_route"; +import { convertOpenApiToMcp } from "./server/lib/mcp-converter"; const Docs = new Elysia() .use(Swagger({ @@ -28,13 +30,23 @@ const Api = new Elysia({ }) .use(apiAuth) .use(ApiKeyRoute) - .use(Dashboard) + .use(DarmasabaRoute) .use(ApiUser) + .use(CredentialRoute) const app = new Elysia() .use(Api) .use(Docs) .use(Auth) + .get("/.well-known/mcp.json", async () => { + const baseUrl = process.env.BUN_PUBLIC_BASE_URL! + return await convertOpenApiToMcp(baseUrl) + }, { + detail: { + description: "MCP manifest", + tags: ["MCP"], + } + }) .get("*", html) .listen(3000, () => { console.log("Server running at http://localhost:3000"); diff --git a/src/pages/dashboard/credential/credential_page.tsx b/src/pages/dashboard/credential/credential_page.tsx new file mode 100644 index 0000000..a3670cb --- /dev/null +++ b/src/pages/dashboard/credential/credential_page.tsx @@ -0,0 +1,89 @@ +import apiFetch from "@/lib/apiFetch"; +import { Button, Card, Container, Flex, Group, Paper, Stack, Text, TextInput, Title } from "@mantine/core"; +import { useShallowEffect } from "@mantine/hooks"; +import { showNotification } from "@mantine/notifications"; +import { useState } from "react"; +import useSwr from 'swr' +import { proxy, subscribe, useSnapshot } from 'valtio' + +const state = proxy({ + reload: "" +}) + +function reloadState() { + state.reload = Math.random().toString() +} + +export default function CredentialPage() { + return + + + + + +} + +function CredentialCreate() { + const [name, setName] = useState("") + const [apikey, setApikey] = useState("") + + async function handleSubmit() { + const { data } = await apiFetch.api.credential.create.post({ + name: name, + value: apikey + }) + + setName("") + setApikey("") + + showNotification({ + message: data?.message + }) + + reloadState() + } + return + + Credential Create + setName(e.target.value)} /> + setApikey(e.target.value)} /> + + + + + +} + +function CredentialList() { + const { data, mutate } = useSwr("/", () => apiFetch.api.credential.list.get()) + + useShallowEffect(() => { + const unsubscribe = subscribe(state, async () => { + console.log('state has changed to', state) + mutate() + }) + + return () => unsubscribe() + }, []) + + async function handleRm(id: string) { + await apiFetch.api.credential.rm.delete({ + id: id + }) + + reloadState() + + } + return + + {data?.data?.list.map((v, k) => + + {v.name} + + + + + )} + + +} \ No newline at end of file diff --git a/src/pages/dashboard/dashboard_layout.tsx b/src/pages/dashboard/dashboard_layout.tsx index fa6b626..5f7a1db 100644 --- a/src/pages/dashboard/dashboard_layout.tsx +++ b/src/pages/dashboard/dashboard_layout.tsx @@ -21,7 +21,9 @@ import { useLocalStorage } from '@mantine/hooks' import { IconChevronLeft, IconChevronRight, - IconDashboard + IconDashboard, + IconKey, + IconLock } from '@tabler/icons-react' import type { User } from 'generated/prisma' import { Outlet, useLocation, useNavigate } from 'react-router-dom' @@ -170,11 +172,18 @@ function NavigationDashboard() { /> } + leftSection={} label="Dashboard Overview" description="Quick summary and activity highlights" onClick={() => navigate(clientRoutes['/dashboard/apikey'])} /> + } + label="Dashboard Overview" + description="Quick summary and activity highlights" + onClick={() => navigate(clientRoutes['/dashboard/credential'])} + /> ) } diff --git a/src/server/lib/mcp-converter.ts b/src/server/lib/mcp-converter.ts new file mode 100644 index 0000000..ebeff1d --- /dev/null +++ b/src/server/lib/mcp-converter.ts @@ -0,0 +1,133 @@ +/** + * 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 { writeFileSync } from "fs" + +interface OpenAPI { + info: { title?: string; description?: string; version?: string } + paths: Record +} + +interface McpManifest { + schema_version: string + name: string + description: string + version?: string + endpoints: Record + capabilities: Record + contact?: { email?: string } +} + +/** + * Convert OpenAPI JSON to MCP manifest format + */ +export async function convertOpenApiToMcp(baseUrl: string): Promise { + const res = await fetch(`${baseUrl}/docs/json`) + if (!res.ok) throw new Error(`Failed to fetch Swagger JSON from ${baseUrl}/docs/json`) + + 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: {} + } + + for (const [path, methods] of Object.entries(openapi.paths || {})) { + for (const [method, def] of Object.entries(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 = {} + 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(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 +} + +/** + * 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) diff --git a/src/server/routes/credential_route.ts b/src/server/routes/credential_route.ts new file mode 100644 index 0000000..9b9a5e6 --- /dev/null +++ b/src/server/routes/credential_route.ts @@ -0,0 +1,46 @@ +import Elysia, { t } from "elysia"; +import { prisma } from "../lib/prisma"; + +const CredentialRoute = new Elysia({ + prefix: "/credential" +}) + .post("/create", async (ctx) => { + const { name, value } = ctx.body + const create = await prisma.credential.create({ + data: { + name, + value + } + }) + return { + message: "success", + create + } + }, { + body: t.Object({ + name: t.String(), + value: t.String(), + }) + }) + .get("/list", async (ctx) => { + const list = await prisma.credential.findMany() + return { + message: "success", + list + } + }) + .delete("/rm", async (ctx) => { + const { id } = ctx.body + const rm = await prisma.credential.delete({ + where: { + id: id + } + }) + + }, { + body: t.Object({ + id: t.String() + }) + }) + +export default CredentialRoute \ No newline at end of file diff --git a/src/server/routes/darmasaba.ts b/src/server/routes/darmasaba.ts deleted file mode 100644 index 3d483eb..0000000 --- a/src/server/routes/darmasaba.ts +++ /dev/null @@ -1,8 +0,0 @@ -import Elysia from "elysia"; - -const Dashboard = new Elysia({ - prefix: "/dashboard" -}) - .get("/apa", () => "Hello World") - -export default Dashboard diff --git a/src/server/routes/darmasaba_route.ts b/src/server/routes/darmasaba_route.ts new file mode 100644 index 0000000..6b2ec78 --- /dev/null +++ b/src/server/routes/darmasaba_route.ts @@ -0,0 +1,131 @@ +import Elysia, { t } from "elysia"; + +const url = "https://cld-dkr-makuro-seafile.wibudev.com/api2" +const TOKEN = "fa49bf1774cad2ec89d2882ae2c6ac1f5d7df445" + +const DarmasabaRoute = new Elysia({ + prefix: "/darmasaba", + tags: ["darmasaba"] +}) + .get("/repos", async () => { + const res = await fetch(url + "/repos", { + headers: { + Authorization: "Bearer " + TOKEN + } + }) + + if (!res.ok) { + console.log(res) + return { + message: "Failed to fetch directory" + } + } + const data = await res.json() as { name: string, id: string, type: string }[] + return data.map((v) => { + return { + name: v.name, + id: v.id, + type: v.type + } + }) + }, { + detail: { + summary: "/repos", + description: "get list of repositories" + } + }) + .get("/ls", async () => { + const res = await fetch(url + `/repos/de64ff3c-0081-45f3-a5a6-6c799a098649/dir/?p=${encodeURIComponent('darmasaba')}`, { + headers: { + Authorization: "Bearer " + TOKEN + } + }) + + if (!res.ok) { + console.log(res) + return { + message: "Failed to fetch directory" + } + } + const data = await res.json() as { name: string, id: string, type: string }[] + return data.map((v) => { + return { + name: v.name, + id: v.id, + type: v.type + } + }) + }, { + detail: { + summary: "/ls", + description: "get list of dir in darmasaba" + } + }) + .get("/ls/:dir", async ({ params }) => { + const { dir } = params + const res = await fetch(url + `/repos/de64ff3c-0081-45f3-a5a6-6c799a098649/dir/?p=${encodeURIComponent('darmasaba/' + dir)}`, { + headers: { + Authorization: "Bearer " + TOKEN + } + }) + + if (!res.ok) { + console.log(res) + return { + message: "Failed to fetch directory" + } + } + const data = await res.json() as { name: string, id: string, type: string }[] + return data.map((v) => { + return { + name: v.name, + id: v.id, + type: v.type + } + }) + }, { + params: t.Object({ + dir: t.String() + }), + detail: { + summary: "/ls/:dir", + description: "get list of files in darmasaba/" + } + }) + .get("/file/:dir/:file_name", async ({ params }) => { + const { dir, file_name } = params + const res = await fetch(url + `/repos/de64ff3c-0081-45f3-a5a6-6c799a098649/file/?p=${encodeURIComponent('darmasaba/' + dir + '/' + file_name)}`, { + headers: { + Authorization: "Bearer " + TOKEN + } + }) + + if (!res.ok) { + console.log(res) + return { + message: "Failed to fetch directory" + } + } + + const downloadUrl = (await res.text()).replace(/"/g, ''); + + const resText = await fetch(downloadUrl, { + headers: { + Authorization: "Bearer " + TOKEN + } + }) + + return resText.text() + }, { + params: t.Object({ + dir: t.String(), + file_name: t.String() + }), + detail: { + summary: "/file/:dir/:file_name", + description: "get content of file in darmasaba//" + } + }) + + +export default DarmasabaRoute diff --git a/x.ts b/x.ts new file mode 100644 index 0000000..787484d --- /dev/null +++ b/x.ts @@ -0,0 +1,133 @@ +/** + * 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 { writeFileSync } from "fs" + +interface OpenAPI { + info: { title?: string; description?: string; version?: string } + paths: Record +} + +interface McpManifest { + schema_version: string + name: string + description: string + version?: string + endpoints: Record + capabilities: Record + contact?: { email?: string } +} + +/** + * Convert OpenAPI JSON to MCP manifest format + */ +async function convertOpenApiToMcp(baseUrl: string): Promise { + const res = await fetch(`${baseUrl}/docs/json`) + if (!res.ok) throw new Error(`Failed to fetch Swagger JSON from ${baseUrl}/docs/json`) + + 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: {} + } + + for (const [path, methods] of Object.entries(openapi.paths || {})) { + for (const [method, def] of Object.entries(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 = {} + 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(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 +} + +/** + * 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)