feat: add credential routes and mcp manifest

This commit is contained in:
bipproduction
2025-10-08 14:17:06 +08:00
parent 94a8d78fe3
commit 2366710ccd
15 changed files with 801 additions and 18 deletions

149
.well-known/mcp.json Normal file
View File

@@ -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"
}
}
}
}

View File

@@ -21,6 +21,7 @@
"react-dom": "^19", "react-dom": "^19",
"react-router-dom": "^7.9.3", "react-router-dom": "^7.9.3",
"swr": "^2.3.6", "swr": "^2.3.6",
"valtio": "^2.1.8",
}, },
"devDependencies": { "devDependencies": {
"@types/bun": "latest", "@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=="], "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=="], "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=="], "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=="], "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=="], "zhead": ["zhead@2.2.4", "", {}, "sha512-8F0OI5dpWIA5IGG5NHUg9staDwz/ZPxZtvGVf01j7vHqSyZ0raHY+78atOVxRqb73AotX22uV1pXt3gYSstGag=="],
"zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], "zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],

72
mcp.json Normal file
View File

@@ -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"
}
}
}

View File

@@ -26,7 +26,8 @@
"react": "^19", "react": "^19",
"react-dom": "^19", "react-dom": "^19",
"react-router-dom": "^7.9.3", "react-router-dom": "^7.9.3",
"swr": "^2.3.6" "swr": "^2.3.6",
"valtio": "^2.1.8"
}, },
"devDependencies": { "devDependencies": {
"@types/bun": "latest", "@types/bun": "latest",

View File

@@ -29,3 +29,11 @@ model ApiKey {
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
} }
model Credential {
id String @id @default(cuid())
name String?
value String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}

View File

@@ -7,6 +7,7 @@ import ProtectedRoute from "./components/ProtectedRoute";
import Dashboard from "./pages/dashboard/dashboard_page"; import Dashboard from "./pages/dashboard/dashboard_page";
import DashboardLayout from "./pages/dashboard/dashboard_layout"; import DashboardLayout from "./pages/dashboard/dashboard_layout";
import ApiKeyPage from "./pages/dashboard/apikey/apikey_page"; import ApiKeyPage from "./pages/dashboard/apikey/apikey_page";
import CredentialPage from "./pages/dashboard/credential/credential_page";
export default function AppRoutes() { export default function AppRoutes() {
return ( return (
@@ -19,6 +20,7 @@ export default function AppRoutes() {
<Route index element={<Dashboard />} /> <Route index element={<Dashboard />} />
<Route path="landing" element={<Dashboard />} /> <Route path="landing" element={<Dashboard />} />
<Route path="apikey" element={<ApiKeyPage />} /> <Route path="apikey" element={<ApiKeyPage />} />
<Route path="credential" element={<CredentialPage />} />
</Route> </Route>
</Route> </Route>

View File

@@ -5,6 +5,7 @@ const clientRoutes = {
"/dashboard": "/dashboard", "/dashboard": "/dashboard",
"/dashboard/landing": "/dashboard/landing", "/dashboard/landing": "/dashboard/landing",
"/dashboard/apikey": "/dashboard/apikey", "/dashboard/apikey": "/dashboard/apikey",
"/dashboard/credential": "/dashboard/credential",
"/*": "/*" "/*": "/*"
} as const; } as const;

View File

@@ -1,12 +1,14 @@
import Elysia, { t } from "elysia";
import Swagger from "@elysiajs/swagger"; import Swagger from "@elysiajs/swagger";
import html from "./index.html" import Elysia from "elysia";
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 type { User } from "generated/prisma"; 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() const Docs = new Elysia()
.use(Swagger({ .use(Swagger({
@@ -28,13 +30,23 @@ const Api = new Elysia({
}) })
.use(apiAuth) .use(apiAuth)
.use(ApiKeyRoute) .use(ApiKeyRoute)
.use(Dashboard) .use(DarmasabaRoute)
.use(ApiUser) .use(ApiUser)
.use(CredentialRoute)
const app = new Elysia() const app = new Elysia()
.use(Api) .use(Api)
.use(Docs) .use(Docs)
.use(Auth) .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) .get("*", html)
.listen(3000, () => { .listen(3000, () => {
console.log("Server running at http://localhost:3000"); console.log("Server running at http://localhost:3000");

View File

@@ -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 <Container size={"md"} w={"100%"}>
<Stack>
<CredentialCreate />
<CredentialList />
</Stack>
</Container>
}
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 <Card>
<Stack>
<Title>Credential Create</Title>
<TextInput placeholder="name" value={name} onChange={(e) => setName(e.target.value)} />
<TextInput placeholder="apikey" value={apikey} onChange={(e) => setApikey(e.target.value)} />
<Group>
<Button onClick={handleSubmit}>Save</Button>
</Group>
</Stack>
</Card>
}
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 <Card>
<Stack>
{data?.data?.list.map((v, k) => <Stack key={k}>
<Flex justify={"space-between"}>
<Text>{v.name}</Text>
<Group>
<Button onClick={() => handleRm(v.id)}>delete</Button>
</Group>
</Flex>
</Stack>)}
</Stack>
</Card>
}

View File

@@ -21,7 +21,9 @@ import { useLocalStorage } from '@mantine/hooks'
import { import {
IconChevronLeft, IconChevronLeft,
IconChevronRight, IconChevronRight,
IconDashboard IconDashboard,
IconKey,
IconLock
} from '@tabler/icons-react' } from '@tabler/icons-react'
import type { User } from 'generated/prisma' import type { User } from 'generated/prisma'
import { Outlet, useLocation, useNavigate } from 'react-router-dom' import { Outlet, useLocation, useNavigate } from 'react-router-dom'
@@ -170,11 +172,18 @@ function NavigationDashboard() {
/> />
<NavLink <NavLink
active={isActive('/dashboard/apikey')} active={isActive('/dashboard/apikey')}
leftSection={<IconDashboard size={20} />} leftSection={<IconKey size={20} />}
label="Dashboard Overview" label="Dashboard Overview"
description="Quick summary and activity highlights" description="Quick summary and activity highlights"
onClick={() => navigate(clientRoutes['/dashboard/apikey'])} onClick={() => navigate(clientRoutes['/dashboard/apikey'])}
/> />
<NavLink
active={isActive('/dashboard/credential')}
leftSection={<IconLock size={20} />}
label="Dashboard Overview"
description="Quick summary and activity highlights"
onClick={() => navigate(clientRoutes['/dashboard/credential'])}
/>
</Stack> </Stack>
) )
} }

View File

@@ -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<string, any>
}
interface McpManifest {
schema_version: string
name: string
description: string
version?: string
endpoints: Record<string, string>
capabilities: Record<string, any>
contact?: { email?: string }
}
/**
* Convert OpenAPI JSON to MCP manifest format
*/
export 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`)
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<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
}
/**
* 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)

View File

@@ -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

View File

@@ -1,8 +0,0 @@
import Elysia from "elysia";
const Dashboard = new Elysia({
prefix: "/dashboard"
})
.get("/apa", () => "Hello World")
export default Dashboard

View File

@@ -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/<dir>"
}
})
.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/<dir>/<file_name>"
}
})
export default DarmasabaRoute

133
x.ts Normal file
View File

@@ -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<string, any>
}
interface McpManifest {
schema_version: string
name: string
description: string
version?: string
endpoints: Record<string, string>
capabilities: Record<string, any>
contact?: { email?: string }
}
/**
* 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`)
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<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
}
/**
* 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)