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

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