diff --git a/docs/api-reference/openapi.mdx b/docs/api-reference/openapi.mdx new file mode 100644 index 0000000..6dee896 --- /dev/null +++ b/docs/api-reference/openapi.mdx @@ -0,0 +1,250 @@ +--- +title: "OpenAPI Integration" +description: "Generate OpenAPI specifications from MCP tools for seamless integration with OpenWebUI and other systems" +--- + +# OpenAPI Generation for OpenWebUI Integration + +MCPHub now supports generating OpenAPI 3.0.3 specifications from MCP tools, enabling seamless integration with OpenWebUI and other OpenAPI-compatible systems without requiring MCPO as an intermediary proxy. + +## Features + +- ✅ **Automatic OpenAPI Generation**: Converts MCP tools to OpenAPI 3.0.3 specification +- ✅ **OpenWebUI Compatible**: Direct integration without MCPO proxy +- ✅ **Real-time Tool Discovery**: Dynamically includes tools from connected MCP servers +- ✅ **Dual Parameter Support**: Supports both GET (query params) and POST (JSON body) for tool execution +- ✅ **No Authentication Required**: OpenAPI endpoints are public for easy integration +- ✅ **Comprehensive Metadata**: Full OpenAPI specification with proper schemas and documentation + +## API Endpoints + +### OpenAPI Specification + + + +```bash GET /api/openapi.json +curl "http://localhost:3000/api/openapi.json" +``` + +```bash With Parameters +curl "http://localhost:3000/api/openapi.json?title=My MCP API&version=2.0.0" +``` + + + +Generates and returns the complete OpenAPI 3.0.3 specification for all connected MCP tools. + +**Query Parameters:** + + + Custom API title + + + + Custom API description + + + + Custom API version + + + + Custom server URL + + + + Include disabled tools + + + + Comma-separated list of server names to include + + +### Available Servers + + + +```bash GET /api/openapi/servers +curl "http://localhost:3000/api/openapi/servers" +``` + + + +Returns a list of connected MCP server names. + + + +```json Example Response +{ + "success": true, + "data": ["amap", "playwright", "slack"] +} +``` + + + +### Tool Statistics + + + +```bash GET /api/openapi/stats +curl "http://localhost:3000/api/openapi/stats" +``` + + + +Returns statistics about available tools and servers. + + + +```json Example Response +{ + "success": true, + "data": { + "totalServers": 3, + "totalTools": 41, + "serverBreakdown": [ + {"name": "amap", "toolCount": 12, "status": "connected"}, + {"name": "playwright", "toolCount": 21, "status": "connected"}, + {"name": "slack", "toolCount": 8, "status": "connected"} + ] + } +} +``` + + + +### Tool Execution + + + +```bash GET /api/tools/{serverName}/{toolName} +curl "http://localhost:3000/api/tools/amap/amap-maps_weather?city=Beijing" +``` + +```bash POST /api/tools/{serverName}/{toolName} +curl -X POST "http://localhost:3000/api/tools/playwright/playwright-browser_navigate" \ + -H "Content-Type: application/json" \ + -d '{"url": "https://example.com"}' +``` + + + +Execute MCP tools via OpenAPI-compatible endpoints. + +**Path Parameters:** + + + The name of the MCP server + + + + The name of the tool to execute + + +## OpenWebUI Integration + +To integrate MCPHub with OpenWebUI: + + + + Ensure MCPHub is running with your MCP servers configured + + + ```bash + curl http://localhost:3000/api/openapi.json > mcphub-api.json + ``` + + + Import the OpenAPI specification file or point to the URL directly in OpenWebUI + + + +### Configuration Example + +In OpenWebUI, you can add MCPHub as an OpenAPI tool by using: + + + + `http://localhost:3000/api/openapi.json` + + + `http://localhost:3000/api` + + + +## Generated OpenAPI Structure + +The generated OpenAPI specification includes: + +### Tool Conversion Logic + +- **Simple tools** (≤10 primitive parameters) → GET endpoints with query parameters +- **Complex tools** (objects, arrays, or >10 parameters) → POST endpoints with JSON request body +- **All tools** include comprehensive response schemas and error handling + +### Example Generated Operation + +```yaml +/tools/amap/amap-maps_weather: + get: + summary: "根据城市名称或者标准adcode查询指定城市的天气" + operationId: "amap_amap-maps_weather" + tags: ["amap"] + parameters: + - name: city + in: query + required: true + description: "城市名称或者adcode" + schema: + type: string + responses: + '200': + description: "Successful tool execution" + content: + application/json: + schema: + $ref: '#/components/schemas/ToolResponse' +``` + +### Security + +- Bearer authentication is defined but not enforced for tool execution endpoints +- Enables flexible integration with various OpenAPI-compatible systems + +## Benefits over MCPO + + + + No need for intermediate proxy + + + OpenAPI spec updates automatically as MCP servers connect/disconnect + + + Direct tool execution without proxy overhead + + + One less component to manage + + + +## Troubleshooting + + + + Ensure MCP servers are connected. Check `/api/openapi/stats` for server status. + + + + Verify the tool name and parameters match the OpenAPI specification. Check server logs for details. + + + + Ensure MCPHub is accessible from OpenWebUI and the OpenAPI URL is correct. + + + + Check if tools are enabled in your MCP server configuration. Use `includeDisabled=true` to see all tools. + + diff --git a/docs/docs.json b/docs/docs.json index ead9557..f0a4f36 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -83,6 +83,12 @@ "api-reference/smart-routing" ] }, + { + "group": "OpenAPI Endpoints", + "pages": [ + "api-reference/openapi" + ] + }, { "group": "Management Endpoints", "pages": [ @@ -107,6 +113,12 @@ "zh/api-reference/smart-routing" ] }, + { + "group": "OpenAPI 端点", + "pages": [ + "zh/api-reference/openapi" + ] + }, { "group": "管理端点", "pages": [ diff --git a/docs/zh/api-reference/openapi.mdx b/docs/zh/api-reference/openapi.mdx new file mode 100644 index 0000000..ca0a23c --- /dev/null +++ b/docs/zh/api-reference/openapi.mdx @@ -0,0 +1,250 @@ +--- +title: "OpenAPI 集成" +description: "从 MCP 工具生成 OpenAPI 规范,与 OpenWebUI 和其他系统无缝集成" +--- + +# OpenWebUI 集成的 OpenAPI 生成 + +MCPHub 现在支持从 MCP 工具生成 OpenAPI 3.0.3 规范,实现与 OpenWebUI 和其他 OpenAPI 兼容系统的无缝集成,无需 MCPO 作为中间代理。 + +## 功能特性 + +- ✅ **自动 OpenAPI 生成**:将 MCP 工具转换为 OpenAPI 3.0.3 规范 +- ✅ **OpenWebUI 兼容**:无需 MCPO 代理的直接集成 +- ✅ **实时工具发现**:动态包含已连接 MCP 服务器的工具 +- ✅ **双参数支持**:支持 GET(查询参数)和 POST(JSON 正文)进行工具执行 +- ✅ **无需身份验证**:OpenAPI 端点公开,便于集成 +- ✅ **完整元数据**:具有适当模式和文档的完整 OpenAPI 规范 + +## API 端点 + +### OpenAPI 规范 + + + +```bash GET /api/openapi.json +curl "http://localhost:3000/api/openapi.json" +``` + +```bash 带参数 +curl "http://localhost:3000/api/openapi.json?title=我的 MCP API&version=2.0.0" +``` + + + +生成并返回所有已连接 MCP 工具的完整 OpenAPI 3.0.3 规范。 + +**查询参数:** + + + 自定义 API 标题 + + + + 自定义 API 描述 + + + + 自定义 API 版本 + + + + 自定义服务器 URL + + + + 包含禁用的工具 + + + + 要包含的服务器名称列表(逗号分隔) + + +### 可用服务器 + + + +```bash GET /api/openapi/servers +curl "http://localhost:3000/api/openapi/servers" +``` + + + +返回已连接的 MCP 服务器名称列表。 + + + +```json 示例响应 +{ + "success": true, + "data": ["amap", "playwright", "slack"] +} +``` + + + +### 工具统计 + + + +```bash GET /api/openapi/stats +curl "http://localhost:3000/api/openapi/stats" +``` + + + +返回有关可用工具和服务器的统计信息。 + + + +```json 示例响应 +{ + "success": true, + "data": { + "totalServers": 3, + "totalTools": 41, + "serverBreakdown": [ + {"name": "amap", "toolCount": 12, "status": "connected"}, + {"name": "playwright", "toolCount": 21, "status": "connected"}, + {"name": "slack", "toolCount": 8, "status": "connected"} + ] + } +} +``` + + + +### 工具执行 + + + +```bash GET /api/tools/{serverName}/{toolName} +curl "http://localhost:3000/api/tools/amap/amap-maps_weather?city=Beijing" +``` + +```bash POST /api/tools/{serverName}/{toolName} +curl -X POST "http://localhost:3000/api/tools/playwright/playwright-browser_navigate" \ + -H "Content-Type: application/json" \ + -d '{"url": "https://example.com"}' +``` + + + +通过 OpenAPI 兼容端点执行 MCP 工具。 + +**路径参数:** + + + MCP 服务器的名称 + + + + 要执行的工具名称 + + +## OpenWebUI 集成 + +要将 MCPHub 与 OpenWebUI 集成: + + + + 确保 MCPHub 正在运行,并且已配置 MCP 服务器 + + + ```bash + curl http://localhost:3000/api/openapi.json > mcphub-api.json + ``` + + + 在 OpenWebUI 中导入 OpenAPI 规范文件或直接指向 URL + + + +### 配置示例 + +在 OpenWebUI 中,您可以通过以下方式将 MCPHub 添加为 OpenAPI 工具: + + + + `http://localhost:3000/api/openapi.json` + + + `http://localhost:3000/api` + + + +## 生成的 OpenAPI 结构 + +生成的 OpenAPI 规范包括: + +### 工具转换逻辑 + +- **简单工具**(≤10 个原始参数)→ 带查询参数的 GET 端点 +- **复杂工具**(对象、数组或 >10 个参数)→ 带 JSON 请求正文的 POST 端点 +- **所有工具**都包含完整的响应模式和错误处理 + +### 生成操作示例 + +```yaml +/tools/amap/amap-maps_weather: + get: + summary: "根据城市名称或者标准adcode查询指定城市的天气" + operationId: "amap_amap-maps_weather" + tags: ["amap"] + parameters: + - name: city + in: query + required: true + description: "城市名称或者adcode" + schema: + type: string + responses: + '200': + description: "Successful tool execution" + content: + application/json: + schema: + $ref: '#/components/schemas/ToolResponse' +``` + +### 安全性 + +- 定义了 Bearer 身份验证但不对工具执行端点强制执行 +- 支持与各种 OpenAPI 兼容系统的灵活集成 + +## 相比 MCPO 的优势 + + + + 无需中间代理 + + + OpenAPI 规范随着 MCP 服务器连接/断开自动更新 + + + 直接工具执行,无代理开销 + + + 减少一个需要管理的组件 + + + +## 故障排除 + + + + 确保 MCP 服务器已连接。检查 `/api/openapi/stats` 查看服务器状态。 + + + + 验证工具名称和参数是否与 OpenAPI 规范匹配。检查服务器日志以获取详细信息。 + + + + 确保 MCPHub 可从 OpenWebUI 访问,并且 OpenAPI URL 正确。 + + + + 检查您的 MCP 服务器配置中是否启用了工具。使用 `includeDisabled=true` 查看所有工具。 + + diff --git a/package-lock.json b/package-lock.json index 1192c6e..5d1d2ed 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,81 +10,81 @@ "license": "ISC", "dependencies": { "@apidevtools/swagger-parser": "^11.0.1", - "@modelcontextprotocol/sdk": "^1.17.2", + "@modelcontextprotocol/sdk": "^1.17.4", "@types/adm-zip": "^0.5.7", "@types/multer": "^1.4.13", - "@types/pg": "^8.15.2", + "@types/pg": "^8.15.5", "adm-zip": "^0.5.16", - "axios": "^1.10.0", + "axios": "^1.11.0", "bcryptjs": "^3.0.2", - "dotenv": "^16.3.1", + "dotenv": "^16.6.1", "dotenv-expand": "^12.0.2", "express": "^4.21.2", "express-validator": "^7.2.1", "i18next-fs-backend": "^2.6.0", "jsonwebtoken": "^9.0.2", - "multer": "^2.0.1", - "openai": "^4.103.0", + "multer": "^2.0.2", + "openai": "^4.104.0", "openapi-types": "^12.1.3", - "pg": "^8.16.0", + "pg": "^8.16.3", "pgvector": "^0.2.1", "postgres": "^3.4.7", "reflect-metadata": "^0.2.2", - "typeorm": "^0.3.24", + "typeorm": "^0.3.26", "uuid": "^11.1.0" }, "bin": { "mcphub": "bin/cli.js" }, "devDependencies": { - "@radix-ui/react-accordion": "^1.2.3", - "@radix-ui/react-slot": "^1.1.2", + "@radix-ui/react-accordion": "^1.2.12", + "@radix-ui/react-slot": "^1.2.3", "@shadcn/ui": "^0.0.4", - "@swc/core": "^1.13.0", + "@swc/core": "^1.13.5", "@swc/jest": "^0.2.39", "@tailwindcss/line-clamp": "^0.4.4", - "@tailwindcss/postcss": "^4.1.3", - "@tailwindcss/vite": "^4.1.7", + "@tailwindcss/postcss": "^4.1.12", + "@tailwindcss/vite": "^4.1.12", "@types/bcryptjs": "^3.0.0", - "@types/express": "^4.17.21", - "@types/jest": "^29.5.5", - "@types/jsonwebtoken": "^9.0.9", - "@types/node": "^22.15.21", - "@types/react": "^19.0.12", - "@types/react-dom": "^19.0.4", + "@types/express": "^4.17.23", + "@types/jest": "^29.5.14", + "@types/jsonwebtoken": "^9.0.10", + "@types/node": "^22.17.2", + "@types/react": "^19.1.11", + "@types/react-dom": "^19.1.7", "@types/supertest": "^6.0.3", "@types/uuid": "^10.0.0", - "@typescript-eslint/eslint-plugin": "^6.7.4", - "@typescript-eslint/parser": "^6.7.4", - "@vitejs/plugin-react": "^4.4.1", + "@typescript-eslint/eslint-plugin": "^6.21.0", + "@typescript-eslint/parser": "^6.21.0", + "@vitejs/plugin-react": "^4.7.0", "autoprefixer": "^10.4.21", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", - "concurrently": "^9.1.2", - "eslint": "^8.50.0", + "concurrently": "^9.2.0", + "eslint": "^8.57.1", "i18next": "^24.2.3", - "i18next-browser-languagedetector": "^8.0.4", + "i18next-browser-languagedetector": "^8.2.0", "jest": "^29.7.0", - "jest-environment-node": "^30.0.0", + "jest-environment-node": "^30.0.5", "jest-mock-extended": "4.0.0-beta1", "lucide-react": "^0.486.0", - "next": "^15.2.4", - "postcss": "^8.5.3", - "prettier": "^3.0.3", - "react": "^19.1.0", - "react-dom": "^19.1.0", - "react-i18next": "^15.4.1", - "react-router-dom": "^7.6.0", - "supertest": "^7.1.1", - "tailwind-merge": "^3.1.0", + "next": "^15.5.0", + "postcss": "^8.5.6", + "prettier": "^3.6.2", + "react": "^19.1.1", + "react-dom": "^19.1.1", + "react-i18next": "^15.7.2", + "react-router-dom": "^7.8.2", + "supertest": "^7.1.4", + "tailwind-merge": "^3.3.1", "tailwind-scrollbar-hide": "^2.0.0", - "tailwindcss": "^4.0.17", - "ts-jest": "^29.1.1", + "tailwindcss": "^4.1.12", + "ts-jest": "^29.4.1", "ts-node-dev": "^2.0.0", - "tsx": "^4.7.0", - "typescript": "^5.2.2", + "tsx": "^4.20.5", + "typescript": "^5.9.2", "vite": "^6.3.5", - "zod": "^3.24.2" + "zod": "^3.25.76" }, "engines": { "node": "^18.0.0 || >=20.0.0" @@ -727,7 +727,7 @@ "version": "0.8.1", "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@jridgewell/trace-mapping": "0.3.9" @@ -740,7 +740,7 @@ "version": "0.3.9", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "^3.0.3", @@ -2443,7 +2443,7 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=6.0.0" @@ -2453,7 +2453,7 @@ "version": "1.5.5", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { @@ -2468,9 +2468,9 @@ } }, "node_modules/@modelcontextprotocol/sdk": { - "version": "1.17.3", - "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.17.3.tgz", - "integrity": "sha512-JPwUKWSsbzx+DLFznf/QZ32Qa+ptfbUlHhRLrBQBAFu9iI1iYvizM4p+zhhRDceSsPutXp4z+R/HPVphlIiclg==", + "version": "1.17.4", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.17.4.tgz", + "integrity": "sha512-zq24hfuAmmlNZvik0FLI58uE5sriN0WWsQzIlYnzSuKDAHFqJtBFrl/LfB1NLgJT5Y7dEBzaX4yAKqOPrcetaw==", "license": "MIT", "dependencies": { "ajv": "^6.12.6", @@ -3618,15 +3618,15 @@ "license": "MIT" }, "node_modules/@swc/core": { - "version": "1.13.3", - "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.13.3.tgz", - "integrity": "sha512-ZaDETVWnm6FE0fc+c2UE8MHYVS3Fe91o5vkmGfgwGXFbxYvAjKSqxM/j4cRc9T7VZNSJjriXq58XkfCp3Y6f+w==", + "version": "1.13.5", + "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.13.5.tgz", + "integrity": "sha512-WezcBo8a0Dg2rnR82zhwoR6aRNxeTGfK5QCD6TQ+kg3xx/zNT02s/0o+81h/3zhvFSB24NtqEr8FTw88O5W/JQ==", "dev": true, "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { "@swc/counter": "^0.1.3", - "@swc/types": "^0.1.23" + "@swc/types": "^0.1.24" }, "engines": { "node": ">=10" @@ -3636,16 +3636,16 @@ "url": "https://opencollective.com/swc" }, "optionalDependencies": { - "@swc/core-darwin-arm64": "1.13.3", - "@swc/core-darwin-x64": "1.13.3", - "@swc/core-linux-arm-gnueabihf": "1.13.3", - "@swc/core-linux-arm64-gnu": "1.13.3", - "@swc/core-linux-arm64-musl": "1.13.3", - "@swc/core-linux-x64-gnu": "1.13.3", - "@swc/core-linux-x64-musl": "1.13.3", - "@swc/core-win32-arm64-msvc": "1.13.3", - "@swc/core-win32-ia32-msvc": "1.13.3", - "@swc/core-win32-x64-msvc": "1.13.3" + "@swc/core-darwin-arm64": "1.13.5", + "@swc/core-darwin-x64": "1.13.5", + "@swc/core-linux-arm-gnueabihf": "1.13.5", + "@swc/core-linux-arm64-gnu": "1.13.5", + "@swc/core-linux-arm64-musl": "1.13.5", + "@swc/core-linux-x64-gnu": "1.13.5", + "@swc/core-linux-x64-musl": "1.13.5", + "@swc/core-win32-arm64-msvc": "1.13.5", + "@swc/core-win32-ia32-msvc": "1.13.5", + "@swc/core-win32-x64-msvc": "1.13.5" }, "peerDependencies": { "@swc/helpers": ">=0.5.17" @@ -3657,9 +3657,9 @@ } }, "node_modules/@swc/core-darwin-arm64": { - "version": "1.13.3", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.13.3.tgz", - "integrity": "sha512-ux0Ws4pSpBTqbDS9GlVP354MekB1DwYlbxXU3VhnDr4GBcCOimpocx62x7cFJkSpEBF8bmX8+/TTCGKh4PbyXw==", + "version": "1.13.5", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.13.5.tgz", + "integrity": "sha512-lKNv7SujeXvKn16gvQqUQI5DdyY8v7xcoO3k06/FJbHJS90zEwZdQiMNRiqpYw/orU543tPaWgz7cIYWhbopiQ==", "cpu": [ "arm64" ], @@ -3674,9 +3674,9 @@ } }, "node_modules/@swc/core-darwin-x64": { - "version": "1.13.3", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.13.3.tgz", - "integrity": "sha512-p0X6yhxmNUOMZrbeZ3ZNsPige8lSlSe1llllXvpCLkKKxN/k5vZt1sULoq6Nj4eQ7KeHQVm81/+AwKZyf/e0TA==", + "version": "1.13.5", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.13.5.tgz", + "integrity": "sha512-ILd38Fg/w23vHb0yVjlWvQBoE37ZJTdlLHa8LRCFDdX4WKfnVBiblsCU9ar4QTMNdeTBEX9iUF4IrbNWhaF1Ng==", "cpu": [ "x64" ], @@ -3691,9 +3691,9 @@ } }, "node_modules/@swc/core-linux-arm-gnueabihf": { - "version": "1.13.3", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.13.3.tgz", - "integrity": "sha512-OmDoiexL2fVWvQTCtoh0xHMyEkZweQAlh4dRyvl8ugqIPEVARSYtaj55TBMUJIP44mSUOJ5tytjzhn2KFxFcBA==", + "version": "1.13.5", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.13.5.tgz", + "integrity": "sha512-Q6eS3Pt8GLkXxqz9TAw+AUk9HpVJt8Uzm54MvPsqp2yuGmY0/sNaPPNVqctCX9fu/Nu8eaWUen0si6iEiCsazQ==", "cpu": [ "arm" ], @@ -3708,9 +3708,9 @@ } }, "node_modules/@swc/core-linux-arm64-gnu": { - "version": "1.13.3", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.13.3.tgz", - "integrity": "sha512-STfKku3QfnuUj6k3g9ld4vwhtgCGYIFQmsGPPgT9MK/dI3Lwnpe5Gs5t1inoUIoGNP8sIOLlBB4HV4MmBjQuhw==", + "version": "1.13.5", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.13.5.tgz", + "integrity": "sha512-aNDfeN+9af+y+M2MYfxCzCy/VDq7Z5YIbMqRI739o8Ganz6ST+27kjQFd8Y/57JN/hcnUEa9xqdS3XY7WaVtSw==", "cpu": [ "arm64" ], @@ -3725,9 +3725,9 @@ } }, "node_modules/@swc/core-linux-arm64-musl": { - "version": "1.13.3", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.13.3.tgz", - "integrity": "sha512-bc+CXYlFc1t8pv9yZJGus372ldzOVscBl7encUBlU1m/Sig0+NDJLz6cXXRcFyl6ABNOApWeR4Yl7iUWx6C8og==", + "version": "1.13.5", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.13.5.tgz", + "integrity": "sha512-9+ZxFN5GJag4CnYnq6apKTnnezpfJhCumyz0504/JbHLo+Ue+ZtJnf3RhyA9W9TINtLE0bC4hKpWi8ZKoETyOQ==", "cpu": [ "arm64" ], @@ -3742,9 +3742,9 @@ } }, "node_modules/@swc/core-linux-x64-gnu": { - "version": "1.13.3", - "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.13.3.tgz", - "integrity": "sha512-dFXoa0TEhohrKcxn/54YKs1iwNeW6tUkHJgXW33H381SvjKFUV53WR231jh1sWVJETjA3vsAwxKwR23s7UCmUA==", + "version": "1.13.5", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.13.5.tgz", + "integrity": "sha512-WD530qvHrki8Ywt/PloKUjaRKgstQqNGvmZl54g06kA+hqtSE2FTG9gngXr3UJxYu/cNAjJYiBifm7+w4nbHbA==", "cpu": [ "x64" ], @@ -3759,9 +3759,9 @@ } }, "node_modules/@swc/core-linux-x64-musl": { - "version": "1.13.3", - "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.13.3.tgz", - "integrity": "sha512-ieyjisLB+ldexiE/yD8uomaZuZIbTc8tjquYln9Quh5ykOBY7LpJJYBWvWtm1g3pHv6AXlBI8Jay7Fffb6aLfA==", + "version": "1.13.5", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.13.5.tgz", + "integrity": "sha512-Luj8y4OFYx4DHNQTWjdIuKTq2f5k6uSXICqx+FSabnXptaOBAbJHNbHT/06JZh6NRUouaf0mYXN0mcsqvkhd7Q==", "cpu": [ "x64" ], @@ -3776,9 +3776,9 @@ } }, "node_modules/@swc/core-win32-arm64-msvc": { - "version": "1.13.3", - "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.13.3.tgz", - "integrity": "sha512-elTQpnaX5vESSbhCEgcwXjpMsnUbqqHfEpB7ewpkAsLzKEXZaK67ihSRYAuAx6ewRQTo7DS5iTT6X5aQD3MzMw==", + "version": "1.13.5", + "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.13.5.tgz", + "integrity": "sha512-cZ6UpumhF9SDJvv4DA2fo9WIzlNFuKSkZpZmPG1c+4PFSEMy5DFOjBSllCvnqihCabzXzpn6ykCwBmHpy31vQw==", "cpu": [ "arm64" ], @@ -3793,9 +3793,9 @@ } }, "node_modules/@swc/core-win32-ia32-msvc": { - "version": "1.13.3", - "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.13.3.tgz", - "integrity": "sha512-nvehQVEOdI1BleJpuUgPLrclJ0TzbEMc+MarXDmmiRFwEUGqj+pnfkTSb7RZyS1puU74IXdK/YhTirHurtbI9w==", + "version": "1.13.5", + "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.13.5.tgz", + "integrity": "sha512-C5Yi/xIikrFUzZcyGj9L3RpKljFvKiDMtyDzPKzlsDrKIw2EYY+bF88gB6oGY5RGmv4DAX8dbnpRAqgFD0FMEw==", "cpu": [ "ia32" ], @@ -3810,9 +3810,9 @@ } }, "node_modules/@swc/core-win32-x64-msvc": { - "version": "1.13.3", - "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.13.3.tgz", - "integrity": "sha512-A+JSKGkRbPLVV2Kwx8TaDAV0yXIXm/gc8m98hSkVDGlPBBmydgzNdWy3X7HTUBM7IDk7YlWE7w2+RUGjdgpTmg==", + "version": "1.13.5", + "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.13.5.tgz", + "integrity": "sha512-YrKdMVxbYmlfybCSbRtrilc6UA8GF5aPmGKBdPvjrarvsmf4i7ZHGCEnLtfOMd3Lwbs2WUZq3WdMbozYeLU93Q==", "cpu": [ "x64" ], @@ -4176,28 +4176,28 @@ "version": "1.0.11", "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/@tsconfig/node12": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/@tsconfig/node14": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/@tsconfig/node16": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/@types/adm-zip": { @@ -4465,9 +4465,9 @@ "license": "MIT" }, "node_modules/@types/react": { - "version": "19.1.10", - "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.10.tgz", - "integrity": "sha512-EhBeSYX0Y6ye8pNebpKrwFJq7BoQ8J5SO6NlvNwwHjSj6adXJViPQrKlsyPw7hLBLvckEMO1yxeGdR82YBBlDg==", + "version": "19.1.11", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.11.tgz", + "integrity": "sha512-lr3jdBw/BGj49Eps7EvqlUaoeA0xpj3pc0RoJkHpYaCHkVK7i28dKyImLQb3JVlqs3aYSXf7qYuWOW/fgZnTXQ==", "dev": true, "license": "MIT", "dependencies": { @@ -4836,7 +4836,7 @@ "version": "8.15.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", - "devOptional": true, + "dev": true, "license": "MIT", "bin": { "acorn": "bin/acorn" @@ -4859,7 +4859,7 @@ "version": "8.3.4", "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "acorn": "^8.11.0" @@ -5014,7 +5014,7 @@ "version": "4.1.3", "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/argparse": { @@ -6013,7 +6013,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/cross-spawn": { @@ -6194,7 +6194,7 @@ "version": "4.0.2", "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", - "devOptional": true, + "dev": true, "license": "BSD-3-Clause", "engines": { "node": ">=0.3.1" @@ -9633,7 +9633,7 @@ "version": "1.3.6", "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", - "devOptional": true, + "dev": true, "license": "ISC" }, "node_modules/makeerror": { @@ -10956,9 +10956,9 @@ } }, "node_modules/react-i18next": { - "version": "15.6.1", - "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-15.6.1.tgz", - "integrity": "sha512-uGrzSsOUUe2sDBG/+FJq2J1MM+Y4368/QW8OLEKSFvnDflHBbZhSd1u3UkW0Z06rMhZmnB/AQrhCpYfE5/5XNg==", + "version": "15.7.2", + "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-15.7.2.tgz", + "integrity": "sha512-xJxq7ibnhUlMvd82lNC4te1GxGUMoM1A05KKyqoqsBXVZtEvZg/fz/fnVzdlY/hhQ3SpP/79qCocZOtICGhd3g==", "dev": true, "license": "MIT", "dependencies": { @@ -10966,7 +10966,7 @@ "html-parse-stringify": "^3.0.1" }, "peerDependencies": { - "i18next": ">= 23.2.3", + "i18next": ">= 25.4.1", "react": ">= 16.8.0", "typescript": "^5" }, @@ -11000,9 +11000,9 @@ } }, "node_modules/react-router": { - "version": "7.8.1", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.8.1.tgz", - "integrity": "sha512-5cy/M8DHcG51/KUIka1nfZ2QeylS4PJRs6TT8I4PF5axVsI5JUxp0hC0NZ/AEEj8Vw7xsEoD7L/6FY+zoYaOGA==", + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.8.2.tgz", + "integrity": "sha512-7M2fR1JbIZ/jFWqelpvSZx+7vd7UlBTfdZqf6OSdF9g6+sfdqJDAWcak6ervbHph200ePlu+7G8LdoiC3ReyAQ==", "dev": true, "license": "MIT", "dependencies": { @@ -11023,13 +11023,13 @@ } }, "node_modules/react-router-dom": { - "version": "7.8.1", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.8.1.tgz", - "integrity": "sha512-NkgBCF3sVgCiAWIlSt89GR2PLaksMpoo3HDCorpRfnCEfdtRPLiuTf+CNXvqZMI5SJLZCLpVCvcZrTdtGW64xQ==", + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.8.2.tgz", + "integrity": "sha512-Z4VM5mKDipal2jQ385H6UBhiiEDlnJPx6jyWsTYoZQdl5TrjxEV2a9yl3Fi60NBJxYzOTGTTHXPi0pdizvTwow==", "dev": true, "license": "MIT", "dependencies": { - "react-router": "7.8.1" + "react-router": "7.8.2" }, "engines": { "node": ">=20.0.0" @@ -12358,7 +12358,7 @@ "version": "10.9.2", "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@cspotcode/source-map-support": "^0.8.0", @@ -12500,9 +12500,9 @@ "license": "0BSD" }, "node_modules/tsx": { - "version": "4.20.4", - "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.20.4.tgz", - "integrity": "sha512-yyxBKfORQ7LuRt/BQKBXrpcq59ZvSW0XxwfjAt3w2/8PmdxaFzijtMhTawprSHhpzeM5BgU2hXHG3lklIERZXg==", + "version": "4.20.5", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.20.5.tgz", + "integrity": "sha512-+wKjMNU9w/EaQayHXb7WA7ZaHY6hN8WgfvHNQ3t1PnU91/7O8TcTnIhCDYTZwnt8JsO9IBqZ30Ln1r7pPF52Aw==", "dev": true, "license": "MIT", "dependencies": { @@ -12729,7 +12729,7 @@ "version": "5.9.2", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz", "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==", - "devOptional": true, + "dev": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", @@ -12850,7 +12850,7 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/v8-to-istanbul": { @@ -13212,7 +13212,7 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=6" diff --git a/package.json b/package.json index 7010272..67ac0e2 100644 --- a/package.json +++ b/package.json @@ -53,6 +53,7 @@ "adm-zip": "^0.5.16", "axios": "^1.11.0", "bcryptjs": "^3.0.2", + "cors": "^2.8.5", "dotenv": "^16.6.1", "dotenv-expand": "^12.0.2", "express": "^4.21.2", @@ -79,6 +80,7 @@ "@tailwindcss/postcss": "^4.1.12", "@tailwindcss/vite": "^4.1.12", "@types/bcryptjs": "^3.0.0", + "@types/cors": "^2.8.19", "@types/express": "^4.17.23", "@types/jest": "^29.5.14", "@types/jsonwebtoken": "^9.0.10", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 06850a6..787935f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -36,6 +36,9 @@ importers: bcryptjs: specifier: ^3.0.2 version: 3.0.2 + cors: + specifier: ^2.8.5 + version: 2.8.5 dotenv: specifier: ^16.6.1 version: 16.6.1 @@ -109,6 +112,9 @@ importers: '@types/bcryptjs': specifier: ^3.0.0 version: 3.0.0 + '@types/cors': + specifier: ^2.8.19 + version: 2.8.19 '@types/express': specifier: ^4.17.23 version: 4.17.23 @@ -1444,6 +1450,9 @@ packages: '@types/cookiejar@2.1.5': resolution: {integrity: sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q==} + '@types/cors@2.8.19': + resolution: {integrity: sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==} + '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} @@ -5411,6 +5420,10 @@ snapshots: '@types/cookiejar@2.1.5': {} + '@types/cors@2.8.19': + dependencies: + '@types/node': 22.17.2 + '@types/estree@1.0.8': {} '@types/express-serve-static-core@4.19.6': diff --git a/src/controllers/openApiController.ts b/src/controllers/openApiController.ts new file mode 100644 index 0000000..b5bd6c7 --- /dev/null +++ b/src/controllers/openApiController.ts @@ -0,0 +1,134 @@ +import { Request, Response } from 'express'; +import { + generateOpenAPISpec, + getAvailableServers, + getToolStats, + OpenAPIGenerationOptions +} from '../services/openApiGeneratorService.js'; + +/** + * Controller for OpenAPI generation endpoints + * Provides OpenAPI specifications for MCP tools to enable OpenWebUI integration + */ + +/** + * Generate and return OpenAPI specification + * GET /api/openapi.json + */ +export const getOpenAPISpec = (req: Request, res: Response): void => { + try { + const options: OpenAPIGenerationOptions = { + title: req.query.title as string, + description: req.query.description as string, + version: req.query.version as string, + serverUrl: req.query.serverUrl as string, + includeDisabledTools: req.query.includeDisabled === 'true', + groupFilter: req.query.group as string, + serverFilter: req.query.servers ? (req.query.servers as string).split(',') : undefined + }; + + const openApiSpec = generateOpenAPISpec(options); + + res.setHeader('Content-Type', 'application/json'); + res.setHeader('Access-Control-Allow-Origin', '*'); + res.setHeader('Access-Control-Allow-Methods', 'GET, OPTIONS'); + res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization'); + + res.json(openApiSpec); + } catch (error) { + console.error('Error generating OpenAPI specification:', error); + res.status(500).json({ + error: 'Failed to generate OpenAPI specification', + message: error instanceof Error ? error.message : 'Unknown error' + }); + } +}; + +/** + * Get available servers for filtering + * GET /api/openapi/servers + */ +export const getOpenAPIServers = (req: Request, res: Response): void => { + try { + const servers = getAvailableServers(); + res.json({ + success: true, + data: servers + }); + } catch (error) { + console.error('Error getting available servers:', error); + res.status(500).json({ + success: false, + error: 'Failed to get available servers', + message: error instanceof Error ? error.message : 'Unknown error' + }); + } +}; + +/** + * Get tool statistics + * GET /api/openapi/stats + */ +export const getOpenAPIStats = (req: Request, res: Response): void => { + try { + const stats = getToolStats(); + res.json({ + success: true, + data: stats + }); + } catch (error) { + console.error('Error getting tool statistics:', error); + res.status(500).json({ + success: false, + error: 'Failed to get tool statistics', + message: error instanceof Error ? error.message : 'Unknown error' + }); + } +}; + +/** + * Execute tool via OpenAPI-compatible endpoint + * This allows OpenWebUI to call MCP tools directly + * POST /api/tools/:serverName/:toolName + * GET /api/tools/:serverName/:toolName (for simple tools) + */ +export const executeToolViaOpenAPI = async (req: Request, res: Response): Promise => { + try { + const { serverName, toolName } = req.params; + + // Import handleCallToolRequest function + const { handleCallToolRequest } = await import('../services/mcpService.js'); + + // Prepare arguments from query params (GET) or body (POST) + const args = req.method === 'GET' + ? req.query + : req.body || {}; + + // Create a mock request structure that matches what handleCallToolRequest expects + const mockRequest = { + params: { + name: toolName, // Just use the tool name without server prefix as it gets added by handleCallToolRequest + arguments: args, + }, + }; + + const extra = { + sessionId: req.headers['x-session-id'] as string || 'openapi-session', + server: serverName, + }; + + console.log(`OpenAPI tool execution: ${serverName}/${toolName} with args:`, args); + + const result = await handleCallToolRequest(mockRequest, extra); + + // Return the result in OpenAPI format (matching MCP tool response structure) + res.json(result); + + } catch (error) { + console.error('Error executing tool via OpenAPI:', error); + res.status(500).json({ + error: 'Failed to execute tool', + message: error instanceof Error ? error.message : 'Unknown error' + }); + } +}; \ No newline at end of file diff --git a/src/routes/index.ts b/src/routes/index.ts index d07de2f..af48e4c 100644 --- a/src/routes/index.ts +++ b/src/routes/index.ts @@ -63,6 +63,12 @@ import { callTool } from '../controllers/toolController.js'; import { getPrompt } from '../controllers/promptController.js'; import { uploadDxtFile, uploadMiddleware } from '../controllers/dxtController.js'; import { healthCheck } from '../controllers/healthController.js'; +import { + getOpenAPISpec, + getOpenAPIServers, + getOpenAPIStats, + executeToolViaOpenAPI, +} from '../controllers/openApiController.js'; import { auth } from '../middlewares/auth.js'; const router = express.Router(); @@ -180,6 +186,15 @@ export const initRoutes = (app: express.Application): void => { // Public configuration endpoint (no auth required to check skipAuth setting) app.get(`${config.basePath}/public-config`, getPublicConfig); + // OpenAPI generation endpoints (no auth required for OpenWebUI integration) + app.get(`${config.basePath}/api/openapi.json`, getOpenAPISpec); + app.get(`${config.basePath}/api/openapi/servers`, getOpenAPIServers); + app.get(`${config.basePath}/api/openapi/stats`, getOpenAPIStats); + + // OpenAPI-compatible tool execution endpoints (no auth required for OpenWebUI integration) + app.get(`${config.basePath}/api/tools/:serverName/:toolName`, executeToolViaOpenAPI); + app.post(`${config.basePath}/api/tools/:serverName/:toolName`, executeToolViaOpenAPI); + app.use(`${config.basePath}/api`, router); }; diff --git a/src/server.ts b/src/server.ts index 20c2bf9..04a21ea 100644 --- a/src/server.ts +++ b/src/server.ts @@ -1,4 +1,5 @@ import express from 'express'; +import cors from 'cors'; import config from './config/index.js'; import path from 'path'; import fs from 'fs'; @@ -26,6 +27,7 @@ export class AppServer { constructor() { this.app = express(); + this.app.use(cors()); this.port = config.port; this.basePath = config.basePath; } diff --git a/src/services/openApiGeneratorService.ts b/src/services/openApiGeneratorService.ts new file mode 100644 index 0000000..e51d9fb --- /dev/null +++ b/src/services/openApiGeneratorService.ts @@ -0,0 +1,316 @@ +import { OpenAPIV3 } from 'openapi-types'; +import { Tool } from '../types/index.js'; +import { getServersInfo } from './mcpService.js'; +import config from '../config/index.js'; +import { loadSettings } from '../config/index.js'; + +/** + * Service for generating OpenAPI 3.x specifications from MCP tools + * This enables integration with OpenWebUI and other OpenAPI-compatible systems + */ + +export interface OpenAPIGenerationOptions { + title?: string; + description?: string; + version?: string; + serverUrl?: string; + includeDisabledTools?: boolean; + groupFilter?: string; + serverFilter?: string[]; +} + +/** + * Convert MCP tool input schema to OpenAPI parameter or request body schema + */ +function convertToolSchemaToOpenAPI(tool: Tool): { + parameters?: OpenAPIV3.ParameterObject[]; + requestBody?: OpenAPIV3.RequestBodyObject; +} { + const schema = tool.inputSchema as any; + + if (!schema || typeof schema !== 'object') { + return {}; + } + + // If schema has properties, convert them to parameters or request body + if (schema.properties && typeof schema.properties === 'object') { + const properties = schema.properties; + const required = Array.isArray(schema.required) ? schema.required : []; + + // For simple tools with only primitive parameters, use query parameters + const hasComplexTypes = Object.values(properties).some( + (prop: any) => + prop.type === 'object' || + prop.type === 'array' || + (prop.type === 'string' && prop.enum && prop.enum.length > 10), + ); + + if (!hasComplexTypes && Object.keys(properties).length <= 10) { + // Use query parameters for simple tools + const parameters: OpenAPIV3.ParameterObject[] = Object.entries(properties).map( + ([name, prop]: [string, any]) => ({ + name, + in: 'query', + required: required.includes(name), + description: prop.description || `Parameter ${name}`, + schema: { + type: prop.type || 'string', + ...(prop.enum && { enum: prop.enum }), + ...(prop.default !== undefined && { default: prop.default }), + ...(prop.format && { format: prop.format }), + }, + }), + ); + + return { parameters }; + } else { + // Use request body for complex tools + const requestBody: OpenAPIV3.RequestBodyObject = { + required: required.length > 0, + content: { + 'application/json': { + schema: { + type: 'object', + properties, + ...(required.length > 0 && { required }), + }, + }, + }, + }; + + return { requestBody }; + } + } + + return {}; +} + +/** + * Generate OpenAPI operation from MCP tool + */ +function generateOperationFromTool(tool: Tool, serverName: string): OpenAPIV3.OperationObject { + const { parameters, requestBody } = convertToolSchemaToOpenAPI(tool); + const operation: OpenAPIV3.OperationObject = { + summary: tool.description || `Execute ${tool.name} tool`, + description: tool.description || `Execute the ${tool.name} tool from ${serverName} server`, + operationId: `${serverName}_${tool.name}`, + tags: [serverName], + ...(parameters && parameters.length > 0 && { parameters }), + ...(requestBody && { requestBody }), + responses: { + '200': { + description: 'Successful tool execution', + content: { + 'application/json': { + schema: { + type: 'object', + properties: { + content: { + type: 'array', + items: { + type: 'object', + properties: { + type: { type: 'string' }, + text: { type: 'string' }, + }, + }, + }, + isError: { type: 'boolean' }, + }, + }, + }, + }, + }, + '400': { + description: 'Bad request - invalid parameters', + content: { + 'application/json': { + schema: { + type: 'object', + properties: { + error: { type: 'string' }, + message: { type: 'string' }, + }, + }, + }, + }, + }, + '500': { + description: 'Internal server error', + content: { + 'application/json': { + schema: { + type: 'object', + properties: { + error: { type: 'string' }, + message: { type: 'string' }, + }, + }, + }, + }, + }, + }, + }; + + return operation; +} + +/** + * Generate OpenAPI specification from MCP tools + */ +export function generateOpenAPISpec(options: OpenAPIGenerationOptions = {}): OpenAPIV3.Document { + const serverInfos = getServersInfo(); + + // Filter servers based on options + const filteredServers = serverInfos.filter( + (server) => + server.status === 'connected' && + (!options.serverFilter || options.serverFilter.includes(server.name)), + ); + + // Collect all tools from filtered servers + const allTools: Array<{ tool: Tool; serverName: string }> = []; + + for (const serverInfo of filteredServers) { + const tools = options.includeDisabledTools + ? serverInfo.tools + : serverInfo.tools.filter((tool) => tool.enabled !== false); + + for (const tool of tools) { + allTools.push({ tool, serverName: serverInfo.name }); + } + } + + // Generate paths from tools + const paths: OpenAPIV3.PathsObject = {}; + + for (const { tool, serverName } of allTools) { + const operation = generateOperationFromTool(tool, serverName); + const { requestBody } = convertToolSchemaToOpenAPI(tool); + + // Create path for the tool + const pathName = `/tools/${serverName}/${tool.name}`; + const method = requestBody ? 'post' : 'get'; + + if (!paths[pathName]) { + paths[pathName] = {}; + } + + paths[pathName][method] = operation; + } + + const settings = loadSettings(); + // Get server URL + const baseUrl = + options.serverUrl || + settings.systemConfig?.install?.baseUrl || + `http://localhost:${config.port}`; + const serverUrl = `${baseUrl}${config.basePath}/api`; + + // Generate OpenAPI document + const openApiDoc: OpenAPIV3.Document = { + openapi: '3.0.3', + info: { + title: options.title || 'MCPHub API', + description: + options.description || + 'OpenAPI specification for MCP tools managed by MCPHub. This enables integration with OpenWebUI and other OpenAPI-compatible systems.', + version: options.version || '1.0.0', + contact: { + name: 'MCPHub', + url: 'https://github.com/samanhappy/mcphub', + }, + license: { + name: 'ISC', + url: 'https://github.com/samanhappy/mcphub/blob/main/LICENSE', + }, + }, + servers: [ + { + url: serverUrl, + description: 'MCPHub API Server', + }, + ], + paths, + components: { + schemas: { + ToolResponse: { + type: 'object', + properties: { + content: { + type: 'array', + items: { + type: 'object', + properties: { + type: { type: 'string' }, + text: { type: 'string' }, + }, + }, + }, + isError: { type: 'boolean' }, + }, + }, + ErrorResponse: { + type: 'object', + properties: { + error: { type: 'string' }, + message: { type: 'string' }, + }, + }, + }, + securitySchemes: { + bearerAuth: { + type: 'http', + scheme: 'bearer', + bearerFormat: 'JWT', + }, + }, + }, + security: [ + { + bearerAuth: [], + }, + ], + tags: filteredServers.map((server) => ({ + name: server.name, + description: `Tools from ${server.name} server`, + })), + }; + + return openApiDoc; +} + +/** + * Get available server names for filtering + */ +export function getAvailableServers(): string[] { + const serverInfos = getServersInfo(); + return serverInfos.filter((server) => server.status === 'connected').map((server) => server.name); +} + +/** + * Get statistics about available tools + */ +export function getToolStats(): { + totalServers: number; + totalTools: number; + serverBreakdown: Array<{ name: string; toolCount: number; status: string }>; +} { + const serverInfos = getServersInfo(); + + const serverBreakdown = serverInfos.map((server) => ({ + name: server.name, + toolCount: server.tools.length, + status: server.status, + })); + + const totalTools = serverInfos + .filter((server) => server.status === 'connected') + .reduce((sum, server) => sum + server.tools.length, 0); + + return { + totalServers: serverInfos.filter((server) => server.status === 'connected').length, + totalTools, + serverBreakdown, + }; +} diff --git a/tests/services/openApiGeneratorService.test.ts b/tests/services/openApiGeneratorService.test.ts new file mode 100644 index 0000000..b10bfd1 --- /dev/null +++ b/tests/services/openApiGeneratorService.test.ts @@ -0,0 +1,69 @@ +import { generateOpenAPISpec, getToolStats } from '../../src/services/openApiGeneratorService'; + +describe('OpenAPI Generator Service', () => { + describe('generateOpenAPISpec', () => { + it('should generate a valid OpenAPI specification', () => { + const spec = generateOpenAPISpec(); + + // Check basic structure + expect(spec).toHaveProperty('openapi'); + expect(spec).toHaveProperty('info'); + expect(spec).toHaveProperty('servers'); + expect(spec).toHaveProperty('paths'); + expect(spec).toHaveProperty('components'); + + // Check OpenAPI version + expect(spec.openapi).toBe('3.0.3'); + + // Check info section + expect(spec.info).toHaveProperty('title'); + expect(spec.info).toHaveProperty('description'); + expect(spec.info).toHaveProperty('version'); + + // Check components + expect(spec.components).toHaveProperty('schemas'); + expect(spec.components).toHaveProperty('securitySchemes'); + + // Check security schemes + expect(spec.components?.securitySchemes).toHaveProperty('bearerAuth'); + }); + + it('should generate spec with custom options', () => { + const options = { + title: 'Custom API', + description: 'Custom description', + version: '2.0.0', + serverUrl: 'https://custom.example.com' + }; + + const spec = generateOpenAPISpec(options); + + expect(spec.info.title).toBe('Custom API'); + expect(spec.info.description).toBe('Custom description'); + expect(spec.info.version).toBe('2.0.0'); + expect(spec.servers[0].url).toContain('https://custom.example.com'); + }); + + it('should handle empty server list gracefully', () => { + const spec = generateOpenAPISpec(); + + // Should not throw and should have valid structure + expect(spec).toHaveProperty('paths'); + expect(typeof spec.paths).toBe('object'); + }); + }); + + describe('getToolStats', () => { + it('should return valid tool statistics', () => { + const stats = getToolStats(); + + expect(stats).toHaveProperty('totalServers'); + expect(stats).toHaveProperty('totalTools'); + expect(stats).toHaveProperty('serverBreakdown'); + + expect(typeof stats.totalServers).toBe('number'); + expect(typeof stats.totalTools).toBe('number'); + expect(Array.isArray(stats.serverBreakdown)).toBe(true); + }); + }); +}); \ No newline at end of file