feat: add credential routes and mcp manifest
This commit is contained in:
133
src/server/lib/mcp-converter.ts
Normal file
133
src/server/lib/mcp-converter.ts
Normal 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)
|
||||
46
src/server/routes/credential_route.ts
Normal file
46
src/server/routes/credential_route.ts
Normal 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
|
||||
@@ -1,8 +0,0 @@
|
||||
import Elysia from "elysia";
|
||||
|
||||
const Dashboard = new Elysia({
|
||||
prefix: "/dashboard"
|
||||
})
|
||||
.get("/apa", () => "Hello World")
|
||||
|
||||
export default Dashboard
|
||||
131
src/server/routes/darmasaba_route.ts
Normal file
131
src/server/routes/darmasaba_route.ts
Normal 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
|
||||
Reference in New Issue
Block a user