feat: add credential routes and mcp manifest
This commit is contained in:
149
.well-known/mcp.json
Normal file
149
.well-known/mcp.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
5
bun.lock
5
bun.lock
@@ -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
72
mcp.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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",
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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");
|
||||||
|
|||||||
89
src/pages/dashboard/credential/credential_page.tsx
Normal file
89
src/pages/dashboard/credential/credential_page.tsx
Normal 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>
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
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
|
||||||
133
x.ts
Normal file
133
x.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
|
||||||
|
*/
|
||||||
|
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)
|
||||||
Reference in New Issue
Block a user