Compare commits

..

120 Commits

Author SHA1 Message Date
samanhappy
a55405b974 feat: add cluster routing support 2025-10-26 21:41:03 +08:00
Copilot
ff797b4ab9 Add group-scoped smart routing via $smart/{group} pattern (#388)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: samanhappy <2755122+samanhappy@users.noreply.github.com>
2025-10-26 20:51:55 +08:00
samanhappy
9105507722 Refactor: Clean up code formatting and improve readability across multiple files (#387) 2025-10-26 19:27:30 +08:00
Copilot
f79028ed64 Expand environment variables throughout mcp_settings.json configuration (#384)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: samanhappy <2755122+samanhappy@users.noreply.github.com>
2025-10-26 19:25:53 +08:00
Copilot
5ca5e2ad47 Add password security: default credential warning and strength validation (#386)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: samanhappy <2755122+samanhappy@users.noreply.github.com>
2025-10-26 19:22:51 +08:00
Copilot
2f7726b008 Add JSON import for MCP servers (#385)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: samanhappy <2755122+samanhappy@users.noreply.github.com>
Co-authored-by: samanhappy <samanhappy@gmail.com>
2025-10-26 19:13:06 +08:00
Copilot
26b26a5fb1 Add OAuth support for upstream MCP servers (#381)
Co-authored-by: samanhappy <samanhappy@gmail.com>
2025-10-26 16:09:34 +08:00
Copilot
7dbd6c386e Fix: Environment variable expansion in headers for HTTP-based MCP transports (#380)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: samanhappy <2755122+samanhappy@users.noreply.github.com>
2025-10-23 19:18:50 +08:00
Copilot
c1fee91142 Fix Dependabot alert #18: Remove outdated package-lock.json causing axios vulnerability false positive (#379)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: samanhappy <2755122+samanhappy@users.noreply.github.com>
2025-10-22 15:16:24 +08:00
samanhappy
1130f6833e fix: use reconnect mechanism for sse tool calling error (#378) 2025-10-22 12:05:21 +08:00
dependabot[bot]
c3f1de8f5b chore(deps-dev): bump vite from 6.3.6 to 6.4.1 (#376)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-22 12:04:16 +08:00
samanhappy
86367a4875 feat: integrate offcial mcp server registry (#374) 2025-10-19 21:15:25 +08:00
samanhappy
bd4c546bba fix settings data export & parsing error (#373) 2025-10-16 13:08:28 +08:00
Copilot
3e9e5cc3c9 feat: Auto-start Docker daemon when installed in container (#370)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: samanhappy <2755122+samanhappy@users.noreply.github.com>
Co-authored-by: samanhappy <samanhappy@gmail.com>
2025-10-13 22:38:13 +08:00
samanhappy
16a92096b3 feat: Enhance package root detection and version retrieval using ESM-compatible methods (#371) 2025-10-13 22:36:29 +08:00
Copilot
4d736c543d feat: Add MCP settings export and copy functionality (#367)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: samanhappy <2755122+samanhappy@users.noreply.github.com>
Co-authored-by: samanhappy <samanhappy@gmail.com>
2025-10-13 19:39:01 +08:00
samanhappy
f53c4a0e3b fix: assign server name from key in getMarketServers function (#369) 2025-10-13 18:19:21 +08:00
Copilot
d4bdb099d0 Add Docker CLI support to Docker image with INSTALL_EXT build argument (#366)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: samanhappy <2755122+samanhappy@users.noreply.github.com>
2025-10-12 16:51:02 +08:00
samanhappy
435227cbd4 fix: improve error handling and directory creation for settings path (#364) 2025-10-12 15:30:40 +08:00
Copilot
6a59becd8d Fix Windows startup error: Convert paths to file:// URLs for ESM dynamic imports (#363)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: samanhappy <2755122+samanhappy@users.noreply.github.com>
Co-authored-by: samanhappy <samanhappy@gmail.com>
2025-10-12 11:31:44 +08:00
samanhappy
91698a50e3 fix: use specified environment setting path when available (#359) 2025-10-11 23:44:23 +08:00
samanhappy
a5d5045832 fix: add groups handling in mergeSettings method (#362) 2025-10-11 23:29:59 +08:00
samanhappy
198ea85225 feat: implement user management features with add, edit, and delete functionality 2025-10-02 15:11:08 +08:00
dependabot[bot]
6b39916909 chore(deps): bump typeorm from 0.3.26 to 0.3.27 (#356)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-02 14:47:55 +08:00
dependabot[bot]
9e8db370ff chore(deps-dev): bump @tailwindcss/postcss from 4.1.12 to 4.1.13 (#358)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-02 14:47:09 +08:00
dependabot[bot]
5d8bc44a73 chore(deps-dev): bump @types/node from 22.17.2 to 24.6.1 (#357)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-02 14:44:13 +08:00
dependabot[bot]
021901dbda chore(deps-dev): bump jest and @types/jest (#354)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-02 14:40:42 +08:00
WuWen
f6934a32dc feat: add configurable name separator for tools and prompts (#353) 2025-10-02 14:40:01 +08:00
samanhappy
7685b9bca8 feat: enhance visual hierarchy on LoginPage by increasing slogan size and spacing (#347) 2025-09-20 17:23:54 +08:00
samanhappy
c2dd91606f chore(deps): update @modelcontextprotocol/sdk to 1.18.1 and axios to 1.12.2 (#346) 2025-09-20 17:16:04 +08:00
samanhappy
66b6053f7f feat: add passthrough headers support for OpenAPI client and MCP protocol (#345) 2025-09-20 17:12:20 +08:00
dependabot[bot]
ba50a78879 chore(deps): bump axios from 1.11.0 to 1.12.0 (#342)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-14 16:14:13 +08:00
comeback01
a856404963 docs: add French translation for README (#339)
Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
2025-09-12 17:56:00 +08:00
comeback01
9a65532a50 feat: add french localization (#337)
Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
2025-09-12 14:27:05 +08:00
dependabot[bot]
c5aa97de50 chore(deps-dev): bump vite from 6.3.5 to 6.3.6 (#334)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-10 12:42:00 +08:00
dependabot[bot]
271c9fe2c3 chore(deps-dev): bump i18next from 24.2.3 to 25.4.2 (#329)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-05 08:51:54 +08:00
dependabot[bot]
d59961c4d4 chore(deps-dev): bump next from 15.5.0 to 15.5.2 (#328)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-05 08:51:23 +08:00
dependabot[bot]
d0ec80303a chore(deps-dev): bump jest-mock-extended from 4.0.0-beta1 to 4.0.0 (#330)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-04 22:49:30 +08:00
dependabot[bot]
69e92b5aa8 chore(deps): bump @apidevtools/swagger-parser from 11.0.1 to 12.0.0 (#331)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-04 22:48:29 +08:00
dependabot[bot]
5acae64b29 chore(deps-dev): bump @typescript-eslint/eslint-plugin from 6.21.0 to 7.0.0 (#327)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-04 22:47:30 +08:00
samanhappy
a5fc4a429d fix: ensure settings cache is cleared after saving and clearing cache (#326) 2025-09-01 10:15:50 +08:00
Junsen Huang
ce15330016 fix(server-context): fix server edite error (#325) 2025-08-30 17:37:30 +08:00
samanhappy
621bc36560 feat: Add OpenAPI-compatible tool execution endpoints for named APIs (#316) 2025-08-30 16:00:15 +08:00
Junsen Huang
c398223824 feat: refactor server data management to use global context (#319) 2025-08-28 22:15:16 +08:00
Copilot
5dd3e7978e Generate comprehensive GitHub Copilot instructions for MCPHub development (#314)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: samanhappy <2755122+samanhappy@users.noreply.github.com>
2025-08-27 21:58:06 +08:00
samanhappy
f577351f04 fix: set current working directory for StdioClientTransport to homedir (#311) 2025-08-27 19:23:00 +08:00
Copilot
62de87b1a4 Add granular OpenAPI endpoints for server-level and group-level tool access (#309)
Co-authored-by: samanhappy <samanhappy@gmail.com>
2025-08-27 17:25:32 +08:00
samanhappy
bbd6c891c9 feat(dao): Implement comprehensive DAO layer (#308)
Co-authored-by: samanhappy@qq.com <my6051199>
2025-08-27 15:21:30 +08:00
Copilot
f9019545c3 Fix integer parameter conversion in OpenAPI endpoints (#306)
Co-authored-by: samanhappy <samanhappy@gmail.com>
2025-08-27 11:04:25 +08:00
samanhappy
d778536388 fix: update tool call API endpoint structure and enhance error handling (#300) 2025-08-26 18:49:34 +08:00
Copilot
976e90679d Add OpenAPI specification generation for OpenWebUI integration (#295)
Co-authored-by: samanhappy <samanhappy@gmail.com>
2025-08-26 14:54:19 +08:00
samanhappy
f6ee9beed3 refactor: remove MCPRouter referer and title input sections from SettingsPage (#294) 2025-08-25 15:51:02 +08:00
samanhappy
69a800fa7a fix: update MCPRouter referer URL to new domain (#293) 2025-08-25 13:25:37 +08:00
Copilot
83cbd16821 Fix Dependabot security alert #11 - resolve sha.js and brace-expansion vulnerabilities (#292)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: samanhappy <2755122+samanhappy@users.noreply.github.com>
2025-08-25 12:26:04 +08:00
samanhappy
9300814994 Add .git to .dockerignore to prevent Git files from being included in Docker builds (#290) 2025-08-24 15:37:38 +08:00
Rilomilo
9952927a13 remove redundant code (#288) 2025-08-24 11:40:33 +08:00
samanhappy
4547ae526a fix: adjust spacing and heading size in LoginPage for improved layout (#286)
Co-authored-by: samanhappy@qq.com <my6051199>
2025-08-20 22:14:41 +08:00
Copilot
80b83bb029 Fix missing i18n translation for api.errors.invalid_credentials (#285)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: samanhappy <2755122+samanhappy@users.noreply.github.com>
2025-08-20 21:06:03 +08:00
Copilot
fa2de88fea Center login form and simplify layout with main slogan only (#283)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: samanhappy <2755122+samanhappy@users.noreply.github.com>
Co-authored-by: samanhappy <samanhappy@gmail.com>
2025-08-20 14:37:10 +08:00
samanhappy
6020611f57 Add prompt management functionality to MCP server (#281) 2025-08-20 14:23:55 +08:00
samanhappy
81c3091a5c fix: filter out empty values in tool arguments for improved functionality (#280)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-08-20 14:21:51 +08:00
samanhappy
6a8f246dff fix: adjust layout for LoginPage to improve responsiveness and styling (#278) 2025-08-14 16:28:47 +08:00
samanhappy
2bef1fb0bd fix: update @modelcontextprotocol/sdk dependency to version 1.17.2 (#277) 2025-08-14 15:17:19 +08:00
samanhappy
bdb5b37cf5 Add API documentation (#275) 2025-08-14 14:12:15 +08:00
samanhappy
cbb3b15ba2 fix: standardize naming to MCPHub across documentation and UI (#271)
Co-authored-by: samanhappy@qq.com <my6051199>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-08-13 21:54:41 +08:00
samanhappy
77b423fbcc Refactor JWT secret management and enhance documentation (#270) 2025-08-11 19:09:33 +08:00
samanhappy
604fe4f71d fix: remove registration endpoint from authentication bypass in middleware (#267) 2025-08-11 14:18:28 +08:00
samanhappy
907bca8aac Refactor cloud and market pages for improved functionality and UI consistency (#265) 2025-08-10 17:39:34 +08:00
samanhappy
8c58200dcc feat: add health check endpoint to monitor MCP server status (#264) 2025-08-10 16:46:22 +08:00
samanhappy
0b4dc453a5 fix: reinitialize mcp server connection on update (#263) 2025-08-10 16:20:36 +08:00
samanhappy
35012f99fc refine docs 2025-08-10 12:52:54 +08:00
samanhappy
22ad4f83f6 fix: handle undefined and null values for number inputs in DynamicForm (#261)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-08-10 12:52:20 +08:00
samanhappy
26720d9e49 feat: introduce cloud server market (#260) 2025-08-09 21:14:26 +08:00
samanhappy
a9aa4a9a08 feat: Update registerService to handle environment-specific service overrides (#257) 2025-08-05 14:48:48 +08:00
samanhappy
48bcf9f5f0 feat: Add cleanInputSchema function to remove $schema field from inputSchema (#255) 2025-08-05 13:45:52 +08:00
samanhappy
f63f06d879 feat: Enhance authentication flow by integrating permissions retrieval and updating related services (#256) 2025-08-05 13:45:31 +08:00
samanhappy
63b356b8d7 Add Chinese localization support and i18n middleware (#253) 2025-08-03 11:53:04 +08:00
samanhappy
a6cea2ad3f feat: Enhance group management with server tool configuration (#250) 2025-07-29 17:31:05 +08:00
samanhappy
5bb2715094 refactor: simplify LanguageSwitch component by removing unnecessary language count checks and enhancing dropdown behavior (#249) 2025-07-27 20:44:50 +08:00
samanhappy
9b40f7e101 feat: enhance LanguageSwitch component with language toggle functionality and improve dropdown behavior; update UserProfileMenu styles (#248) 2025-07-26 22:58:01 +08:00
samanhappy
df872823c1 Implement language and theme switchers in Header (#247) 2025-07-26 21:46:14 +08:00
samanhappy
9304653c34 feat: enhance GroupCard with copy options for ID, URL, and JSON; update translations (#246)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-07-25 14:30:52 +08:00
dependabot[bot]
b5685b7010 chore(deps): bump axios from 1.10.0 to 1.11.0 (#245)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-24 10:39:17 +08:00
samanhappy
89c37b2f02 Enhance operation name generation in OpenAPIClient (#244) 2025-07-23 19:02:43 +08:00
Oven
c316cb896e fix: create when dxt upload path does not exist (#243) 2025-07-23 13:47:11 +08:00
samanhappy
bc3c8facfa feat: add replaceEnvVarsInArray function and integrate it into server transport configuration (#241)
Co-authored-by: samanhappy@qq.com <my6051199>
2025-07-22 23:24:04 +08:00
dependabot[bot]
69afb865c0 chore(deps): bump brace-expansion from 1.1.11 to 1.1.12 (#231)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-22 23:07:20 +08:00
dependabot[bot]
ba30d88840 chore(deps): bump multer from 2.0.1 to 2.0.2 (#229)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-22 23:06:24 +08:00
samanhappy
6d0d622bd8 feat: add permissions for contents and packages in build workflow (#238) 2025-07-22 10:05:16 +08:00
samanhappy
ab50c7e9eb feat: add conditional check for repository in build and npm publish workflows (#236)
Co-authored-by: samanhappy@qq.com <my6051199>
2025-07-22 08:56:50 +08:00
samanhappy
e507bea2e3 Refactor service registration and revert lazy loading implementation (#234) 2025-07-20 22:30:09 +08:00
samanhappy
0f00ad7200 feat: implement lazy loading for data service and enhance service registration (#233) 2025-07-20 21:37:43 +08:00
samanhappy
b0b0c93337 feat: enable immediate loading of service overrides during registration (#232) 2025-07-20 20:35:00 +08:00
samanhappy
20fd355b87 feat: enhance JSON serialization safety & add dxt upload limit (#230) 2025-07-20 19:18:10 +08:00
dependabot[bot]
4388084704 chore(deps): bump typeorm from 0.3.24 to 0.3.25 (#210)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-07 09:54:52 +08:00
dependabot[bot]
fe2535461d chore(deps-dev): bump @types/react-dom from 19.1.5 to 19.1.6 (#211)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-07 09:39:52 +08:00
dependabot[bot]
985598e529 chore(deps): bump pg from 8.16.0 to 8.16.3 (#212)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-05 15:24:37 +08:00
dependabot[bot]
b2b6d0588b chore(deps-dev): bump tailwindcss from 4.1.8 to 4.1.11 (#213)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-03 14:11:24 +08:00
dependabot[bot]
64628ee3ed chore(deps-dev): bump tsx from 4.19.4 to 4.20.3 (#214)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-02 18:04:25 +08:00
samanhappy
66d4142039 feat: add variable detection and confirmation dialogs in server forms (#205) 2025-06-29 22:23:42 +08:00
samanhappy
cf72295f99 Refactor UI components across multiple pages for improved styling and consistency (#204) 2025-06-29 22:01:00 +08:00
samanhappy
89f85c73ff fix: resolve race conditions in initializeClientsFromSettings (#201) 2025-06-28 22:11:14 +08:00
samanhappy
adabf1d92b feat:support DXT file server installation (#200) 2025-06-27 14:45:24 +08:00
samanhappy
c3a6dfadb4 feat: Add dynamic header input fields for server configuration in ServerForm (#193) 2025-06-20 14:52:22 +08:00
samanhappy
d119be0f82 feat: Implement bearer token validation in auth middleware (#186) 2025-06-19 12:11:35 +08:00
samanhappy
1e308ec4c5 feat: add Jest testing framework and CI/CD configuration (#187)
Co-authored-by: samanhappy@qq.com <my6051199>
2025-06-18 14:02:52 +08:00
samanhappy
1bd4fd6d9c feat: Add OpenAPI support with comprehensive configuration options and client integration (#184)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-06-16 17:50:51 +08:00
Imgodmaoyouknow
4b3bb26301 fix: 修复从市场安装服务器时,选择SSE和Streamable HTTP不生效的问题 (#180) 2025-06-13 22:50:36 +08:00
samanhappy
40af398f68 chore: update @modelcontextprotocol/sdk to version 1.12.1 (#176) 2025-06-10 16:50:28 +08:00
samanhappy
4726f00a22 feat: add request options configuration to server form (#171) 2025-06-10 13:51:01 +08:00
samanhappy
77f64b7b98 feat: enhance DynamicForm to support array and object (#173)
Co-authored-by: samanhappy@qq.com <my6051199>
2025-06-10 13:50:14 +08:00
samanhappy
d9cbc5381a feat: implement keep-alive functionality for SSE connections (#166)
Co-authored-by: samanhappy@qq.com <my6051199>
2025-06-07 20:41:51 +08:00
samanhappy
56c6447469 feat: implement settings cache with load, save, clear, and status functions (#167) 2025-06-07 20:36:52 +08:00
samanhappy
f8149c4b0b fix: update SSE transport path to use basePath from config (#165)
Co-authored-by: samanhappy@qq.com <my6051199>
2025-06-07 20:35:20 +08:00
purefkh
e259f30539 fix: save user config when install mcp server from market (#168) 2025-06-07 20:34:51 +08:00
samanhappy
3a421bc476 fix: update tool name formatting from '/' to '-' in ToolCard and mcpService (#164)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-06-05 15:34:49 +08:00
samanhappy
503b60edb7 feat: add tool management features including toggle and description updates (#163)
Co-authored-by: samanhappy@qq.com <my6051199>
2025-06-04 16:03:45 +08:00
dependabot[bot]
4039a85ee1 chore(deps-dev): bump concurrently from 8.2.2 to 9.1.2 (#156)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-02 19:49:58 +08:00
dependabot[bot]
3a83b83a9e chore(deps-dev): bump @vitejs/plugin-react from 4.4.1 to 4.5.0 (#160)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-02 19:47:04 +08:00
samanhappy
c1621805de fix: expand environment variables in database and OpenAI configuration (#162) 2025-06-02 19:46:39 +08:00
270 changed files with 40607 additions and 11108 deletions

16
.coveragerc Normal file
View File

@@ -0,0 +1,16 @@
# Test coverage configuration
# This file tells Jest what to include/exclude from coverage reports
# Coverage patterns
- "src/**/*.{ts,tsx}"
# Exclusions
- "!src/**/*.d.ts"
- "!src/index.ts"
- "!src/**/__tests__/**"
- "!src/**/*.test.{ts,tsx}"
- "!src/**/*.spec.{ts,tsx}"
- "!**/node_modules/**"
- "!coverage/**"
- "!dist/**"
- "!build/**"

1
.dockerignore Normal file
View File

@@ -0,0 +1 @@
.git

View File

@@ -20,6 +20,6 @@
}
],
"@typescript-eslint/no-explicit-any": "off",
"no-undef": "off",
"no-undef": "off"
}
}

124
.github/DOCKER_CLI_TEST.md vendored Normal file
View File

@@ -0,0 +1,124 @@
# Docker Engine Installation Test Procedure
This document describes how to test the Docker Engine installation feature added with the `INSTALL_EXT=true` build argument.
## Test 1: Build with INSTALL_EXT=false (default)
```bash
# Build without extended features
docker build -t mcphub:base .
# Run the container
docker run --rm mcphub:base docker --version
```
**Expected Result**: `docker: not found` error (Docker is NOT installed)
## Test 2: Build with INSTALL_EXT=true
```bash
# Build with extended features
docker build --build-arg INSTALL_EXT=true -t mcphub:extended .
# Test Docker CLI is available
docker run --rm mcphub:extended docker --version
```
**Expected Result**: Docker version output (e.g., `Docker version 27.x.x, build xxxxx`)
## Test 3: Docker-in-Docker with Auto-start Daemon
```bash
# Build with extended features
docker build --build-arg INSTALL_EXT=true -t mcphub:extended .
# Run with privileged mode (allows Docker daemon to start)
docker run -d \
--name mcphub-test \
--privileged \
-p 3000:3000 \
mcphub:extended
# Wait a few seconds for daemon to start
sleep 5
# Test Docker commands from inside the container
docker exec mcphub-test docker ps
docker exec mcphub-test docker images
docker exec mcphub-test docker info
# Cleanup
docker stop mcphub-test
docker rm mcphub-test
```
**Expected Result**:
- Docker daemon should auto-start inside the container
- Docker commands should work without mounting the host's Docker socket
- `docker info` should show the container's own Docker daemon
## Test 4: Docker-in-Docker with Host Socket (Alternative)
```bash
# Build with extended features
docker build --build-arg INSTALL_EXT=true -t mcphub:extended .
# Run with Docker socket mounted (uses host's daemon)
docker run -d \
--name mcphub-test \
-p 3000:3000 \
-v /var/run/docker.sock:/var/run/docker.sock \
mcphub:extended
# Test Docker commands from inside the container
docker exec mcphub-test docker ps
docker exec mcphub-test docker images
# Cleanup
docker stop mcphub-test
docker rm mcphub-test
```
**Expected Result**:
- Docker daemon should NOT auto-start (socket already exists from host)
- Docker commands should work and show the host's containers and images
## Test 5: Verify Image Size
```bash
# Build both versions
docker build -t mcphub:base .
docker build --build-arg INSTALL_EXT=true -t mcphub:extended .
# Compare image sizes
docker images mcphub:*
```
**Expected Result**:
- The `extended` image should be larger than the `base` image
- The size difference should be reasonable (Docker Engine adds ~100-150MB)
## Test 6: Architecture Support
```bash
# On AMD64/x86_64
docker build --build-arg INSTALL_EXT=true --platform linux/amd64 -t mcphub:extended-amd64 .
# On ARM64
docker build --build-arg INSTALL_EXT=true --platform linux/arm64 -t mcphub:extended-arm64 .
```
**Expected Result**:
- Both builds should succeed
- AMD64 includes Chrome/Playwright + Docker Engine
- ARM64 includes Docker Engine only (Chrome installation is skipped)
## Notes
- The Docker Engine installation follows the official Docker documentation
- Includes full Docker daemon (`dockerd`), CLI (`docker`), and containerd
- The daemon auto-starts when running in privileged mode
- The installation uses the Debian Bookworm repository
- All temporary files are cleaned up to minimize image size
- The feature is opt-in via the `INSTALL_EXT` build argument
- `iptables` is installed as it's required for Docker networking

263
.github/copilot-instructions.md vendored Normal file
View File

@@ -0,0 +1,263 @@
# MCPHub Coding Instructions
**ALWAYS follow these instructions first and only fallback to additional search and context gathering if the information here is incomplete or found to be in error.**
## Project Overview
MCPHub is a TypeScript/Node.js MCP (Model Context Protocol) server management hub that provides unified access through HTTP endpoints. It serves as a centralized dashboard for managing multiple MCP servers with real-time monitoring, authentication, and flexible routing.
**Core Components:**
- **Backend**: Express.js + TypeScript + ESM (`src/server.ts`)
- **Frontend**: React/Vite + Tailwind CSS (`frontend/`)
- **MCP Integration**: Connects multiple MCP servers (`src/services/mcpService.ts`)
- **Authentication**: JWT-based with bcrypt password hashing
- **Configuration**: JSON-based MCP server definitions (`mcp_settings.json`)
- **Documentation**: API docs and usage instructions(`docs/`)
## Working Effectively
### Bootstrap and Setup (CRITICAL - Follow Exact Steps)
```bash
# Install pnpm if not available
npm install -g pnpm
# Install dependencies - takes ~30 seconds
pnpm install
# Setup environment (optional)
cp .env.example .env
# Build and test to verify setup
pnpm lint # ~3 seconds - NEVER CANCEL
pnpm backend:build # ~5 seconds - NEVER CANCEL
pnpm test:ci # ~16 seconds - NEVER CANCEL. Set timeout to 60+ seconds
pnpm frontend:build # ~5 seconds - NEVER CANCEL
pnpm build # ~10 seconds total - NEVER CANCEL. Set timeout to 60+ seconds
```
**CRITICAL TIMING**: These commands are fast but NEVER CANCEL them. Always wait for completion.
### Development Environment
```bash
# Start both backend and frontend (recommended for most development)
pnpm dev # Backend on :3001, Frontend on :5173
# OR start separately (required on Windows, optional on Linux/macOS)
# Terminal 1: Backend only
pnpm backend:dev # Runs on port 3000 (or PORT env var)
# Terminal 2: Frontend only
pnpm frontend:dev # Runs on port 5173, proxies API to backend
```
**NEVER CANCEL**: Development servers may take 10-15 seconds to fully initialize all MCP servers.
### Build Commands (Production)
```bash
# Full production build - takes ~10 seconds total
pnpm build # NEVER CANCEL - Set timeout to 60+ seconds
# Individual builds
pnpm backend:build # TypeScript compilation - ~5 seconds
pnpm frontend:build # Vite build - ~5 seconds
# Start production server
pnpm start # Requires dist/ and frontend/dist/ to exist
```
### Testing and Validation
```bash
# Run all tests - takes ~16 seconds with 73 tests
pnpm test:ci # NEVER CANCEL - Set timeout to 60+ seconds
# Development testing
pnpm test # Interactive mode
pnpm test:watch # Watch mode for development
pnpm test:coverage # With coverage report
# Code quality
pnpm lint # ESLint - ~3 seconds
pnpm format # Prettier formatting - ~3 seconds
```
**CRITICAL**: All tests MUST pass before committing. Do not modify tests to make them pass unless specifically required for your changes.
## Manual Validation Requirements
**ALWAYS perform these validation steps after making changes:**
### 1. Basic Application Functionality
```bash
# Start the application
pnpm dev
# Verify backend responds (in another terminal)
curl http://localhost:3000/api/health
# Expected: Should return health status
# Verify frontend serves
curl -I http://localhost:3000/
# Expected: HTTP 200 OK with HTML content
```
### 2. MCP Server Integration Test
```bash
# Check MCP servers are loading (look for log messages)
# Expected log output should include:
# - "Successfully connected client for server: [name]"
# - "Successfully listed [N] tools for server: [name]"
# - Some servers may fail due to missing API keys (normal in dev)
```
### 3. Build Verification
```bash
# Verify production build works
pnpm build
node scripts/verify-dist.js
# Expected: "✅ Verification passed! Frontend and backend dist files are present."
```
**NEVER skip these validation steps**. If any fail, debug and fix before proceeding.
## Project Structure and Key Files
### Critical Backend Files
- `src/index.ts` - Application entry point
- `src/server.ts` - Express server setup and middleware
- `src/services/mcpService.ts` - **Core MCP server management logic**
- `src/config/index.ts` - Configuration management
- `src/routes/` - HTTP route definitions
- `src/controllers/` - HTTP request handlers
- `src/dao/` - Data access layer for users, groups, servers
- `src/types/index.ts` - TypeScript type definitions
### Critical Frontend Files
- `frontend/src/` - React application source
- `frontend/src/pages/` - Page components (development entry point)
- `frontend/src/components/` - Reusable UI components
- `frontend/src/utils/fetchInterceptor.js` - Backend API interaction
### Configuration Files
- `mcp_settings.json` - **MCP server definitions and user accounts**
- `package.json` - Dependencies and scripts
- `tsconfig.json` - TypeScript configuration
- `jest.config.cjs` - Test configuration
- `.eslintrc.json` - Linting rules
### Docker and Deployment
- `Dockerfile` - Multi-stage build with Python base + Node.js
- `entrypoint.sh` - Docker startup script
- `bin/cli.js` - NPM package CLI entry point
## Development Process and Conventions
### Code Style Requirements
- **ESM modules**: Always use `.js` extensions in imports, not `.ts`
- **English only**: All code comments must be written in English
- **TypeScript strict**: Follow strict type checking rules
- **Import style**: `import { something } from './file.js'` (note .js extension)
### Key Configuration Notes
- **MCP servers**: Defined in `mcp_settings.json` with command/args
- **Endpoints**: `/mcp/{group|server}` and `/mcp/$smart` for routing
- **i18n**: Frontend uses react-i18next with files in `locales/` folder
- **Authentication**: JWT tokens with bcrypt password hashing
- **Default credentials**: admin/admin123 (configured in mcp_settings.json)
### Development Entry Points
- **Add MCP server**: Modify `mcp_settings.json` and restart
- **New API endpoint**: Add route in `src/routes/`, controller in `src/controllers/`
- **Frontend feature**: Start from `frontend/src/pages/` or `frontend/src/components/`
- **Add tests**: Follow patterns in `tests/` directory
### Common Development Tasks
#### Adding a new MCP server:
1. Add server definition to `mcp_settings.json`
2. Restart backend to load new server
3. Check logs for successful connection
4. Test via dashboard or API endpoints
#### API development:
1. Define route in `src/routes/`
2. Implement controller in `src/controllers/`
3. Add types in `src/types/index.ts` if needed
4. Write tests in `tests/controllers/`
#### Frontend development:
1. Create/modify components in `frontend/src/components/`
2. Add pages in `frontend/src/pages/`
3. Update routing if needed
4. Test in development mode with `pnpm frontend:dev`
#### Documentation:
1. Update or add docs in `docs/` folder
2. Ensure README.md reflects any major changes
## Validation and CI Requirements
### Before Committing - ALWAYS Run:
```bash
pnpm lint # Must pass - ~3 seconds
pnpm backend:build # Must compile - ~5 seconds
pnpm test:ci # All tests must pass - ~16 seconds
pnpm build # Full build must work - ~10 seconds
```
**CRITICAL**: CI will fail if any of these commands fail. Fix issues locally first.
### CI Pipeline (.github/workflows/ci.yml)
- Runs on Node.js 20.x
- Tests: linting, type checking, unit tests with coverage
- **NEVER CANCEL**: CI builds may take 2-3 minutes total
## Troubleshooting
### Common Issues
- **"uvx command not found"**: Some MCP servers require `uvx` (Python package manager) - this is expected in development
- **Port already in use**: Change PORT environment variable or kill existing processes
- **Frontend not loading**: Ensure frontend was built with `pnpm frontend:build`
- **MCP server connection failed**: Check server command/args in `mcp_settings.json`
### Build Failures
- **TypeScript errors**: Run `pnpm backend:build` to see compilation errors
- **Test failures**: Run `pnpm test:verbose` for detailed test output
- **Lint errors**: Run `pnpm lint` and fix reported issues
### Development Issues
- **Backend not starting**: Check for port conflicts, verify `mcp_settings.json` syntax
- **Frontend proxy errors**: Ensure backend is running before starting frontend
- **Hot reload not working**: Restart development server
## Performance Notes
- **Install time**: pnpm install takes ~30 seconds
- **Build time**: Full build takes ~10 seconds
- **Test time**: Complete test suite takes ~16 seconds
- **Startup time**: Backend initialization takes 10-15 seconds (MCP server connections)
**Remember**: NEVER CANCEL any build or test commands. Always wait for completion even if they seem slow.

View File

@@ -8,6 +8,9 @@ on:
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
strategy:
matrix:
variant: ${{ startsWith(github.ref, 'refs/tags/') && fromJSON('["base", "full"]') || fromJSON('["base"]') }}
@@ -30,16 +33,27 @@ jobs:
uses: docker/setup-buildx-action@v3
- name: Login to Docker Hub
if: endsWith(github.repository, 'mcphub')
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Login to GitHub Container Registry
if: endsWith(github.repository, 'mcphubx')
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Docker metadata
id: meta
uses: docker/metadata-action@v5
with:
images: samanhappy/mcphub
images: |
${{ endsWith(github.repository, 'mcphub') && github.repository || '' }}
${{ endsWith(github.repository, 'mcphubx') && format('ghcr.io/{0}', github.repository) || '' }}
tags: |
type=raw,value=edge${{ matrix.variant == 'full' && '-full' || '' }},enable=${{ github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' }}
type=semver,pattern={{version}}${{ matrix.variant == 'full' && '-full' || '' }},enable=${{ startsWith(github.ref, 'refs/tags/') }}
@@ -48,6 +62,7 @@ jobs:
latest=false
- name: Build and Push Docker Image
if: endsWith(github.repository, 'mcphub') || endsWith(github.repository, 'mcphubx')
uses: docker/build-push-action@v5
with:
context: .

112
.github/workflows/ci.yml vendored Normal file
View File

@@ -0,0 +1,112 @@
name: CI/CD Pipeline
on:
push:
branches: [main, develop]
pull_request:
branches: [main, develop]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [20.x]
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
- name: Enable Corepack
run: corepack enable
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Run linter
run: pnpm lint
- name: Run type checking
run: pnpm backend:build
- name: Run tests
run: pnpm test:ci
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v4
with:
file: ./coverage/lcov.info
flags: unittests
name: codecov-umbrella
# build:
# runs-on: ubuntu-latest
# needs: test
# steps:
# - name: Checkout code
# uses: actions/checkout@v4
# - name: Setup Node.js
# uses: actions/setup-node@v4
# with:
# node-version: '20.x'
# - name: Enable Corepack
# run: corepack enable
# - name: Install dependencies
# run: pnpm install --frozen-lockfile
# - name: Build application
# run: pnpm build
# - name: Verify build artifacts
# run: node scripts/verify-dist.js
# integration-test:
# runs-on: ubuntu-latest
# needs: test
# services:
# postgres:
# image: postgres:15
# env:
# POSTGRES_PASSWORD: postgres
# POSTGRES_DB: mcphub_test
# options: >-
# --health-cmd pg_isready
# --health-interval 10s
# --health-timeout 5s
# --health-retries 5
# steps:
# - name: Checkout code
# uses: actions/checkout@v4
# - name: Setup Node.js
# uses: actions/setup-node@v4
# with:
# node-version: '20.x'
# - name: Enable Corepack
# run: corepack enable
# - name: Install dependencies
# run: pnpm install --frozen-lockfile
# - name: Build application
# run: pnpm build
# - name: Run integration tests
# run: |
# export DATABASE_URL="postgresql://postgres:postgres@localhost:5432/mcphub_test"
# node test-integration.ts
# env:
# NODE_ENV: test

View File

@@ -7,6 +7,7 @@ on:
jobs:
publish-npm:
runs-on: ubuntu-latest
if: endsWith(github.repository, 'mcphub')
steps:
- name: Checkout
uses: actions/checkout@v4

4
.gitignore vendored
View File

@@ -2,6 +2,7 @@
/node_modules
/.pnp
.pnp.js
package-lock.json
# production
dist
@@ -24,3 +25,6 @@ yarn-error.log*
.vscode/
*.log
coverage/
data/
temp-test-config/

View File

@@ -4,4 +4,4 @@
"singleQuote": true,
"printWidth": 100,
"tabWidth": 2
}
}

30
AGENTS.md Normal file
View File

@@ -0,0 +1,30 @@
# Repository Guidelines
These notes align current contributors around the code layout, daily commands, and collaboration habits that keep `@samanhappy/mcphub` moving quickly.
## Project Structure & Module Organization
- Backend services live in `src`, grouped by responsibility (`controllers/`, `services/`, `dao/`, `routes/`, `utils/`), with `server.ts` orchestrating HTTP bootstrap.
- `frontend/src` contains the Vite + React dashboard; `frontend/public` hosts static assets and translations sit in `locales/`.
- Jest-aware test code is split between colocated specs (`src/**/*.{test,spec}.ts`) and higher-level suites in `tests/`; use `tests/utils/` helpers when exercising the CLI or SSE flows.
- Build artifacts and bundles are generated into `dist/`, `frontend/dist/`, and `coverage/`; never edit these manually.
## Build, Test, and Development Commands
- `pnpm dev` runs backend (`tsx watch src/index.ts`) and frontend (`vite`) together for local iteration.
- `pnpm backend:dev`, `pnpm frontend:dev`, and `pnpm frontend:preview` target each surface independently; prefer them when debugging one stack.
- `pnpm build` executes `pnpm backend:build` (TypeScript to `dist/`) and `pnpm frontend:build`; run before release or publishing.
- `pnpm test`, `pnpm test:watch`, and `pnpm test:coverage` drive Jest; `pnpm lint` and `pnpm format` enforce style via ESLint and Prettier.
## Coding Style & Naming Conventions
- TypeScript everywhere; default to 2-space indentation and single quotes, letting Prettier settle formatting. ESLint configuration assumes ES modules.
- Name services and data access layers with suffixes (`UserService`, `AuthDao`), React components and files in `PascalCase`, and utility modules in `camelCase`.
- Keep DTOs and shared types in `src/types` to avoid duplication; re-export through index files only when it clarifies imports.
## Testing Guidelines
- Use Jest with the `ts-jest` ESM preset; place shared setup in `tests/setup.ts` and mock helpers under `tests/utils/`.
- Mirror production directory names when adding new suites and end filenames with `.test.ts` or `.spec.ts` for automatic discovery.
- Aim to maintain or raise coverage when touching critical flows (auth, OAuth, SSE); add integration tests under `tests/integration/` when touching cross-service logic.
## Commit & Pull Request Guidelines
- Follow the existing Conventional Commit pattern (`feat:`, `fix:`, `chore:`, etc.) with imperative, present-tense summaries and optional multi-line context.
- Each PR should describe the behavior change, list testing performed, and link issues; include before/after screenshots or GIFs for frontend tweaks.
- Re-run `pnpm build` and `pnpm test` before requesting review, and ensure generated artifacts stay out of the diff.

View File

@@ -2,12 +2,6 @@ FROM python:3.13-slim-bookworm AS base
COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/
# 添加 HTTP_PROXY 和 HTTPS_PROXY 环境变量
ARG HTTP_PROXY=""
ARG HTTPS_PROXY=""
ENV HTTP_PROXY=$HTTP_PROXY
ENV HTTPS_PROXY=$HTTPS_PROXY
RUN apt-get update && apt-get install -y curl gnupg git \
&& curl -fsSL https://deb.nodesource.com/setup_22.x | bash - \
&& apt-get install -y nodejs \
@@ -15,12 +9,6 @@ RUN apt-get update && apt-get install -y curl gnupg git \
RUN npm install -g pnpm
ARG REQUEST_TIMEOUT=60000
ENV REQUEST_TIMEOUT=$REQUEST_TIMEOUT
ARG BASE_PATH=""
ENV BASE_PATH=$BASE_PATH
ENV PNPM_HOME=/usr/local/share/pnpm
ENV PATH=$PNPM_HOME:$PATH
RUN mkdir -p $PNPM_HOME && \
@@ -34,6 +22,16 @@ RUN if [ "$INSTALL_EXT" = "true" ]; then \
else \
echo "Skipping Chrome installation on non-amd64 architecture: $ARCH"; \
fi; \
# Install Docker Engine (includes CLI and daemon) \
apt-get update && \
apt-get install -y ca-certificates curl iptables && \
install -m 0755 -d /etc/apt/keyrings && \
curl -fsSL https://download.docker.com/linux/debian/gpg -o /etc/apt/keyrings/docker.asc && \
chmod a+r /etc/apt/keyrings/docker.asc && \
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/debian bookworm stable" | tee /etc/apt/sources.list.d/docker.list > /dev/null && \
apt-get update && \
apt-get install -y docker-ce docker-ce-cli containerd.io && \
apt-get clean && rm -rf /var/lib/apt/lists/*; \
fi
RUN uv tool install mcp-server-fetch

126
QWEN.md Normal file
View File

@@ -0,0 +1,126 @@
# MCPHub Project Overview
## Project Summary
MCPHub is a centralized hub server for managing multiple Model Context Protocol (MCP) servers. It allows organizing these servers into flexible Streamable HTTP (SSE) endpoints, supporting access to all servers, individual servers, or logical server groups. It provides a web dashboard for monitoring and managing servers, along with features like authentication, group-based access control, and Smart Routing using vector semantic search.
## Technology Stack
### Backend
- **Language:** TypeScript (Node.js)
- **Framework:** Express
- **Key Libraries:**
- `@modelcontextprotocol/sdk`: Core library for MCP interactions.
- `typeorm`: ORM for database interactions.
- `pg` & `pgvector`: PostgreSQL database and vector support.
- `jsonwebtoken` & `bcryptjs`: Authentication (JWT) and password hashing.
- `openai`: For embedding generation in Smart Routing.
- Various utility and validation libraries (e.g., `dotenv`, `express-validator`, `uuid`).
### Frontend
- **Framework:** React (via Vite)
- **Language:** TypeScript
- **UI Library:** Tailwind CSS
- **Routing:** `react-router-dom`
- **Internationalization:** `i18next`
- **Component Structure:** Modular components and pages within `frontend/src`.
### Infrastructure
- **Build Tool:** `pnpm` (package manager and script runner).
- **Containerization:** Docker (`Dockerfile` provided).
- **Process Management:** Not explicitly defined in core files, but likely managed by Docker or host system.
## Key Features
- **MCP Server Management:** Configure, start, stop, and monitor multiple upstream MCP servers via `stdio`, `SSE`, or `Streamable HTTP` protocols.
- **Centralized Dashboard:** Web UI for server status, group management, user administration, and logs.
- **Flexible Endpoints:**
- Global MCP/SSE endpoint (`/mcp`, `/sse`) for all enabled servers.
- Group-based endpoints (`/mcp/{group}`, `/sse/{group}`).
- Server-specific endpoints (`/mcp/{server}`, `/sse/{server}`).
- Smart Routing endpoint (`/mcp/$smart`, `/sse/$smart`) using vector search.
- **Authentication & Authorization:** JWT-based user authentication with role-based access control (admin/user).
- **Group Management:** Logical grouping of servers for targeted access and permission control.
- **Smart Routing (Experimental):** Uses pgvector and OpenAI embeddings to semantically search and find relevant tools across all connected servers.
- **Configuration:** Managed via `mcp_settings.json`.
- **Logging:** Server logs are captured and viewable via the dashboard.
- **Marketplace Integration:** Access to a marketplace of MCP servers (`servers.json`).
## Project Structure
```
C:\code\mcphub\
├───src\ # Backend source code (TypeScript)
├───frontend\ # Frontend source code (React/TypeScript)
│ ├───src\
│ ├───components\ # Reusable UI components
│ ├───pages\ # Top-level page components
│ ├───contexts\ # React contexts (Auth, Theme, Toast)
│ ├───layouts\ # Page layouts
│ ├───utils\ # Frontend utilities
│ └───...
├───dist\ # Compiled backend output
├───frontend\dist\ # Compiled frontend output
├───tests\ # Backend tests
├───docs\ # Documentation
├───scripts\ # Utility scripts
├───bin\ # CLI entry points
├───assets\ # Static assets (e.g., images for README)
├───.github\ # GitHub workflows
├───.vscode\ # VS Code settings
├───mcp_settings.json # Main configuration file for MCP servers and users
├───servers.json # Marketplace server definitions
├───package.json # Node.js project definition, dependencies, and scripts
├───pnpm-lock.yaml # Dependency lock file
├───tsconfig.json # TypeScript compiler configuration (Backend)
├───README.md # Project documentation
├───Dockerfile # Docker image definition
└───...
```
## Building and Running
### Prerequisites
- Node.js (>=18.0.0 or >=20.0.0)
- pnpm
- Python 3.13 (for some upstream servers and uvx)
- Docker (optional, for containerized deployment)
- PostgreSQL with pgvector (optional, for Smart Routing)
### Local Development
1. Clone the repository.
2. Install dependencies: `pnpm install`.
3. Start development servers: `pnpm dev`.
- This runs `pnpm backend:dev` (Node.js with `tsx watch`) and `pnpm frontend:dev` (Vite dev server) concurrently.
- Access the dashboard at `http://localhost:5173` (Vite default) or the configured port/path.
### Production Build
1. Install dependencies: `pnpm install`.
2. Build the project: `pnpm build`.
- This runs `pnpm backend:build` (TypeScript compilation to `dist/`) and `pnpm frontend:build` (Vite build to `frontend/dist/`).
3. Start the production server: `pnpm start`.
- This runs `node dist/index.js`.
### Docker Deployment
- Pull the image: `docker pull samanhappy/mcphub`.
- Run with default settings: `docker run -p 3000:3000 samanhappy/mcphub`.
- Run with custom config: `docker run -p 3000:3000 -v ./mcp_settings.json:/app/mcp_settings.json -v ./data:/app/data samanhappy/mcphub`.
- Access the dashboard at `http://localhost:3000`.
## Configuration
The main configuration file is `mcp_settings.json`. It defines:
- `mcpServers`: A map of server configurations (command, args, env, URL, etc.).
- `users`: A list of user accounts (username, hashed password, admin status).
- `groups`: A map of server groups.
- `systemConfig`: System-wide settings (e.g., proxy, registry, installation options).
## Development Conventions
- **Language:** TypeScript for both backend and frontend.
- **Backend Style:** Modular structure with clear separation of concerns (controllers, services, models, middlewares, routes, config, utils).
- **Frontend Style:** Component-based React architecture with contexts for state management.
- **Database:** TypeORM with PostgreSQL is used, leveraging decorators for entity definition.
- **Testing:** Uses `jest` for backend testing.
- **Linting/Formatting:** Uses `eslint` and `prettier`.
- **Scripts:** Defined in `package.json` under the `scripts` section for common tasks (dev, build, start, test, lint, format).

235
README.fr.md Normal file
View File

@@ -0,0 +1,235 @@
[English](README.md) | Français | [中文版](README.zh.md)
# MCPHub : Le Hub Unifié pour les Serveurs MCP (Model Context Protocol)
MCPHub facilite la gestion et la mise à l'échelle de plusieurs serveurs MCP (Model Context Protocol) en les organisant en points de terminaison HTTP streamables (SSE) flexibles, prenant en charge l'accès à tous les serveurs, à des serveurs individuels ou à des groupes de serveurs logiques.
![Aperçu du tableau de bord](assets/dashboard.zh.png)
## 🌐 Démo en direct et Documentation
- **Documentation** : [docs.mcphubx.com](https://docs.mcphubx.com/)
- **Environnement de démo** : [demo.mcphubx.com](https://demo.mcphubx.com/)
## 🚀 Fonctionnalités
- **Support étendu des serveurs MCP** : Intégrez de manière transparente n'importe quel serveur MCP avec une configuration minimale.
- **Tableau de bord centralisé** : Surveillez l'état en temps réel et les métriques de performance depuis une interface web élégante.
- **Gestion flexible des protocoles** : Compatibilité totale avec les protocoles MCP stdio et SSE.
- **Configuration à chaud** : Ajoutez, supprimez ou mettez à jour les serveurs MCP à la volée, sans temps d'arrêt.
- **Contrôle d'accès basé sur les groupes** : Organisez les serveurs en groupes personnalisables pour une gestion simplifiée des autorisations.
- **Authentification sécurisée** : Gestion des utilisateurs intégrée avec contrôle d'accès basé sur les rôles, optimisée par JWT et bcrypt.
- **Prêt pour Docker** : Déployez instantanément avec notre configuration conteneurisée.
## 🔧 Démarrage rapide
### Configuration
Créez un fichier `mcp_settings.json` pour personnaliser les paramètres de votre serveur :
```json
{
"mcpServers": {
"amap": {
"command": "npx",
"args": ["-y", "@amap/amap-maps-mcp-server"],
"env": {
"AMAP_MAPS_API_KEY": "votre-clé-api"
}
},
"playwright": {
"command": "npx",
"args": ["@playwright/mcp@latest", "--headless"]
},
"fetch": {
"command": "uvx",
"args": ["mcp-server-fetch"]
},
"slack": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-slack"],
"env": {
"SLACK_BOT_TOKEN": "votre-jeton-bot",
"SLACK_TEAM_ID": "votre-id-équipe"
}
}
}
}
```
### Déploiement avec Docker
**Recommandé** : Montez votre configuration personnalisée :
```bash
docker run -p 3000:3000 -v ./mcp_settings.json:/app/mcp_settings.json -v ./data:/app/data samanhappy/mcphub
```
Ou exécutez avec les paramètres par défaut :
```bash
docker run -p 3000:3000 samanhappy/mcphub
```
### Accéder au tableau de bord
Ouvrez `http://localhost:3000` et connectez-vous avec vos identifiants.
> **Note** : Les identifiants par défaut sont `admin` / `admin123`.
**Aperçu du tableau de bord** :
- État en direct de tous les serveurs MCP
- Activer/désactiver ou reconfigurer les serveurs
- Gestion des groupes pour organiser les serveurs
- Administration des utilisateurs pour le contrôle d'accès
### Point de terminaison HTTP streamable
> Pour le moment, la prise en charge des points de terminaison HTTP en streaming varie selon les clients IA. Si vous rencontrez des problèmes, vous pouvez utiliser le point de terminaison SSE ou attendre les futures mises à jour.
Connectez les clients IA (par exemple, Claude Desktop, Cursor, DeepChat, etc.) via :
```
http://localhost:3000/mcp
```
Ce point de terminaison fournit une interface HTTP streamable unifiée pour tous vos serveurs MCP. Il vous permet de :
- Envoyer des requêtes à n'importe quel serveur MCP configuré
- Recevoir des réponses en temps réel
- Intégrer facilement avec divers clients et outils IA
- Utiliser le même point de terminaison pour tous les serveurs, simplifiant votre processus d'intégration
**Routage intelligent (expérimental)** :
Le routage intelligent est le système de découverte d'outils intelligent de MCPHub qui utilise la recherche sémantique vectorielle pour trouver automatiquement les outils les plus pertinents pour une tâche donnée.
```
http://localhost:3000/mcp/$smart
```
**Comment ça marche** :
1. **Indexation des outils** : Tous les outils MCP sont automatiquement convertis en plongements vectoriels et stockés dans PostgreSQL avec pgvector.
2. **Recherche sémantique** : Les requêtes des utilisateurs sont converties en vecteurs et comparées aux plongements des outils en utilisant la similarité cosinus.
3. **Filtrage intelligent** : Des seuils dynamiques garantissent des résultats pertinents sans bruit.
4. **Exécution précise** : Les outils trouvés peuvent être directement exécutés avec une validation appropriée des paramètres.
**Prérequis pour la configuration** :
![Routage intelligent](assets/smart-routing.zh.png)
Pour activer le routage intelligent, vous avez besoin de :
- PostgreSQL avec l'extension pgvector
- Une clé API OpenAI (ou un service de plongement compatible)
- Activer le routage intelligent dans les paramètres de MCPHub
**Points de terminaison spécifiques aux groupes (recommandé)** :
![Gestion des groupes](assets/group.zh.png)
Pour un accès ciblé à des groupes de serveurs spécifiques, utilisez le point de terminaison HTTP basé sur les groupes :
```
http://localhost:3000/mcp/{group}
```
`{group}` est l'ID ou le nom du groupe que vous avez créé dans le tableau de bord. Cela vous permet de :
- Vous connecter à un sous-ensemble spécifique de serveurs MCP organisés par cas d'utilisation
- Isoler différents outils IA pour n'accéder qu'aux serveurs pertinents
- Mettre en œuvre un contrôle d'accès plus granulaire pour différents environnements ou équipes
**Points de terminaison spécifiques aux serveurs** :
Pour un accès direct à des serveurs individuels, utilisez le point de terminaison HTTP spécifique au serveur :
```
http://localhost:3000/mcp/{server}
```
`{server}` est le nom du serveur auquel vous souhaitez vous connecter. Cela vous permet d'accéder directement à un serveur MCP spécifique.
> **Note** : Si le nom du serveur et le nom du groupe sont identiques, le nom du groupe aura la priorité.
### Point de terminaison SSE (obsolète à l'avenir)
Connectez les clients IA (par exemple, Claude Desktop, Cursor, DeepChat, etc.) via :
```
http://localhost:3000/sse
```
Pour le routage intelligent, utilisez :
```
http://localhost:3000/sse/$smart
```
Pour un accès ciblé à des groupes de serveurs spécifiques, utilisez le point de terminaison SSE basé sur les groupes :
```
http://localhost:3000/sse/{group}
```
Pour un accès direct à des serveurs individuels, utilisez le point de terminaison SSE spécifique au serveur :
```
http://localhost:3000/sse/{server}
```
## 🧑‍💻 Développement local
```bash
git clone https://github.com/samanhappy/mcphub.git
cd mcphub
pnpm install
pnpm dev
```
Cela démarre à la fois le frontend et le backend en mode développement avec rechargement à chaud.
> Pour les utilisateurs de Windows, vous devrez peut-être démarrer le serveur backend et le frontend séparément : `pnpm backend:dev`, `pnpm frontend:dev`.
## 🛠️ Problèmes courants
### Utiliser Nginx comme proxy inverse
Si vous utilisez Nginx pour inverser le proxy de MCPHub, assurez-vous d'ajouter la configuration suivante dans votre configuration Nginx :
```nginx
proxy_buffering off
```
## 🔍 Stack technique
- **Backend** : Node.js, Express, TypeScript
- **Frontend** : React, Vite, Tailwind CSS
- **Authentification** : JWT & bcrypt
- **Protocole** : Model Context Protocol SDK
## 👥 Contribuer
Les contributions de toute nature sont les bienvenues !
- Nouvelles fonctionnalités et optimisations
- Améliorations de la documentation
- Rapports de bugs et corrections
- Traductions et suggestions
Rejoignez notre [communauté Discord](https://discord.gg/qMKNsn5Q) pour des discussions et du soutien.
## ❤️ Sponsor
Si vous aimez ce projet, vous pouvez peut-être envisager de :
[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/samanhappy)
## 🌟 Historique des étoiles
[![Historique des étoiles](https://api.star-history.com/svg?repos=samanhappy/mcphub&type=Date)](https://www.star-history.com/#samanhappy/mcphub&Date)
## 📄 Licence
Sous licence [Apache 2.0 License](LICENSE).

View File

@@ -1,11 +1,16 @@
# MCPHub: The Unified Hub for Model Context Protocol (MCP) Servers
English | [中文版](README.zh.md)
English | [Français](README.fr.md) | [中文版](README.zh.md)
MCPHub makes it easy to manage and scale multiple MCP (Model Context Protocol) servers by organizing them into flexible Streamable HTTP (SSE) endpoints—supporting access to all servers, individual servers, or logical server groups.
![Dashboard Preview](assets/dashboard.png)
## 🌐 Live Demo & Docs
- **Documentation**: [docs.mcphubx.com](https://docs.mcphubx.com/)
- **Demo Environment**: [demo.mcphubx.com](https://demo.mcphubx.com/)
## 🚀 Features
- **Broadened MCP Server Support**: Seamlessly integrate any MCP server with minimal configuration.
@@ -14,6 +19,8 @@ MCPHub makes it easy to manage and scale multiple MCP (Model Context Protocol) s
- **Hot-Swappable Configuration**: Add, remove, or update MCP servers on the fly — no downtime required.
- **Group-Based Access Control**: Organize servers into customizable groups for streamlined permissions management.
- **Secure Authentication**: Built-in user management with role-based access powered by JWT and bcrypt.
- **OAuth 2.0 Support**: Full OAuth support for upstream MCP servers with proxy authorization capabilities.
- **Environment Variable Expansion**: Use environment variables anywhere in your configuration for secure credential management. See [Environment Variables Guide](docs/environment-variables.md).
- **Docker-Ready**: Deploy instantly with our containerized setup.
## 🔧 Quick Start
@@ -52,12 +59,51 @@ Create a `mcp_settings.json` file to customize your server settings:
}
```
#### OAuth Configuration (Optional)
MCPHub supports OAuth 2.0 for authenticating with upstream MCP servers. See the [OAuth feature guide](docs/features/oauth.mdx) for a full walkthrough. In practice you will run into two configuration patterns:
- **Dynamic registration servers** (e.g., Vercel, Linear) publish all metadata and allow MCPHub to self-register. Simply declare the server URL and MCPHub handles the rest.
- **Manually provisioned servers** (e.g., GitHub Copilot) require you to create an OAuth App and provide the issued client ID/secret to MCPHub.
Dynamic registration example:
```json
{
"mcpServers": {
"vercel": {
"type": "sse",
"url": "https://mcp.vercel.com"
}
}
}
```
Manual registration example:
```json
{
"mcpServers": {
"github": {
"type": "sse",
"url": "https://api.githubcopilot.com/mcp/",
"oauth": {
"clientId": "${GITHUB_OAUTH_APP_ID}",
"clientSecret": "${GITHUB_OAUTH_APP_SECRET}"
}
}
}
}
```
For manual providers, create the OAuth App in the upstream console, set the redirect URI to `http://localhost:3000/oauth/callback` (or your deployed domain), and then plug the credentials into the dashboard or config file.
### Docker Deployment
**Recommended**: Mount your custom config:
```bash
docker run -p 3000:3000 -v $(pwd)/mcp_settings.json:/app/mcp_settings.json samanhappy/mcphub
docker run -p 3000:3000 -v ./mcp_settings.json:/app/mcp_settings.json -v ./data:/app/data samanhappy/mcphub
```
or run with default settings:
@@ -101,7 +147,11 @@ This endpoint provides a unified streamable HTTP interface for all your MCP serv
Smart Routing is MCPHub's intelligent tool discovery system that uses vector semantic search to automatically find the most relevant tools for any given task.
```
# Search across all servers
http://localhost:3000/mcp/$smart
# Search within a specific group
http://localhost:3000/mcp/$smart/{group}
```
**How it Works:**
@@ -110,6 +160,7 @@ http://localhost:3000/mcp/$smart
2. **Semantic Search**: User queries are converted to vectors and matched against tool embeddings using cosine similarity
3. **Intelligent Filtering**: Dynamic thresholds ensure relevant results without noise
4. **Precise Execution**: Found tools can be directly executed with proper parameter validation
5. **Group Scoping**: Optionally limit searches to servers within a specific group for focused results
**Setup Requirements:**
@@ -121,6 +172,23 @@ To enable Smart Routing, you need:
- OpenAI API key (or compatible embedding service)
- Enable Smart Routing in MCPHub settings
**Group-Scoped Smart Routing**:
You can combine Smart Routing with group filtering to search only within specific server groups:
```
# Search only within production servers
http://localhost:3000/mcp/$smart/production
# Search only within development servers
http://localhost:3000/mcp/$smart/development
```
This enables:
- **Focused Discovery**: Find tools only from relevant servers
- **Environment Isolation**: Separate tool discovery by environment (dev, staging, prod)
- **Team-Based Access**: Limit tool search to team-specific server groups
**Group-Specific Endpoints (Recommended)**:
![Group Management](assets/group.png)
@@ -159,7 +227,11 @@ http://localhost:3000/sse
For smart routing, use:
```
# Search across all servers
http://localhost:3000/sse/$smart
# Search within a specific group
http://localhost:3000/sse/$smart/{group}
```
For targeted access to specific server groups, use the group-based SSE endpoint:

View File

@@ -1,11 +1,16 @@
# MCPHub一站式 MCP 服务器聚合平台
[English Version](README.md) | 中文版
[English](README.md) | [Français](README.fr.md) | 中文版
MCPHub 通过将多个 MCPModel Context Protocol服务器组织为灵活的流式 HTTPSSE端点简化了管理与扩展工作。系统支持按需访问全部服务器、单个服务器或按场景分组的服务器集合。
![控制面板预览](assets/dashboard.zh.png)
## 🌐 在线文档与演示
- **文档地址**: [docs.mcphubx.com](https://docs.mcphubx.com/)
- **演示环境**: [demo.mcphubx.com](https://demo.mcphubx.com/)
## 🚀 功能亮点
- **广泛的 MCP 服务器支持**:无缝集成任何 MCP 服务器,配置简单。
@@ -52,12 +57,51 @@ MCPHub 通过将多个 MCPModel Context Protocol服务器组织为灵活
}
```
#### OAuth 配置(可选)
MCPHub 支持通过 OAuth 2.0 访问上游 MCP 服务器。完整说明请参阅[《OAuth 功能指南》](docs/zh/features/oauth.mdx)。实际使用中通常会遇到两类配置:
- **支持动态注册的服务器**(如 Vercel、Linear会公开全部元数据MCPHub 可自动注册并完成授权,仅需声明服务器地址。
- **需要手动配置客户端的服务器**(如 GitHub Copilot需要在提供商后台创建 OAuth 应用,并将获得的 Client ID/Secret 写入 MCPHub。
动态注册示例:
```json
{
"mcpServers": {
"vercel": {
"type": "sse",
"url": "https://mcp.vercel.com"
}
}
}
```
手动注册示例:
```json
{
"mcpServers": {
"github": {
"type": "sse",
"url": "https://api.githubcopilot.com/mcp/",
"oauth": {
"clientId": "${GITHUB_OAUTH_APP_ID}",
"clientSecret": "${GITHUB_OAUTH_APP_SECRET}"
}
}
}
}
```
对于需要手动注册的提供商,请先在上游控制台创建 OAuth 应用,将回调地址设置为 `http://localhost:3000/oauth/callback`(或你的部署域名),然后在控制台或配置文件中填写凭据。
### Docker 部署
**推荐**:挂载自定义配置:
```bash
docker run -p 3000:3000 -v $(pwd)/mcp_settings.json:/app/mcp_settings.json samanhappy/mcphub
docker run -p 3000:3000 -v ./mcp_settings.json:/app/mcp_settings.json -v ./data:/app/data samanhappy/mcphub
```
或使用默认配置运行:
@@ -101,7 +145,11 @@ http://localhost:3000/mcp
智能路由是 MCPHub 的智能工具发现系统,使用向量语义搜索自动为任何给定任务找到最相关的工具。
```
# 在所有服务器中搜索
http://localhost:3000/mcp/$smart
# 在特定分组中搜索
http://localhost:3000/mcp/$smart/{group}
```
**工作原理:**
@@ -110,6 +158,7 @@ http://localhost:3000/mcp/$smart
2. **语义搜索**:用户查询转换为向量并使用余弦相似度与工具嵌入匹配
3. **智能筛选**:动态阈值确保相关结果且无噪声
4. **精确执行**:找到的工具可以直接执行并进行适当的参数验证
5. **分组限定**:可选择将搜索限制在特定分组的服务器内以获得更精确的结果
**设置要求:**
@@ -121,6 +170,23 @@ http://localhost:3000/mcp/$smart
- OpenAI API 密钥(或兼容的嵌入服务)
- 在 MCPHub 设置中启用智能路由
**分组限定的智能路由**
您可以将智能路由与分组筛选结合使用,仅在特定服务器分组内搜索:
```
# 仅在生产服务器中搜索
http://localhost:3000/mcp/$smart/production
# 仅在开发服务器中搜索
http://localhost:3000/mcp/$smart/development
```
这样可以实现:
- **精准发现**:仅从相关服务器查找工具
- **环境隔离**:按环境(开发、测试、生产)分离工具发现
- **基于团队的访问**:将工具搜索限制在特定团队的服务器分组
**基于分组的 HTTP 端点(推荐)**
![分组](assets/group.zh.png)
要针对特定服务器分组进行访问,请使用基于分组的 HTTP 端点:
@@ -159,7 +225,11 @@ http://localhost:3000/sse
要启用智能路由,请使用:
```
# 在所有服务器中搜索
http://localhost:3000/sse/$smart
# 在特定分组中搜索
http://localhost:3000/sse/$smart/{group}
```
要针对特定服务器分组进行访问,请使用基于分组的 SSE 端点:

View File

@@ -48,11 +48,11 @@ MCPHub 已内置多个常用 MCP 服务如高德地图、GitHub、Slack、Fet
![配置高德地图](../assets/amap-edit.png)
点击保存后MCP Hub 将自动重启高德地图的 MCP 服务,使新配置生效。
点击保存后MCPHub 将自动重启高德地图的 MCP 服务,使新配置生效。
### 配置 MCP Hub SSE
### 配置 MCPHub SSE
MCP Hub 提供了单一聚合的 MCP Server SSE 端点:`http://localhost:3000/sse`,可在任意支持 MCP 的客户端中配置使用。这里我们选择开源的 Cherry Studio 进行演示。
MCPHub 提供了单一聚合的 MCP Server SSE 端点:`http://localhost:3000/sse`,可在任意支持 MCP 的客户端中配置使用。这里我们选择开源的 Cherry Studio 进行演示。
![配置 Cherry Studio](../assets/cherry-mcp.png)

View File

@@ -1,8 +1,7 @@
#!/usr/bin/env node
import path from 'path';
import { fileURLToPath } from 'url';
import { execSync } from 'child_process';
import { fileURLToPath, pathToFileURL } from 'url';
import fs from 'fs';
// Enable debug logging if needed
@@ -90,7 +89,10 @@ checkFrontend(projectRoot);
// Start the server
console.log('🚀 Starting MCPHub server...');
import(path.join(projectRoot, 'dist', 'index.js')).catch(err => {
const entryPath = path.join(projectRoot, 'dist', 'index.js');
// Convert to file:// URL for cross-platform ESM compatibility (required on Windows)
const entryUrl = pathToFileURL(entryPath).href;
import(entryUrl).catch(err => {
console.error('Failed to start MCPHub:', err);
process.exit(1);
});

147
docs/api-reference/auth.mdx Normal file
View File

@@ -0,0 +1,147 @@
---
title: "Authentication"
description: "Manage users and authentication."
---
import { Card, Cards } from 'mintlify';
<Card
title="POST /api/auth/login"
href="#login"
>
Log in to get a JWT token.
</Card>
<Card
title="POST /api/auth/register"
href="#register"
>
Register a new user.
</Card>
<Card
title="GET /api/auth/user"
href="#get-current-user"
>
Get the currently authenticated user.
</Card>
<Card
title="POST /api/auth/change-password"
href="#change-password"
>
Change the password for the current user.
</Card>
---
### Login
Authenticates a user and returns a JWT token along with user details.
- **Endpoint**: `/api/auth/login`
- **Method**: `POST`
- **Body**:
- `username` (string, required): The user's username.
- `password` (string, required): The user's password.
- **Request Example**:
```json
{
"username": "admin",
"password": "admin123"
}
```
- **Success Response**:
```json
{
"success": true,
"message": "Login successful",
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"user": {
"username": "admin",
"isAdmin": true,
"permissions": { ... }
}
}
```
---
### Register
Registers a new user and returns a JWT token.
- **Endpoint**: `/api/auth/register`
- **Method**: `POST`
- **Body**:
- `username` (string, required): The desired username.
- `password` (string, required): The desired password (must be at least 6 characters).
- `isAdmin` (boolean, optional): Whether the user should have admin privileges.
- **Request Example**:
```json
{
"username": "newuser",
"password": "password123",
"isAdmin": false
}
```
- **Success Response**:
```json
{
"success": true,
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"user": {
"username": "newuser",
"isAdmin": false,
"permissions": { ... }
}
}
```
---
### Get Current User
Retrieves the profile of the currently authenticated user.
- **Endpoint**: `/api/auth/user`
- **Method**: `GET`
- **Authentication**: Bearer Token required.
- **Success Response**:
```json
{
"success": true,
"user": {
"username": "admin",
"isAdmin": true,
"permissions": { ... }
}
}
```
---
### Change Password
Allows the authenticated user to change their password.
- **Endpoint**: `/api/auth/change-password`
- **Method**: `POST`
- **Authentication**: Bearer Token required.
- **Body**:
- `currentPassword` (string, required): The user's current password.
- `newPassword` (string, required): The desired new password (must be at least 6 characters).
- **Request Example**:
```json
{
"currentPassword": "oldpassword",
"newPassword": "newpassword123"
}
```
- **Success Response**:
```json
{
"success": true,
"message": "Password updated successfully"
}
```

View File

@@ -0,0 +1,111 @@
---
title: "Config"
description: "Manage and retrieve system-wide configurations."
---
import { Card, Cards } from 'mintlify';
<Card title="PUT /api/system-config" href="#update-system-config">Update the main system configuration.</Card>
<Card title="GET /api/settings" href="#get-all-settings">Get all system settings, including servers and groups.</Card>
<Card title="GET /config" href="#get-runtime-config">Get public runtime configuration for the frontend.</Card>
<Card title="GET /public-config" href="#get-public-config">Get public configuration to check for auth skip.</Card>
---
### Update System Config
Updates various parts of the system configuration. You only need to provide the keys for the sections you want to update.
- **Endpoint**: `/api/system-config`
- **Method**: `PUT`
- **Body**: A JSON object containing one or more of the following top-level keys: `routing`, `install`, `smartRouting`, `mcpRouter`.
#### Routing Configuration (`routing`)
- `enableGlobalRoute` (boolean): Enable or disable the global `/api/mcp` route.
- `enableGroupNameRoute` (boolean): Enable or disable group-based routing (e.g., `/api/mcp/group/:groupName`).
- `enableBearerAuth` (boolean): Enable bearer token authentication for MCP routes.
- `bearerAuthKey` (string): The secret key to use for bearer authentication.
- `skipAuth` (boolean): If true, skips all authentication, making the instance public.
#### Install Configuration (`install`)
- `pythonIndexUrl` (string): The base URL of the Python Package Index (PyPI) to use for installations.
- `npmRegistry` (string): The URL of the npm registry to use for installations.
- `baseUrl` (string): The public base URL of this MCPHub instance.
#### Smart Routing Configuration (`smartRouting`)
- `enabled` (boolean): Enable or disable the Smart Routing feature.
- `dbUrl` (string): The database connection URL for storing embeddings.
- `openaiApiBaseUrl` (string): The base URL for the OpenAI-compatible API for generating embeddings.
- `openaiApiKey` (string): The API key for the embeddings service.
- `openaiApiEmbeddingModel` (string): The name of the embedding model to use.
#### MCP Router Configuration (`mcpRouter`)
- `apiKey` (string): The API key for the MCP Router service.
- `referer` (string): The referer header to use for MCP Router requests.
- `title` (string): The title to display for this instance on MCP Router.
- `baseUrl` (string): The base URL for the MCP Router API.
- **Request Example**:
```json
{
"routing": {
"skipAuth": true
},
"smartRouting": {
"enabled": true,
"dbUrl": "postgresql://user:pass@host:port/db"
}
}
```
---
### Get All Settings
Retrieves the entire settings object for the instance, including all server configurations, groups, and system settings. This is a comprehensive dump of the `mcp_settings.json` file.
- **Endpoint**: `/api/settings`
- **Method**: `GET`
---
### Get Runtime Config
Retrieves the essential runtime configuration required for the frontend application. This endpoint does not require authentication.
- **Endpoint**: `/config`
- **Method**: `GET`
- **Success Response**:
```json
{
"success": true,
"data": {
"basePath": "",
"version": "1.0.0",
"name": "MCPHub"
}
}
```
---
### Get Public Config
Retrieves public configuration, primarily to check if authentication is skipped. This allows the frontend to adjust its behavior accordingly before a user has logged in. This endpoint does not require authentication.
- **Endpoint**: `/public-config`
- **Method**: `GET`
- **Success Response**:
```json
{
"success": true,
"data": {
"skipAuth": false,
"permissions": {}
}
}
```

View File

@@ -1,4 +0,0 @@
---
title: 'Create Plant'
openapi: 'POST /plants'
---

View File

@@ -1,4 +0,0 @@
---
title: 'Delete Plant'
openapi: 'DELETE /plants/{id}'
---

View File

@@ -1,4 +0,0 @@
---
title: 'Get Plants'
openapi: 'GET /plants'
---

View File

@@ -1,4 +0,0 @@
---
title: 'New Plant'
openapi: 'WEBHOOK /plant/webhook'
---

View File

@@ -0,0 +1,212 @@
---
title: "Groups"
description: "Manage server groups to organize and route requests."
---
import { Card, Cards } from 'mintlify';
<Card title="GET /api/groups" href="#get-all-groups">Get a list of all groups.</Card>
<Card title="POST /api/groups" href="#create-a-new-group">Create a new group.</Card>
<Card title="GET /api/groups/:id" href="#get-a-group">Get details of a specific group.</Card>
<Card title="PUT /api/groups/:id" href="#update-a-group">Update an existing group.</Card>
<Card title="DELETE /api/groups/:id" href="#delete-a-group">Delete a group.</Card>
<Card title="POST /api/groups/:id/servers" href="#add-server-to-group">Add a server to a group.</Card>
<Card title="DELETE /api/groups/:id/servers/:serverName" href="#remove-server-from-group">Remove a server from a group.</Card>
<Card title="PUT /api/groups/:id/servers/batch" href="#batch-update-group-servers">Batch update servers in a group.</Card>
<Card title="GET /api/groups/:id/server-configs" href="#get-group-server-configs">Get detailed server configurations in a group.</Card>
<Card title="PUT /api/groups/:id/server-configs/:serverName/tools" href="#update-group-server-tools">Update tool selection for a server in a group.</Card>
---
### Get All Groups
Retrieves a list of all server groups.
- **Endpoint**: `/api/groups`
- **Method**: `GET`
- **Success Response**:
```json
{
"success": true,
"data": [
{
"id": "group-1",
"name": "My Group",
"description": "A collection of servers.",
"servers": ["server1", "server2"],
"owner": "admin"
}
]
}
```
---
### Create a New Group
Creates a new server group.
- **Endpoint**: `/api/groups`
- **Method**: `POST`
- **Body**:
- `name` (string, required): The name of the group.
- `description` (string, optional): A description for the group.
- `servers` (array of strings, optional): A list of server names to include in the group.
- **Request Example**:
```json
{
"name": "My New Group",
"description": "A description for the new group",
"servers": ["server1", "server2"]
}
```
---
### Get a Group
Retrieves details for a specific group by its ID or name.
- **Endpoint**: `/api/groups/:id`
- **Method**: `GET`
- **Parameters**:
- `:id` (string, required): The ID or name of the group.
---
### Update a Group
Updates an existing group's name, description, or server list.
- **Endpoint**: `/api/groups/:id`
- **Method**: `PUT`
- **Parameters**:
- `:id` (string, required): The ID or name of the group to update.
- **Body**:
- `name` (string, optional): The new name for the group.
- `description` (string, optional): The new description for the group.
- `servers` (array, optional): The new list of servers for the group. See [Batch Update Group Servers](#batch-update-group-servers) for format.
- **Request Example**:
```json
{
"name": "Updated Group Name",
"description": "Updated description"
}
```
---
### Delete a Group
Deletes a group by its ID or name.
- **Endpoint**: `/api/groups/:id`
- **Method**: `DELETE`
- **Parameters**:
- `:id` (string, required): The ID or name of the group to delete.
---
### Add Server to Group
Adds a single server to a group.
- **Endpoint**: `/api/groups/:id/servers`
- **Method**: `POST`
- **Parameters**:
- `:id` (string, required): The ID or name of the group.
- **Body**:
- `serverName` (string, required): The name of the server to add.
- **Request Example**:
```json
{
"serverName": "my-server"
}
```
---
### Remove Server from Group
Removes a single server from a group.
- **Endpoint**: `/api/groups/:id/servers/:serverName`
- **Method**: `DELETE`
- **Parameters**:
- `:id` (string, required): The ID or name of the group.
- `:serverName` (string, required): The name of the server to remove.
---
### Batch Update Group Servers
Replaces all servers in a group with a new list. The list can be simple strings or detailed configuration objects.
- **Endpoint**: `/api/groups/:id/servers/batch`
- **Method**: `PUT`
- **Parameters**:
- `:id` (string, required): The ID or name of the group.
- **Body**:
- `servers` (array, required): An array of server names (strings) or server configuration objects.
- **Request Example (Simple)**:
```json
{
"servers": ["server1", "server2"]
}
```
- **Request Example (Detailed)**:
```json
{
"servers": [
{ "name": "server1", "tools": "all" },
{ "name": "server2", "tools": ["toolA", "toolB"] }
]
}
```
---
### Get Group Server Configs
Retrieves the detailed configuration for all servers within a group, including which tools are enabled.
- **Endpoint**: `/api/groups/:id/server-configs`
- **Method**: `GET`
- **Parameters**:
- `:id` (string, required): The ID or name of the group.
- **Success Response**:
```json
{
"success": true,
"data": [
{
"name": "server1",
"tools": "all"
},
{
"name": "server2",
"tools": ["toolA", "toolB"]
}
]
}
```
---
### Update Group Server Tools
Updates the tool selection for a specific server within a group.
- **Endpoint**: `/api/groups/:id/server-configs/:serverName/tools`
- **Method**: `PUT`
- **Parameters**:
- `:id` (string, required): The ID or name of the group.
- `:serverName` (string, required): The name of the server to update.
- **Body**:
- `tools` (string or array of strings, required): Either the string `"all"` to enable all tools, or an array of tool names to enable specifically.
- **Request Example**:
```json
{
"tools": ["toolA", "toolC"]
}
```

View File

@@ -1,33 +1,13 @@
---
title: 'Introduction'
description: 'Example section for showcasing API endpoints'
title: "Introduction"
description: "Welcome to the MCPHub API documentation."
---
<Note>
If you're not looking to build API reference documentation, you can delete
this section by removing the api-reference folder.
</Note>
The MCPHub API provides a comprehensive set of endpoints to manage your MCP servers, groups, users, and more. The API is divided into two main categories:
## Welcome
- **MCP Endpoints**: These are the primary endpoints for interacting with your MCP servers. They provide a unified interface for sending requests to your servers and receiving responses in real-time.
- **Management API**: These endpoints are used for managing the MCPHub instance itself. This includes managing servers, groups, users, and system settings.
There are two ways to build API documentation: [OpenAPI](https://mintlify.com/docs/api-playground/openapi/setup) and [MDX components](https://mintlify.com/docs/api-playground/mdx/configuration). For the starter kit, we are using the following OpenAPI specification.
All API endpoints are available under the `/api` path. For example, the endpoint to get all servers is `/api/servers`.
<Card
title="Plant Store Endpoints"
icon="leaf"
href="https://github.com/mintlify/starter/blob/main/api-reference/openapi.json"
>
View the OpenAPI specification file
</Card>
## Authentication
All API endpoints are authenticated using Bearer tokens and picked up from the specification file.
```json
"security": [
{
"bearerAuth": []
}
]
```
Authentication is required for most Management API endpoints. See the [Authentication](/api-reference/auth) section for more details.

View File

@@ -0,0 +1,81 @@
---
title: "Logs"
description: "Access and manage server logs."
---
import { Card, Cards } from 'mintlify';
<Card
title="GET /api/logs"
href="#get-all-logs"
>
Get all logs.
</Card>
<Card
title="DELETE /api/logs"
href="#clear-logs"
>
Clear all logs.
</Card>
<Card
title="GET /api/logs/stream"
href="#stream-logs"
>
Stream logs in real-time.
</Card>
---
### Get All Logs
Retrieves all stored logs.
- **Endpoint**: `/api/logs`
- **Method**: `GET`
- **Success Response**:
```json
{
"success": true,
"data": [
{
"timestamp": "2023-10-27T10:00:00.000Z",
"level": "info",
"message": "Server started successfully.",
"service": "system"
}
]
}
```
---
### Clear Logs
Deletes all stored logs.
- **Endpoint**: `/api/logs`
- **Method**: `DELETE`
- **Success Response**:
```json
{
"success": true,
"message": "Logs cleared successfully"
}
```
---
### Stream Logs
Streams logs in real-time using Server-Sent Events (SSE). The connection will remain open, and new log entries will be sent as they occur.
- **Endpoint**: `/api/logs/stream`
- **Method**: `GET`
- **Response Format**: The stream sends events with a `data` field containing a JSON object. The first event has `type: 'initial'` and contains all historical logs. Subsequent events have `type: 'log'` and contain a single new log entry.
- **Example Event**:
```
data: {"type":"log","log":{"timestamp":"2023-10-27T10:00:05.000Z","level":"debug","message":"Processing request for /api/some-endpoint","service":"mcp-server"}}
```

View File

@@ -0,0 +1,33 @@
---
title: "MCP HTTP Endpoints"
description: "Connect to your MCP servers using the unified HTTP endpoint."
---
MCPHub provides a unified streamable HTTP interface for all your MCP servers. This allows you to send requests to any configured MCP server and receive responses in real-time.
### Unified Endpoint
This endpoint provides access to all enabled MCP servers.
- **Endpoint**: `http://localhost:3000/mcp`
- **Method**: `POST`
### Group-Specific Endpoint
For targeted access to specific server groups, use the group-based HTTP endpoint.
- **Endpoint**: `http://localhost:3000/mcp/{group}`
- **Method**: `POST`
- **Parameters**:
- `{group}`: The ID or name of the group.
### Server-Specific Endpoint
For direct access to individual servers, use the server-specific HTTP endpoint.
- **Endpoint**: `http://localhost:3000/mcp/{server}`
- **Method**: `POST`
- **Parameters**:
- `{server}`: The name of the server.
> **Note**: If a server name and group name are the same, the group will take precedence.

View File

@@ -0,0 +1,25 @@
---
title: "MCP SSE Endpoints (Deprecated)"
description: "Connect to your MCP servers using the SSE endpoint."
---
The SSE endpoint is deprecated and will be removed in a future version. Please use the [MCP HTTP Endpoints](/api-reference/mcp-http) instead.
### Unified Endpoint
- **Endpoint**: `http://localhost:3000/sse`
- **Method**: `GET`
### Group-Specific Endpoint
- **Endpoint**: `http://localhost:3000/sse/{group}`
- **Method**: `GET`
- **Parameters**:
- `{group}`: The ID or name of the group.
### Server-Specific Endpoint
- **Endpoint**: `http://localhost:3000/sse/{server}`
- **Method**: `GET`
- **Parameters**:
- `{server}`: The name of the server.

View File

@@ -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
<CodeGroup>
```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"
```
</CodeGroup>
Generates and returns the complete OpenAPI 3.0.3 specification for all connected MCP tools.
**Query Parameters:**
<ParamField query="title" type="string" optional>
Custom API title
</ParamField>
<ParamField query="description" type="string" optional>
Custom API description
</ParamField>
<ParamField query="version" type="string" optional>
Custom API version
</ParamField>
<ParamField query="serverUrl" type="string" optional>
Custom server URL
</ParamField>
<ParamField query="includeDisabled" type="boolean" optional default="false">
Include disabled tools
</ParamField>
<ParamField query="servers" type="string" optional>
Comma-separated list of server names to include
</ParamField>
### Available Servers
<CodeGroup>
```bash GET /api/openapi/servers
curl "http://localhost:3000/api/openapi/servers"
```
</CodeGroup>
Returns a list of connected MCP server names.
<ResponseExample>
```json Example Response
{
"success": true,
"data": ["amap", "playwright", "slack"]
}
```
</ResponseExample>
### Tool Statistics
<CodeGroup>
```bash GET /api/openapi/stats
curl "http://localhost:3000/api/openapi/stats"
```
</CodeGroup>
Returns statistics about available tools and servers.
<ResponseExample>
```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"}
]
}
}
```
</ResponseExample>
### Tool Execution
<CodeGroup>
```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"}'
```
</CodeGroup>
Execute MCP tools via OpenAPI-compatible endpoints.
**Path Parameters:**
<ParamField path="serverName" type="string" required>
The name of the MCP server
</ParamField>
<ParamField path="toolName" type="string" required>
The name of the tool to execute
</ParamField>
## OpenWebUI Integration
To integrate MCPHub with OpenWebUI:
<Steps>
<Step title="Start MCPHub">
Ensure MCPHub is running with your MCP servers configured
</Step>
<Step title="Get OpenAPI Specification">
```bash
curl http://localhost:3000/api/openapi.json > mcphub-api.json
```
</Step>
<Step title="Add to OpenWebUI">
Import the OpenAPI specification file or point to the URL directly in OpenWebUI
</Step>
</Steps>
### Configuration Example
In OpenWebUI, you can add MCPHub as an OpenAPI tool by using:
<CardGroup cols={2}>
<Card title="OpenAPI URL" icon="link">
`http://localhost:3000/api/openapi.json`
</Card>
<Card title="Base URL" icon="server">
`http://localhost:3000/api`
</Card>
</CardGroup>
## 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
<CardGroup cols={2}>
<Card title="Direct Integration" icon="plug">
No need for intermediate proxy
</Card>
<Card title="Real-time Updates" icon="refresh">
OpenAPI spec updates automatically as MCP servers connect/disconnect
</Card>
<Card title="Better Performance" icon="bolt">
Direct tool execution without proxy overhead
</Card>
<Card title="Simplified Architecture" icon="layer-group">
One less component to manage
</Card>
</CardGroup>
## Troubleshooting
<AccordionGroup>
<Accordion title="OpenAPI spec shows no tools">
Ensure MCP servers are connected. Check `/api/openapi/stats` for server status.
</Accordion>
<Accordion title="Tool execution fails">
Verify the tool name and parameters match the OpenAPI specification. Check server logs for details.
</Accordion>
<Accordion title="OpenWebUI can't connect">
Ensure MCPHub is accessible from OpenWebUI and the OpenAPI URL is correct.
</Accordion>
<Accordion title="Missing tools in specification">
Check if tools are enabled in your MCP server configuration. Use `includeDisabled=true` to see all tools.
</Accordion>
</AccordionGroup>

View File

@@ -0,0 +1,209 @@
---
title: "Servers"
description: "Manage your MCP servers."
---
import { Card, Cards } from 'mintlify';
<Card
title="GET /api/servers"
href="#get-all-servers"
>
Get a list of all MCP servers.
</Card>
<Card
title="POST /api/servers"
href="#create-a-new-server"
>
Create a new MCP server.
</Card>
<Card
title="PUT /api/servers/:name"
href="#update-a-server"
>
Update an existing MCP server.
</Card>
<Card
title="DELETE /api/servers/:name"
href="#delete-a-server"
>
Delete an MCP server.
</Card>
<Card
title="POST /api/servers/:name/toggle"
href="#toggle-a-server"
>
Toggle the enabled state of a server.
</Card>
<Card
title="POST /api/servers/:serverName/tools/:toolName/toggle"
href="#toggle-a-tool"
>
Toggle the enabled state of a tool.
</Card>
<Card
title="PUT /api/servers/:serverName/tools/:toolName/description"
href="#update-tool-description"
>
Update the description of a tool.
</Card>
---
### Get All Servers
Retrieves a list of all configured MCP servers, including their status and available tools.
- **Endpoint**: `/api/servers`
- **Method**: `GET`
- **Response**:
```json
{
"success": true,
"data": [
{
"name": "example-server",
"status": "connected",
"tools": [
{
"name": "tool1",
"description": "Description of tool 1"
}
],
"config": {
"type": "stdio",
"command": "node",
"args": ["server.js"]
}
}
]
}
```
---
### Create a New Server
Adds a new MCP server to the configuration.
- **Endpoint**: `/api/servers`
- **Method**: `POST`
- **Body**:
```json
{
"name": "my-new-server",
"config": {
"type": "stdio",
"command": "python",
"args": ["-u", "my_script.py"],
"owner": "admin"
}
}
```
- `name` (string, required): The unique name for the server.
- `config` (object, required): The server configuration object.
- `type` (string): `stdio`, `sse`, `streamable-http`, or `openapi`.
- `command` (string): Command to execute for `stdio` type.
- `args` (array of strings): Arguments for the command.
- `url` (string): URL for `sse`, `streamable-http`, or `openapi` types.
- `openapi` (object): OpenAPI configuration.
- `url` (string): URL to the OpenAPI schema.
- `schema` (object): The OpenAPI schema object itself.
- `headers` (object): Headers to send with requests for `sse`, `streamable-http`, and `openapi` types.
- `keepAliveInterval` (number): Keep-alive interval in milliseconds for `sse` type. Defaults to 60000.
- `owner` (string): The owner of the server. Defaults to the current user or 'admin'.
---
### Update a Server
Updates the configuration of an existing MCP server.
- **Endpoint**: `/api/servers/:name`
- **Method**: `PUT`
- **Parameters**:
- `:name` (string, required): The name of the server to update.
- **Body**:
```json
{
"config": {
"type": "stdio",
"command": "node",
"args": ["new_server.js"]
}
}
```
- `config` (object, required): The updated server configuration object. See "Create a New Server" for details.
---
### Delete a Server
Removes an MCP server from the configuration.
- **Endpoint**: `/api/servers/:name`
- **Method**: `DELETE`
- **Parameters**:
- `:name` (string, required): The name of the server to delete.
---
### Toggle a Server
Enables or disables an MCP server.
- **Endpoint**: `/api/servers/:name/toggle`
- **Method**: `POST`
- **Parameters**:
- `:name` (string, required): The name of the server to toggle.
- **Body**:
```json
{
"enabled": true
}
```
- `enabled` (boolean, required): `true` to enable the server, `false` to disable it.
---
### Toggle a Tool
Enables or disables a specific tool on a server.
- **Endpoint**: `/api/servers/:serverName/tools/:toolName/toggle`
- **Method**: `POST`
- **Parameters**:
- `:serverName` (string, required): The name of the server.
- `:toolName` (string, required): The name of the tool.
- **Body**:
```json
{
"enabled": true
}
```
- `enabled` (boolean, required): `true` to enable the tool, `false` to disable it.
---
### Update Tool Description
Updates the description of a specific tool.
- **Endpoint**: `/api/servers/:serverName/tools/:toolName/description`
- **Method**: `PUT`
- **Parameters**:
- `:serverName` (string, required): The name of the server.
- `:toolName` (string, required): The name of the tool.
- **Body**:
```json
{
"description": "New tool description"
}
```
- `description` (string, required): The new description for the tool.

View File

@@ -0,0 +1,29 @@
---
title: "Smart Routing"
description: "Intelligent tool discovery using vector semantic search."
---
Smart Routing is MCPHub's intelligent tool discovery system that uses vector semantic search to automatically find the most relevant tools for any given task.
### HTTP Endpoint
- **Endpoint**: `http://localhost:3000/mcp/$smart`
- **Method**: `POST`
### SSE Endpoint (Deprecated)
- **Endpoint**: `http://localhost:3000/sse/$smart`
- **Method**: `GET`
### How it Works
1. **Tool Indexing**: All MCP tools are automatically converted to vector embeddings and stored in PostgreSQL with pgvector.
2. **Semantic Search**: User queries are converted to vectors and matched against tool embeddings using cosine similarity.
3. **Intelligent Filtering**: Dynamic thresholds ensure relevant results without noise.
4. **Precise Execution**: Found tools can be directly executed with proper parameter validation.
### Setup Requirements
- PostgreSQL with pgvector extension
- OpenAI API key (or compatible embedding service)
- Enable Smart Routing in MCPHub settings

View File

@@ -41,6 +41,50 @@ docker run -d \
mcphub:local
```
### Building with Extended Features
The Docker image supports an `INSTALL_EXT` build argument to include additional tools:
```bash
# Build with extended features (includes Docker Engine, Chrome/Playwright)
docker build --build-arg INSTALL_EXT=true -t mcphub:extended .
# Option 1: Run with automatic Docker-in-Docker (requires privileged mode)
docker run -d \
--name mcphub \
--privileged \
-p 3000:3000 \
-v $(pwd)/mcp_settings.json:/app/mcp_settings.json \
mcphub:extended
# Option 2: Run with Docker socket mounted (use host's Docker daemon)
docker run -d \
--name mcphub \
-p 3000:3000 \
-v $(pwd)/mcp_settings.json:/app/mcp_settings.json \
-v /var/run/docker.sock:/var/run/docker.sock \
mcphub:extended
# Verify Docker is available
docker exec mcphub docker --version
docker exec mcphub docker ps
```
<Note>
**What's included with INSTALL_EXT=true:**
- **Docker Engine**: Full Docker daemon with CLI for container management. The daemon auto-starts when the container runs in privileged mode.
- **Chrome/Playwright** (amd64 only): For browser automation tasks
The extended image is larger but provides additional capabilities for advanced use cases.
</Note>
<Warning>
**Docker-in-Docker Security Considerations:**
- **Privileged mode** (`--privileged`): Required for the Docker daemon to start inside the container. This gives the container elevated permissions on the host.
- **Docker socket mounting** (`/var/run/docker.sock`): Gives the container access to the host's Docker daemon. Both approaches should only be used in trusted environments.
- For production, consider using Docker socket mounting instead of privileged mode for better security.
</Warning>
## Docker Compose Setup
### Basic Configuration

View File

@@ -11,261 +11,34 @@ MCPHub uses environment variables for configuration. This guide covers all avail
### Server Configuration
| Variable | Default | Description |
| ----------- | ------------- | ------------------------------------------------------------- |
| `PORT` | `3000` | Port number for the HTTP server |
| `HOST` | `0.0.0.0` | Host address to bind the server |
| `NODE_ENV` | `development` | Application environment (`development`, `production`, `test`) |
| `LOG_LEVEL` | `info` | Logging level (`error`, `warn`, `info`, `debug`) |
| Variable | Default | Description |
| --- | --- | --- |
| `PORT` | `3000` | Port number for the HTTP server |
| `INIT_TIMEOUT` | `300000` | Initial timeout for the application |
| `BASE_PATH` | `''` | The base path of the application |
| `READONLY` | `false` | Set to `true` to enable readonly mode |
| `MCPHUB_SETTING_PATH` | | Path to the MCPHub settings |
| `NODE_ENV` | `development` | Application environment (`development`, `production`, `test`) |
```env
PORT=3000
HOST=0.0.0.0
INIT_TIMEOUT=300000
BASE_PATH=/api
READONLY=true
MCPHUB_SETTING_PATH=/path/to/settings
NODE_ENV=production
LOG_LEVEL=info
```
### Database Configuration
| Variable | Default | Description |
| -------------- | ----------- | ---------------------------------- |
| `DATABASE_URL` | - | PostgreSQL connection string |
| `DB_HOST` | `localhost` | Database host |
| `DB_PORT` | `5432` | Database port |
| `DB_NAME` | `mcphub` | Database name |
| `DB_USER` | `mcphub` | Database username |
| `DB_PASSWORD` | - | Database password |
| `DB_SSL` | `false` | Enable SSL for database connection |
| `DB_POOL_MIN` | `2` | Minimum database pool size |
| `DB_POOL_MAX` | `10` | Maximum database pool size |
```env
# Option 1: Full connection string
DATABASE_URL=postgresql://username:password@localhost:5432/mcphub
# Option 2: Individual components
DB_HOST=localhost
DB_PORT=5432
DB_NAME=mcphub
DB_USER=mcphub
DB_PASSWORD=your-password
DB_SSL=false
```
## Authentication & Security
### JWT Configuration
| Variable | Default | Description |
| ------------------------ | ------- | ------------------------------------------- |
| `JWT_SECRET` | - | Secret key for JWT token signing (required) |
| `JWT_EXPIRES_IN` | `24h` | JWT token expiration time |
| `JWT_REFRESH_EXPIRES_IN` | `7d` | Refresh token expiration time |
| `JWT_ALGORITHM` | `HS256` | JWT signing algorithm |
| Variable | Default | Description |
| --- | --- | --- |
| `JWT_SECRET` | - | Secret key for JWT token signing (required) |
```env
JWT_SECRET=your-super-secret-key-change-this-in-production
JWT_EXPIRES_IN=24h
JWT_REFRESH_EXPIRES_IN=7d
```
### Session & Security
| Variable | Default | Description |
| ------------------- | ------- | ------------------------------- |
| `SESSION_SECRET` | - | Session encryption secret |
| `BCRYPT_ROUNDS` | `12` | bcrypt hashing rounds |
| `RATE_LIMIT_WINDOW` | `15` | Rate limiting window in minutes |
| `RATE_LIMIT_MAX` | `100` | Maximum requests per window |
| `CORS_ORIGIN` | `*` | Allowed CORS origins |
```env
SESSION_SECRET=your-session-secret
BCRYPT_ROUNDS=12
RATE_LIMIT_WINDOW=15
RATE_LIMIT_MAX=100
CORS_ORIGIN=https://your-domain.com,https://admin.your-domain.com
```
## External Services
### OpenAI Configuration
| Variable | Default | Description |
| ------------------------ | ------------------------ | -------------------------------- |
| `OPENAI_API_KEY` | - | OpenAI API key for smart routing |
| `OPENAI_MODEL` | `gpt-3.5-turbo` | OpenAI model for embeddings |
| `OPENAI_EMBEDDING_MODEL` | `text-embedding-ada-002` | Model for vector embeddings |
| `OPENAI_MAX_TOKENS` | `1000` | Maximum tokens per request |
| `OPENAI_TEMPERATURE` | `0.1` | Temperature for AI responses |
```env
OPENAI_API_KEY=sk-your-openai-api-key
OPENAI_MODEL=gpt-3.5-turbo
OPENAI_EMBEDDING_MODEL=text-embedding-ada-002
OPENAI_MAX_TOKENS=1000
OPENAI_TEMPERATURE=0.1
```
### Redis Configuration (Optional)
| Variable | Default | Description |
| ---------------- | ----------- | ----------------------- |
| `REDIS_URL` | - | Redis connection string |
| `REDIS_HOST` | `localhost` | Redis host |
| `REDIS_PORT` | `6379` | Redis port |
| `REDIS_PASSWORD` | - | Redis password |
| `REDIS_DB` | `0` | Redis database number |
| `REDIS_PREFIX` | `mcphub:` | Key prefix for Redis |
```env
# Option 1: Full connection string
REDIS_URL=redis://username:password@localhost:6379/0
# Option 2: Individual components
REDIS_HOST=localhost
REDIS_PORT=6379
REDIS_PASSWORD=your-redis-password
REDIS_DB=0
REDIS_PREFIX=mcphub:
```
## MCP Server Configuration
### Default Settings
| Variable | Default | Description |
| ------------------- | ------------------- | -------------------------------------------- |
| `MCP_SETTINGS_FILE` | `mcp_settings.json` | Path to MCP settings file |
| `MCP_SERVERS_FILE` | `servers.json` | Path to servers configuration |
| `MCP_TIMEOUT` | `30000` | Default timeout for MCP operations (ms) |
| `MCP_MAX_RETRIES` | `3` | Maximum retry attempts for failed operations |
| `MCP_RESTART_DELAY` | `5000` | Delay before restarting failed servers (ms) |
```env
MCP_SETTINGS_FILE=./config/mcp_settings.json
MCP_SERVERS_FILE=./config/servers.json
MCP_TIMEOUT=30000
MCP_MAX_RETRIES=3
MCP_RESTART_DELAY=5000
```
### Smart Routing
| Variable | Default | Description |
| --------------------------- | ------- | -------------------------------- |
| `SMART_ROUTING_ENABLED` | `true` | Enable AI-powered smart routing |
| `SMART_ROUTING_THRESHOLD` | `0.7` | Similarity threshold for routing |
| `SMART_ROUTING_MAX_RESULTS` | `5` | Maximum tools to return |
| `VECTOR_CACHE_TTL` | `3600` | Vector cache TTL in seconds |
```env
SMART_ROUTING_ENABLED=true
SMART_ROUTING_THRESHOLD=0.7
SMART_ROUTING_MAX_RESULTS=5
VECTOR_CACHE_TTL=3600
```
## File Storage & Uploads
| Variable | Default | Description |
| -------------------- | ---------------- | ----------------------------------- |
| `UPLOAD_DIR` | `./uploads` | Directory for file uploads |
| `MAX_FILE_SIZE` | `10485760` | Maximum file size in bytes (10MB) |
| `ALLOWED_FILE_TYPES` | `image/*,text/*` | Allowed MIME types |
| `STORAGE_TYPE` | `local` | Storage type (`local`, `s3`, `gcs`) |
```env
UPLOAD_DIR=./data/uploads
MAX_FILE_SIZE=10485760
ALLOWED_FILE_TYPES=image/*,text/*,application/json
STORAGE_TYPE=local
```
### S3 Storage (Optional)
| Variable | Default | Description |
| ---------------------- | ----------- | ------------------ |
| `S3_BUCKET` | - | S3 bucket name |
| `S3_REGION` | `us-east-1` | S3 region |
| `S3_ACCESS_KEY_ID` | - | S3 access key |
| `S3_SECRET_ACCESS_KEY` | - | S3 secret key |
| `S3_ENDPOINT` | - | Custom S3 endpoint |
```env
S3_BUCKET=mcphub-uploads
S3_REGION=us-east-1
S3_ACCESS_KEY_ID=your-access-key
S3_SECRET_ACCESS_KEY=your-secret-key
```
## Monitoring & Logging
### Application Monitoring
| Variable | Default | Description |
| ------------------------ | ------- | ----------------------------- |
| `METRICS_ENABLED` | `true` | Enable metrics collection |
| `METRICS_PORT` | `9090` | Port for metrics endpoint |
| `HEALTH_CHECK_INTERVAL` | `30000` | Health check interval (ms) |
| `PERFORMANCE_MONITORING` | `false` | Enable performance monitoring |
```env
METRICS_ENABLED=true
METRICS_PORT=9090
HEALTH_CHECK_INTERVAL=30000
PERFORMANCE_MONITORING=true
```
### Logging Configuration
| Variable | Default | Description |
| ------------------ | ------------ | --------------------------------------- |
| `LOG_FORMAT` | `json` | Log format (`json`, `text`) |
| `LOG_FILE` | - | Log file path (if file logging enabled) |
| `LOG_MAX_SIZE` | `10m` | Maximum log file size |
| `LOG_MAX_FILES` | `5` | Maximum number of log files |
| `LOG_DATE_PATTERN` | `YYYY-MM-DD` | Date pattern for log rotation |
```env
LOG_FORMAT=json
LOG_FILE=./logs/mcphub.log
LOG_MAX_SIZE=10m
LOG_MAX_FILES=5
LOG_DATE_PATTERN=YYYY-MM-DD
```
## Development & Debug
| Variable | Default | Description |
| ------------------------ | ------- | ----------------------------------- |
| `DEBUG` | - | Debug namespaces (e.g., `mcphub:*`) |
| `DEV_TOOLS_ENABLED` | `false` | Enable development tools |
| `HOT_RELOAD` | `true` | Enable hot reload in development |
| `MOCK_EXTERNAL_SERVICES` | `false` | Mock external API calls |
```env
DEBUG=mcphub:*
DEV_TOOLS_ENABLED=true
HOT_RELOAD=true
MOCK_EXTERNAL_SERVICES=false
```
## Production Optimization
| Variable | Default | Description |
| ------------------ | ------- | -------------------------------------- |
| `CLUSTER_MODE` | `false` | Enable cluster mode |
| `WORKER_PROCESSES` | `0` | Number of worker processes (0 = auto) |
| `MEMORY_LIMIT` | - | Memory limit per process |
| `CPU_LIMIT` | - | CPU limit per process |
| `GC_OPTIMIZE` | `false` | Enable garbage collection optimization |
```env
CLUSTER_MODE=true
WORKER_PROCESSES=4
MEMORY_LIMIT=512M
GC_OPTIMIZE=true
```
## Configuration Examples
@@ -276,22 +49,9 @@ GC_OPTIMIZE=true
# .env.development
NODE_ENV=development
PORT=3000
LOG_LEVEL=debug
# Database
DATABASE_URL=postgresql://mcphub:password@localhost:5432/mcphub_dev
# Auth
JWT_SECRET=dev-secret-key
JWT_EXPIRES_IN=24h
# OpenAI (optional for development)
# OPENAI_API_KEY=your-dev-key
# Debug
DEBUG=mcphub:*
DEV_TOOLS_ENABLED=true
HOT_RELOAD=true
```
### Production Environment
@@ -300,30 +60,9 @@ HOT_RELOAD=true
# .env.production
NODE_ENV=production
PORT=3000
LOG_LEVEL=info
LOG_FORMAT=json
# Database
DATABASE_URL=postgresql://mcphub:secure-password@db.example.com:5432/mcphub
DB_SSL=true
DB_POOL_MAX=20
# Security
JWT_SECRET=your-super-secure-production-secret
SESSION_SECRET=your-session-secret
BCRYPT_ROUNDS=14
# External Services
OPENAI_API_KEY=your-production-openai-key
REDIS_URL=redis://redis.example.com:6379
# Monitoring
METRICS_ENABLED=true
PERFORMANCE_MONITORING=true
# Optimization
CLUSTER_MODE=true
GC_OPTIMIZE=true
```
### Docker Environment
@@ -331,21 +70,10 @@ GC_OPTIMIZE=true
```env
# .env.docker
NODE_ENV=production
HOST=0.0.0.0
PORT=3000
# Use service names for Docker networking
DATABASE_URL=postgresql://mcphub:password@postgres:5432/mcphub
REDIS_URL=redis://redis:6379
# Security
JWT_SECRET_FILE=/run/secrets/jwt_secret
DB_PASSWORD_FILE=/run/secrets/db_password
# File paths in container
MCP_SETTINGS_FILE=/app/mcp_settings.json
UPLOAD_DIR=/app/data/uploads
LOG_FILE=/app/logs/mcphub.log
```
## Environment Variable Loading
@@ -364,7 +92,6 @@ MCPHub supports variable expansion:
```env
BASE_URL=https://api.example.com
API_ENDPOINT=${BASE_URL}/v1
DATABASE_URL=postgresql://${DB_USER}:${DB_PASSWORD}@${DB_HOST}:${DB_PORT}/${DB_NAME}
```
## Security Best Practices
@@ -375,15 +102,3 @@ DATABASE_URL=postgresql://${DB_USER}:${DB_PASSWORD}@${DB_HOST}:${DB_PORT}/${DB_N
4. **Use environment-specific files**
5. **Validate all environment variables** at startup
6. **Use Docker secrets** for container deployments
## Validation
MCPHub validates environment variables at startup. Invalid configurations will prevent the application from starting with helpful error messages.
Required variables for production:
- `JWT_SECRET`
- `DATABASE_URL` or individual DB components
- `OPENAI_API_KEY` (if smart routing is enabled)
This comprehensive environment configuration ensures MCPHub can be properly configured for any deployment scenario.

View File

@@ -27,10 +27,7 @@ MCPHub uses several configuration files:
"args": ["arg1", "arg2"],
"env": {
"ENV_VAR": "value"
},
"cwd": "/working/directory",
"timeout": 30000,
"restart": true
}
}
}
}
@@ -50,8 +47,7 @@ MCPHub uses several configuration files:
},
"playwright": {
"command": "npx",
"args": ["@playwright/mcp@latest", "--headless"],
"timeout": 60000
"args": ["@playwright/mcp@latest", "--headless"]
},
"slack": {
"command": "npx",
@@ -79,12 +75,6 @@ MCPHub uses several configuration files:
| Field | Type | Default | Description |
| -------------- | ------- | --------------- | --------------------------- |
| `env` | object | `{}` | Environment variables |
| `cwd` | string | `process.cwd()` | Working directory |
| `timeout` | number | `30000` | Startup timeout (ms) |
| `restart` | boolean | `true` | Auto-restart on failure |
| `maxRestarts` | number | `5` | Maximum restart attempts |
| `restartDelay` | number | `5000` | Delay between restarts (ms) |
| `stdio` | string | `pipe` | stdio configuration |
## Common MCP Server Examples
@@ -262,42 +252,14 @@ MCPHub supports environment variable substitution using `${VAR_NAME}` syntax:
"args": ["-m", "api_server"],
"env": {
"API_KEY": "${API_KEY}",
"API_URL": "${API_BASE_URL}/v1",
"DEBUG": "${NODE_ENV:development}"
"API_URL": "${API_BASE_URL}/v1"
}
}
}
}
```
Default values can be specified with `${VAR_NAME:default}`:
```json
{
"timeout": "${MCP_TIMEOUT:30000}",
"maxRestarts": "${MCP_MAX_RESTARTS:5}"
}
```
### Conditional Configuration
Use different configurations based on environment:
```json
{
"mcpServers": {
"database": {
"command": "python",
"args": ["-m", "db_server"],
"env": {
"DB_URL": "${NODE_ENV:development == 'production' ? DATABASE_URL : DEV_DATABASE_URL}"
}
}
}
}
```
### Custom Server Scripts
{/* ### Custom Server Scripts
#### Local Python Server
@@ -373,7 +335,7 @@ Complement `mcp_settings.json` with server metadata:
}
}
}
```
``` */}
## Group Management
@@ -385,25 +347,18 @@ Complement `mcp_settings.json` with server metadata:
"production": {
"name": "Production Tools",
"description": "Stable production servers",
"servers": ["fetch", "slack", "github"],
"access": "authenticated",
"rateLimit": {
"requestsPerMinute": 100,
"burstLimit": 20
}
"servers": ["fetch", "slack", "github"]
},
"experimental": {
"name": "Experimental Features",
"description": "Beta and experimental servers",
"servers": ["experimental-ai", "beta-search"],
"access": "admin",
"enabled": false
"servers": ["experimental-ai", "beta-search"]
}
}
}
```
### Access Control
{/* ### Access Control
| Access Level | Description |
| --------------- | -------------------------- |
@@ -422,9 +377,9 @@ MCPHub supports hot reloading of configurations:
# Reload configurations without restart
curl -X POST http://localhost:3000/api/admin/reload-config \
-H "Authorization: Bearer your-admin-token"
```
``` */}
### Configuration Validation
{/* ### Configuration Validation
MCPHub validates configurations on startup and reload:
@@ -436,7 +391,7 @@ MCPHub validates configurations on startup and reload:
"requireDocumentation": true
}
}
```
``` */}
## Best Practices
@@ -453,7 +408,7 @@ MCPHub validates configurations on startup and reload:
}
```
2. **Limit server permissions**:
{/* 2. **Limit server permissions**:
```json
{
"filesystem": {
@@ -464,9 +419,9 @@ MCPHub validates configurations on startup and reload:
}
}
}
```
``` */}
### Performance
{/* ### Performance
1. **Set appropriate timeouts**:
@@ -486,9 +441,9 @@ MCPHub validates configurations on startup and reload:
"MEMORY_LIMIT": "512MB"
}
}
```
``` */}
### Monitoring
{/* ### Monitoring
1. **Enable health checks**:
@@ -510,9 +465,9 @@ MCPHub validates configurations on startup and reload:
"LOG_FORMAT": "json"
}
}
```
``` */}
## Troubleshooting
{/* ## Troubleshooting
### Common Issues
@@ -521,9 +476,9 @@ MCPHub validates configurations on startup and reload:
```bash
# Test command manually
uvx mcp-server-fetch
```
``` */}
**Environment variables not found**: Verify `.env` file
{/* **Environment variables not found**: Verify `.env` file
```bash
# Check environment
@@ -535,9 +490,9 @@ printenv | grep API_KEY
```bash
# Verify executable permissions
ls -la /path/to/server
```
``` */}
### Debug Configuration
{/* ### Debug Configuration
Enable debug mode for detailed logging:
@@ -550,8 +505,8 @@ Enable debug mode for detailed logging:
"logStartup": true
}
}
```
``` */}
{/*
### Validation Errors
Common validation errors and solutions:
@@ -559,6 +514,6 @@ Common validation errors and solutions:
1. **Missing required fields**: Add `command` and `args`
2. **Invalid timeout**: Use number, not string
3. **Environment variable not found**: Check `.env` file
4. **Command not found**: Verify installation and PATH
4. **Command not found**: Verify installation and PATH */}
This comprehensive guide covers all aspects of configuring MCP servers in MCPHub for various use cases and environments.

View File

@@ -0,0 +1,210 @@
# MCPHub DAO Layer 实现总结
## 项目概述
本次开发为MCPHub项目引入了独立的数据访问对象(DAO)层,用于管理`mcp_settings.json`中的不同类型数据的增删改查操作。
## 已实现的功能
### 1. 核心DAO层架构
#### 基础架构
- **BaseDao.ts**: 定义了通用的CRUD接口和抽象实现
- **JsonFileBaseDao.ts**: 提供JSON文件操作的基础类包含缓存机制
- **DaoFactory.ts**: 工厂模式实现提供DAO实例的创建和管理
#### 具体DAO实现
1. **UserDao**: 用户数据管理
- 用户创建(含密码哈希)
- 密码验证
- 权限管理
- 管理员查询
2. **ServerDao**: 服务器配置管理
- 服务器CRUD操作
- 按所有者/类型/状态查询
- 工具和提示配置管理
- 启用/禁用控制
3. **GroupDao**: 群组管理
- 群组CRUD操作
- 服务器成员管理
- 按所有者查询
- 群组-服务器关系管理
4. **SystemConfigDao**: 系统配置管理
- 系统级配置的读取和更新
- 分段配置管理
- 配置重置功能
5. **UserConfigDao**: 用户个人配置管理
- 用户个人配置的CRUD操作
- 分段配置管理
- 批量配置查询
### 2. 配置服务集成
#### DaoConfigService
- 使用DAO层重新实现配置加载和保存
- 支持用户权限过滤
- 提供配置合并和验证功能
#### ConfigManager
- 双模式支持:传统文件方式 + 新DAO层
- 运行时切换机制
- 环境变量控制 (`USE_DAO_LAYER`)
- 迁移工具集成
### 3. 迁移和验证工具
#### 迁移功能
- 从传统JSON文件格式迁移到DAO层
- 数据完整性验证
- 性能对比分析
- 迁移报告生成
#### 测试工具
- DAO操作完整性测试
- 示例数据生成和清理
- 性能基准测试
## 文件结构
```
src/
├── dao/ # DAO层核心
│ ├── base/
│ │ ├── BaseDao.ts # 基础DAO接口
│ │ └── JsonFileBaseDao.ts # JSON文件基础类
│ ├── UserDao.ts # 用户数据访问
│ ├── ServerDao.ts # 服务器配置访问
│ ├── GroupDao.ts # 群组数据访问
│ ├── SystemConfigDao.ts # 系统配置访问
│ ├── UserConfigDao.ts # 用户配置访问
│ ├── DaoFactory.ts # DAO工厂
│ ├── examples.ts # 使用示例
│ └── index.ts # 统一导出
├── config/
│ ├── DaoConfigService.ts # DAO配置服务
│ ├── configManager.ts # 配置管理器
│ └── migrationUtils.ts # 迁移工具
├── scripts/
│ └── dao-demo.ts # 演示脚本
└── docs/
└── dao-layer.md # 详细文档
```
## 主要特性
### 1. 类型安全
- 完整的TypeScript类型定义
- 编译时类型检查
- 接口约束和验证
### 2. 模块化设计
- 每种数据类型独立的DAO
- 清晰的关注点分离
- 可插拔的实现方式
### 3. 缓存机制
- JSON文件读取缓存
- 文件修改时间检测
- 缓存失效和刷新
### 4. 向后兼容
- 保持现有API不变
- 支持传统和DAO双模式
- 平滑迁移路径
### 5. 未来扩展性
- 数据库切换准备
- 新数据类型支持
- 复杂查询能力
## 使用方法
### 启用DAO层
```bash
# 环境变量配置
export USE_DAO_LAYER=true
```
### 基本操作示例
```typescript
import { getUserDao, getServerDao } from './dao/index.js';
// 用户操作
const userDao = getUserDao();
await userDao.createWithHashedPassword('admin', 'password', true);
const user = await userDao.findByUsername('admin');
// 服务器操作
const serverDao = getServerDao();
await serverDao.create({
name: 'my-server',
command: 'node',
args: ['server.js']
});
```
### 迁移操作
```typescript
import { migrateToDao, validateMigration } from './config/configManager.js';
// 执行迁移
await migrateToDao();
// 验证迁移
await validateMigration();
```
## 依赖包
新增的依赖包:
- `bcrypt`: 用户密码哈希
- `@types/bcrypt`: bcrypt类型定义
- `uuid`: UUID生成群组ID
- `@types/uuid`: uuid类型定义
## 测试状态
**编译测试**: 项目成功编译无TypeScript错误
**类型检查**: 所有类型定义正确
**依赖安装**: 必要依赖包已安装
**运行时测试**: 需要在实际环境中测试
**迁移测试**: 需要使用真实数据测试迁移
## 下一步计划
### 短期目标
1. 在开发环境中测试DAO层功能
2. 完善错误处理和边界情况
3. 添加更多单元测试
4. 性能优化和监控
### 中期目标
1. 集成到现有业务逻辑中
2. 提供Web界面的DAO层管理
3. 添加数据备份和恢复功能
4. 实现配置版本控制
### 长期目标
1. 实现数据库后端支持
2. 添加分布式配置管理
3. 实现实时配置同步
4. 支持配置审计和日志
## 优势总结
通过引入DAO层MCPHub获得了以下优势
1. **🏗️ 架构清晰**: 数据访问逻辑与业务逻辑分离
2. **🔄 易于扩展**: 为未来数据库支持做好准备
3. **🧪 便于测试**: 接口可以轻松模拟和单元测试
4. **🔒 类型安全**: 完整的TypeScript类型支持
5. **⚡ 性能优化**: 内置缓存和批量操作
6. **🛡️ 数据完整性**: 强制数据验证和约束
7. **📦 模块化**: 每种数据类型独立管理
8. **🔧 可维护性**: 代码结构清晰,易于维护
这个DAO层的实现为MCPHub项目提供了坚实的数据管理基础支持项目的长期发展和扩展需求。

254
docs/dao-layer.md Normal file
View File

@@ -0,0 +1,254 @@
# MCPHub DAO Layer 设计文档
## 概述
MCPHub的数据访问对象(DAO)层为项目中`mcp_settings.json`文件中的不同数据类型提供了统一的增删改查操作接口。这个设计使得未来从JSON文件存储切换到数据库存储变得更加容易。
## 架构设计
### 核心组件
```
src/dao/
├── base/
│ ├── BaseDao.ts # 基础DAO接口和抽象实现
│ └── JsonFileBaseDao.ts # JSON文件操作的基础类
├── UserDao.ts # 用户数据访问对象
├── ServerDao.ts # 服务器配置数据访问对象
├── GroupDao.ts # 群组数据访问对象
├── SystemConfigDao.ts # 系统配置数据访问对象
├── UserConfigDao.ts # 用户配置数据访问对象
├── DaoFactory.ts # DAO工厂类
├── examples.ts # 使用示例
└── index.ts # 统一导出
```
### 数据类型映射
| 数据类型 | 原始位置 | DAO类 | 主要功能 |
|---------|---------|-------|---------|
| IUser | `settings.users[]` | UserDao | 用户管理、密码验证、权限控制 |
| ServerConfig | `settings.mcpServers{}` | ServerDao | 服务器配置、启用/禁用、工具管理 |
| IGroup | `settings.groups[]` | GroupDao | 群组管理、服务器分组、成员管理 |
| SystemConfig | `settings.systemConfig` | SystemConfigDao | 系统级配置、路由设置、安装配置 |
| UserConfig | `settings.userConfigs{}` | UserConfigDao | 用户个人配置 |
## 主要特性
### 1. 统一的CRUD接口
所有DAO都实现了基础的CRUD操作
```typescript
interface BaseDao<T, K = string> {
findAll(): Promise<T[]>;
findById(id: K): Promise<T | null>;
create(entity: Omit<T, 'id'>): Promise<T>;
update(id: K, entity: Partial<T>): Promise<T | null>;
delete(id: K): Promise<boolean>;
exists(id: K): Promise<boolean>;
count(): Promise<number>;
}
```
### 2. 特定业务操作
每个DAO还提供了针对其数据类型的特定操作
#### UserDao 特殊功能
- `createWithHashedPassword()` - 创建用户时自动哈希密码
- `validateCredentials()` - 验证用户凭据
- `updatePassword()` - 更新用户密码
- `findAdmins()` - 查找管理员用户
#### ServerDao 特殊功能
- `findByOwner()` - 按所有者查找服务器
- `findEnabled()` - 查找启用的服务器
- `findByType()` - 按类型查找服务器
- `setEnabled()` - 启用/禁用服务器
- `updateTools()` - 更新服务器工具配置
#### GroupDao 特殊功能
- `findByOwner()` - 按所有者查找群组
- `findByServer()` - 查找包含特定服务器的群组
- `addServerToGroup()` - 向群组添加服务器
- `removeServerFromGroup()` - 从群组移除服务器
- `findByName()` - 按名称查找群组
### 3. 配置管理特殊功能
#### SystemConfigDao
- `getSection()` - 获取特定配置段
- `updateSection()` - 更新特定配置段
- `reset()` - 重置为默认配置
#### UserConfigDao
- `getSection()` - 获取用户特定配置段
- `updateSection()` - 更新用户特定配置段
- `getAll()` - 获取所有用户配置
## 使用方法
### 1. 基本使用
```typescript
import { getUserDao, getServerDao, getGroupDao } from './dao/index.js';
// 用户操作
const userDao = getUserDao();
const newUser = await userDao.createWithHashedPassword('username', 'password', false);
const user = await userDao.findByUsername('username');
const isValid = await userDao.validateCredentials('username', 'password');
// 服务器操作
const serverDao = getServerDao();
const server = await serverDao.create({
name: 'my-server',
command: 'node',
args: ['server.js'],
enabled: true
});
// 群组操作
const groupDao = getGroupDao();
const group = await groupDao.create({
name: 'my-group',
description: 'Test group',
servers: ['my-server']
});
```
### 2. 配置服务集成
```typescript
import { DaoConfigService, createDaoConfigService } from './config/DaoConfigService.js';
const daoService = createDaoConfigService();
// 加载完整配置
const settings = await daoService.loadSettings();
// 保存配置
await daoService.saveSettings(updatedSettings);
```
### 3. 迁移管理
```typescript
import { migrateToDao, switchToDao, switchToLegacy } from './config/configManager.js';
// 迁移到DAO层
const success = await migrateToDao();
// 运行时切换
switchToDao(); // 切换到DAO层
switchToLegacy(); // 切换回传统方式
```
## 配置选项
可以通过环境变量控制使用哪种数据访问方式:
```bash
# 使用DAO层 (推荐)
USE_DAO_LAYER=true
# 使用传统文件方式 (默认,向后兼容)
USE_DAO_LAYER=false
```
## 未来扩展
### 数据库支持
DAO层的设计使得切换到数据库变得容易只需要
1. 实现新的DAO实现类如DatabaseUserDao
2. 创建新的DaoFactory
3. 更新配置以使用新的工厂
```typescript
// 未来的数据库实现示例
class DatabaseUserDao implements UserDao {
constructor(private db: Database) {}
async findAll(): Promise<IUser[]> {
return this.db.query('SELECT * FROM users');
}
// ... 其他方法
}
```
### 新数据类型
添加新数据类型只需要:
1. 定义数据接口
2. 创建对应的DAO接口和实现
3. 更新DaoFactory
4. 更新配置服务
## 迁移指南
### 从传统方式迁移到DAO层
1. **备份数据**
```bash
cp mcp_settings.json mcp_settings.json.backup
```
2. **运行迁移**
```typescript
import { performMigration } from './config/migrationUtils.js';
await performMigration();
```
3. **验证迁移**
```typescript
import { validateMigration } from './config/migrationUtils.js';
const isValid = await validateMigration();
```
4. **切换到DAO层**
```bash
export USE_DAO_LAYER=true
```
### 性能对比
可以使用内置工具对比性能:
```typescript
import { performanceComparison } from './config/migrationUtils.js';
await performanceComparison();
```
## 最佳实践
1. **类型安全**: 始终使用TypeScript接口确保类型安全
2. **错误处理**: 在DAO操作周围实现适当的错误处理
3. **事务**: 对于复杂操作,考虑使用事务(未来数据库实现)
4. **缓存**: DAO层包含内置缓存机制
5. **测试**: 使用DAO接口进行单元测试的模拟
## 示例代码
查看以下文件获取完整示例:
- `src/dao/examples.ts` - 基本DAO操作示例
- `src/config/migrationUtils.ts` - 迁移和验证工具
- `src/scripts/dao-demo.ts` - 交互式演示脚本
## 总结
DAO层为MCPHub提供了
- 🏗️ **模块化设计**: 每种数据类型都有专门的访问层
- 🔄 **易于迁移**: 为未来切换到数据库做好准备
- 🧪 **可测试性**: 接口可以轻松模拟和测试
- 🔒 **类型安全**: 完整的TypeScript类型支持
-**性能优化**: 内置缓存和批量操作支持
- 🛡️ **数据完整性**: 强制数据验证和约束
通过引入DAO层MCPHub的数据管理变得更加结构化、可维护和可扩展。

View File

@@ -28,8 +28,7 @@
"features/server-management",
"features/group-management",
"features/smart-routing",
"features/authentication",
"features/monitoring"
"features/oauth"
]
},
{
@@ -40,14 +39,6 @@
"configuration/docker-setup",
"configuration/nginx"
]
},
{
"group": "Development",
"pages": [
"development/getting-started",
"development/architecture",
"development/contributing"
]
}
]
},
@@ -68,8 +59,7 @@
"zh/features/server-management",
"zh/features/group-management",
"zh/features/smart-routing",
"zh/features/authentication",
"zh/features/monitoring"
"zh/features/oauth"
]
},
{
@@ -80,19 +70,11 @@
"zh/configuration/docker-setup",
"zh/configuration/nginx"
]
},
{
"group": "开发指南",
"pages": [
"zh/development/getting-started",
"zh/development/architecture",
"zh/development/contributing"
]
}
]
},
{
"tab": "API Reference",
"tab": "API",
"groups": [
{
"group": "MCP Endpoints",
@@ -104,7 +86,13 @@
]
},
{
"group": "Management API",
"group": "OpenAPI Endpoints",
"pages": [
"api-reference/openapi"
]
},
{
"group": "Management Endpoints",
"pages": [
"api-reference/servers",
"api-reference/groups",
@@ -114,26 +102,40 @@
]
}
]
},
{
"tab": "接口",
"groups": [
{
"group": "MCP 端点",
"pages": [
"zh/api-reference/introduction",
"zh/api-reference/mcp-http",
"zh/api-reference/mcp-sse",
"zh/api-reference/smart-routing"
]
},
{
"group": "OpenAPI 端点",
"pages": [
"zh/api-reference/openapi"
]
},
{
"group": "管理端点",
"pages": [
"zh/api-reference/servers",
"zh/api-reference/groups",
"zh/api-reference/auth",
"zh/api-reference/logs",
"zh/api-reference/config"
]
}
]
}
],
"global": {
"anchors": [
{
"anchor": "GitHub",
"href": "https://github.com/samanhappy/mcphub",
"icon": "github"
},
{
"anchor": "Discord",
"href": "https://discord.gg/qMKNsn5Q",
"icon": "discord"
},
{
"anchor": "Sponsor",
"href": "https://ko-fi.com/samanhappy",
"icon": "heart"
}
]
"anchors": []
}
},
"logo": {
@@ -144,13 +146,13 @@
"links": [
{
"label": "Demo",
"href": "http://localhost:3000"
"href": "https://demo.mcphubx.com"
}
],
"primary": {
"type": "button",
"label": "Get Started",
"href": "https://docs.hubmcp.dev/quickstart"
"href": "https://docs.mcphubx.com/quickstart"
}
},
"footer": {
@@ -159,4 +161,4 @@
"discord": "https://discord.gg/qMKNsn5Q"
}
}
}
}

View File

@@ -0,0 +1,267 @@
# Environment Variable Expansion in mcp_settings.json
## Overview
MCPHub now supports comprehensive environment variable expansion throughout the entire `mcp_settings.json` configuration file. This allows you to externalize sensitive information and configuration values, making your setup more secure and flexible.
## Supported Formats
MCPHub supports two environment variable formats:
1. **${VAR}** - Standard format (recommended)
2. **$VAR** - Unix-style format (variable name must start with an uppercase letter or underscore, followed by uppercase letters, numbers, or underscores)
## What Can Be Expanded
Environment variables can now be used in **ANY** string value throughout your configuration:
- Server URLs
- Commands and arguments
- Headers
- Environment variables passed to child processes
- OpenAPI specifications and security configurations
- OAuth credentials
- System configuration values
- Any other string fields
## Examples
### 1. SSE/HTTP Server Configuration
```json
{
"mcpServers": {
"my-api-server": {
"type": "sse",
"url": "${MCP_SERVER_URL}",
"headers": {
"Authorization": "Bearer ${API_TOKEN}",
"X-Custom-Header": "${CUSTOM_VALUE}"
}
}
}
}
```
Environment variables:
```bash
export MCP_SERVER_URL="https://api.example.com/mcp"
export API_TOKEN="secret-token-123"
export CUSTOM_VALUE="my-custom-value"
```
### 2. Stdio Server Configuration
```json
{
"mcpServers": {
"my-python-server": {
"type": "stdio",
"command": "${PYTHON_PATH}",
"args": ["-m", "${MODULE_NAME}", "--api-key", "${API_KEY}"],
"env": {
"DATABASE_URL": "${DATABASE_URL}",
"DEBUG": "${DEBUG_MODE}"
}
}
}
}
```
Environment variables:
```bash
export PYTHON_PATH="/usr/bin/python3"
export MODULE_NAME="my_mcp_server"
export API_KEY="secret-api-key"
export DATABASE_URL="postgresql://localhost/mydb"
export DEBUG_MODE="true"
```
### 3. OpenAPI Server Configuration
```json
{
"mcpServers": {
"openapi-service": {
"type": "openapi",
"openapi": {
"url": "${OPENAPI_SPEC_URL}",
"security": {
"type": "apiKey",
"apiKey": {
"name": "X-API-Key",
"in": "header",
"value": "${OPENAPI_API_KEY}"
}
}
}
}
}
}
```
Environment variables:
```bash
export OPENAPI_SPEC_URL="https://api.example.com/openapi.json"
export OPENAPI_API_KEY="your-api-key-here"
```
### 4. OAuth Configuration
```json
{
"mcpServers": {
"oauth-server": {
"type": "sse",
"url": "${OAUTH_SERVER_URL}",
"oauth": {
"clientId": "${OAUTH_CLIENT_ID}",
"clientSecret": "${OAUTH_CLIENT_SECRET}",
"accessToken": "${OAUTH_ACCESS_TOKEN}"
}
}
}
}
```
Environment variables:
```bash
export OAUTH_SERVER_URL="https://oauth.example.com/mcp"
export OAUTH_CLIENT_ID="my-client-id"
export OAUTH_CLIENT_SECRET="my-client-secret"
export OAUTH_ACCESS_TOKEN="my-access-token"
```
### 5. System Configuration
```json
{
"systemConfig": {
"install": {
"pythonIndexUrl": "${PYTHON_INDEX_URL}",
"npmRegistry": "${NPM_REGISTRY}"
},
"mcpRouter": {
"apiKey": "${MCPROUTER_API_KEY}",
"referer": "${MCPROUTER_REFERER}"
}
}
}
```
Environment variables:
```bash
export PYTHON_INDEX_URL="https://pypi.tuna.tsinghua.edu.cn/simple"
export NPM_REGISTRY="https://registry.npmmirror.com"
export MCPROUTER_API_KEY="router-api-key"
export MCPROUTER_REFERER="https://myapp.com"
```
## Complete Example
See [examples/mcp_settings_with_env_vars.json](../examples/mcp_settings_with_env_vars.json) for a comprehensive example configuration using environment variables.
## Best Practices
### Security
1. **Never commit sensitive values to version control** - Use environment variables for all secrets
2. **Use .env files for local development** - MCPHub automatically loads `.env` files
3. **Use secure secret management in production** - Consider using Docker secrets, Kubernetes secrets, or cloud provider secret managers
### Organization
1. **Group related variables** - Use prefixes for related configuration (e.g., `API_`, `DB_`, `OAUTH_`)
2. **Document required variables** - Maintain a list of required environment variables in your README
3. **Provide example .env file** - Create a `.env.example` file with placeholder values
### Example .env File
```bash
# Server Configuration
MCP_SERVER_URL=https://api.example.com/mcp
API_TOKEN=your-api-token-here
# Python Server
PYTHON_PATH=/usr/bin/python3
MODULE_NAME=my_mcp_server
# Database
DATABASE_URL=postgresql://localhost/mydb
# OpenAPI
OPENAPI_SPEC_URL=https://api.example.com/openapi.json
OPENAPI_API_KEY=your-openapi-key
# OAuth
OAUTH_CLIENT_ID=your-client-id
OAUTH_CLIENT_SECRET=your-client-secret
OAUTH_ACCESS_TOKEN=your-access-token
```
## Docker Usage
When using Docker, pass environment variables using `-e` flag or `--env-file`:
```bash
# Using individual variables
docker run -e API_TOKEN=secret -e SERVER_URL=https://api.example.com mcphub
# Using env file
docker run --env-file .env mcphub
```
Or in docker-compose.yml:
```yaml
version: '3.8'
services:
mcphub:
image: mcphub
env_file:
- .env
environment:
- MCP_SERVER_URL=${MCP_SERVER_URL}
- API_TOKEN=${API_TOKEN}
```
## Troubleshooting
### Variable Not Expanding
If a variable is not expanding:
1. Check that the variable is set: `echo $VAR_NAME`
2. Verify the variable name matches exactly (case-sensitive)
3. Ensure the variable is exported: `export VAR_NAME=value`
4. Restart MCPHub after setting environment variables
### Empty Values
If an environment variable is not set, it will be replaced with an empty string. Make sure all required variables are set before starting MCPHub.
### Nested Variables
Environment variables in nested objects and arrays are fully supported:
```json
{
"nested": {
"deep": {
"value": "${MY_VAR}"
}
},
"array": ["${VAR1}", "${VAR2}"]
}
```
## Migration from Previous Version
If you were previously using environment variables only in headers, no changes are needed. The new implementation is backward compatible and simply extends support to all configuration fields.
## Technical Details
- Environment variables are expanded once when the configuration is loaded
- Expansion is recursive and handles nested objects and arrays
- Non-string values (booleans, numbers, null) are preserved as-is
- Empty string is used when an environment variable is not set

View File

@@ -30,9 +30,6 @@ Groups are named collections of MCP servers that can be accessed through dedicat
3. **Fill Group Details**:
- **Name**: Unique identifier for the group
- **Display Name**: Human-readable name
- **Description**: Purpose and contents of the group
- **Access Level**: Public, Private, or Restricted
4. **Add Servers**: Select servers to include in the group
@@ -46,14 +43,11 @@ curl -X POST http://localhost:3000/api/groups \
-H "Authorization: Bearer YOUR_JWT_TOKEN" \
-d '{
"name": "web-automation",
"displayName": "Web Automation Tools",
"description": "Browser automation and web scraping tools",
"servers": ["playwright", "fetch"],
"accessLevel": "public"
"servers": ["playwright", "fetch"]
}'
```
### Via Configuration File
{/* ### Via Configuration File
Define groups in your `mcp_settings.json`:
@@ -66,20 +60,16 @@ Define groups in your `mcp_settings.json`:
},
"groups": {
"web-tools": {
"displayName": "Web Tools",
"description": "Web scraping and browser automation",
"name": "web",
"servers": ["fetch", "playwright"],
"accessLevel": "public"
},
"communication": {
"displayName": "Communication Tools",
"description": "Messaging and collaboration tools",
"name": "communication",
"servers": ["slack"],
"accessLevel": "private"
}
}
}
```
``` */}
## Group Types and Use Cases
@@ -177,7 +167,7 @@ Define groups in your `mcp_settings.json`:
</Accordion>
</AccordionGroup>
## Group Access Control
{/* ## Group Access Control
### Access Levels
@@ -254,7 +244,7 @@ curl -X DELETE http://localhost:3000/api/groups/web-tools/members/user123 \
# List group members
curl http://localhost:3000/api/groups/web-tools/members \
-H "Authorization: Bearer YOUR_JWT_TOKEN"
```
``` */}
## Group Endpoints
@@ -346,7 +336,7 @@ Response will only include tools from `fetch` and `playwright` servers.
</Tab>
</Tabs>
### Batch Server Updates
{/* ### Batch Server Updates
Update multiple servers at once:
@@ -357,9 +347,9 @@ curl -X PUT http://localhost:3000/api/groups/web-tools/servers \
-d '{
"servers": ["fetch", "playwright", "selenium"]
}'
```
``` */}
## Group Monitoring
{/* ## Group Monitoring
### Group Status
@@ -393,9 +383,9 @@ Metrics include:
- Request count by tool
- Response times
- Error rates
- User activity
- User activity */}
## Advanced Group Features
{/* ## Advanced Group Features
### Nested Groups
@@ -474,7 +464,7 @@ Define policies for group behavior:
}
}
}
```
``` */}
## Best Practices
@@ -494,7 +484,7 @@ Define policies for group behavior:
**Use Descriptive Names**: Choose names that clearly indicate the group's purpose and contents.
</Tip>
### Security Considerations
{/* ### Security Considerations
<Warning>
**Principle of Least Privilege**: Only give users access to groups they actually need.
@@ -507,7 +497,7 @@ Define policies for group behavior:
<Warning>
**Regular Access Reviews**: Periodically review group memberships and remove unnecessary access.
</Warning>
</Warning> */}
### Performance Optimization

141
docs/features/oauth.mdx Normal file
View File

@@ -0,0 +1,141 @@
# OAuth Support
## At a Glance
- Covers end-to-end OAuth 2.0 Authorization Code with PKCE for upstream MCP servers.
- Supports automatic discovery from `WWW-Authenticate` responses and RFC 8414 metadata.
- Implements dynamic client registration (RFC 7591) and resource indicators (RFC 8707).
- Persists client credentials and tokens to `mcp_settings.json` for reconnects.
## When MCPHub Switches to OAuth
1. MCPHub calls an MCP server that requires authorization and receives `401 Unauthorized`.
2. The response exposes a `WWW-Authenticate` header pointing to protected resource metadata (`authorization_server` or `as_uri`).
3. MCPHub discovers the authorization server metadata, registers (if needed), and opens the browser so the user can authorize once.
4. After the callback is handled, MCPHub reconnects with fresh tokens and resumes requests transparently.
> MCPHub logs each stage (discovery, registration, authorization URL, token exchange) in the server detail view and the backend logs.
## Quick Start by Server Type
### Servers with Dynamic Registration Support
Some servers expose complete OAuth metadata and allow dynamic client registration. For example, Vercel and Linear MCP servers only need their SSE endpoint configured:
```json
{
"mcpServers": {
"vercel": {
"type": "sse",
"url": "https://mcp.vercel.com"
},
"linear": {
"type": "sse",
"url": "https://mcp.linear.app/mcp"
}
}
}
```
- MCPHub discovers the authorization server, registers the client, and handles PKCE automatically.
- Tokens are stored in `mcp_settings.json`; no additional dashboard configuration is needed.
### Servers Requiring Manual Client Provisioning
Other providers do not support dynamic registration. GitHubs MCP endpoint (`https://api.githubcopilot.com/mcp/`) is one example. To connect:
1. Create an OAuth App in the providers console (for GitHub, go to **Settings → Developer settings → OAuth Apps**).
2. Set the callback/redirect URL to `http://localhost:3000/oauth/callback` (or your deployed dashboard domain).
3. Copy the issued client ID and client secret.
4. Supply the credentials through the MCPHub dashboard or by editing `mcp_settings.json` as shown below.
```json
{
"mcpServers": {
"github": {
"type": "sse",
"url": "https://api.githubcopilot.com/mcp/",
"oauth": {
"clientId": "${GITHUB_OAUTH_APP_ID}",
"clientSecret": "${GITHUB_OAUTH_APP_SECRET}",
"scopes": ["replace-with-provider-scope"],
"resource": "https://api.githubcopilot.com"
}
}
}
}
```
- MCPHub skips dynamic registration and uses the credentials you provide to complete the OAuth exchange.
- Update the dashboard or configuration file whenever you rotate secrets.
- Replace `scopes` with the exact scope strings required by the provider.
## Configuration Options
You can rely on auto-detection for most servers or declare OAuth settings explicitly in `mcp_settings.json`. Only populate the fields you need.
### Basic Auto Detection (Minimal Config)
```json
{
"mcpServers": {
"secured-sse": {
"type": "sse",
"url": "https://mcp.example.com/sse",
"oauth": {
"scopes": ["mcp.tools", "mcp.prompts"],
"resource": "https://mcp.example.com"
}
}
}
}
```
- MCPHub will discover the authorization server from challenge headers and walk the user through authorization automatically.
- Tokens (including refresh tokens) are stored on disk and reused on restart.
### Static Client Credentials (Bring Your Own Client)
```json
{
"oauth": {
"clientId": "mcphub-client",
"clientSecret": "replace-me-if-required",
"authorizationEndpoint": "https://auth.example.com/oauth/authorize",
"tokenEndpoint": "https://auth.example.com/oauth/token",
"redirectUri": "http://localhost:3000/oauth/callback"
}
}
```
- Use this when the authorization server requires manual client provisioning.
- `redirectUri` defaults to `http://localhost:3000/oauth/callback`; override it when running behind a custom domain.
### Dynamic Client Registration (RFC 7591)
```json
{
"oauth": {
"dynamicRegistration": {
"enabled": true,
"issuer": "https://auth.example.com",
"metadata": {
"client_name": "MCPHub",
"redirect_uris": [
"http://localhost:3000/oauth/callback",
"https://mcphub.example.com/oauth/callback"
],
"scope": "mcp.tools mcp.prompts",
"grant_types": ["authorization_code", "refresh_token"]
},
"initialAccessToken": "optional-token-if-required"
},
"scopes": ["mcp.tools", "mcp.prompts"],
"resource": "https://mcp.example.com"
}
}
```
- MCPHub discovers endpoints via `issuer`, registers itself, and persists the issued `client_id`/`client_secret`.
- Provide `initialAccessToken` only when the registration endpoint is protected.
## Authorization Flow
1. **Initialization** On startup MCPHub processes every server entry, discovers metadata, and registers the client if `dynamicRegistration.enabled` is true.
2. **User Authorization** Initiating a connection launches the system browser to the servers authorize page with PKCE parameters.
3. **Callback Handling** The built-in route (`/oauth/callback`) verifies the `state`, completes the token exchange, and saves the tokens via the MCP SDK.
4. **Token Lifecycle** Access and refresh tokens are cached in memory, refreshed automatically, and written back to `mcp_settings.json`.
## Tips & Troubleshooting
- Confirm that the redirect URI used during authorization exactly matches one of the `redirect_uris` registered with the authorization server.
- When running behind HTTPS, expose the callback URL publicly or configure a reverse proxy at `/oauth/callback`.
- If discovery fails, supply `authorizationEndpoint` and `tokenEndpoint` explicitly to bypass metadata lookup.
- Remove stale tokens from `mcp_settings.json` if an authorization server revokes access—MCPHub will prompt for a fresh login on the next request.

View File

@@ -311,7 +311,7 @@ Servers can use environment variables for configuration:
- `${VAR_NAME:-default}`: Uses default if variable not set
- `${VAR_NAME:+value}`: Uses value if variable is set
### Working Directory
{/* ### Working Directory
Set the working directory for server execution:
@@ -323,7 +323,7 @@ Set the working directory for server execution:
"cwd": "/path/to/server/directory"
}
}
```
``` */}
### Command Variations
@@ -352,7 +352,7 @@ Different ways to specify server commands:
```
</Tab>
<Tab title="Direct Python">
{/* <Tab title="Direct Python">
```json
{
"direct-python": {
@@ -373,7 +373,7 @@ Different ways to specify server commands:
}
}
```
</Tab>
</Tab> */}
</Tabs>
## Advanced Features
@@ -382,12 +382,12 @@ Different ways to specify server commands:
MCPHub supports hot reloading of server configurations:
1. **Config File Changes**: Automatically detects changes to `mcp_settings.json`
2. **Dashboard Updates**: Immediately applies changes made through the web interface
3. **API Updates**: Real-time updates via REST API calls
4. **Zero Downtime**: Graceful server restarts without affecting other servers
{/* 1. **Config File Changes**: Automatically detects changes to `mcp_settings.json` */}
1. **Dashboard Updates**: Immediately applies changes made through the web interface
2. **API Updates**: Real-time updates via REST API calls
3. **Zero Downtime**: Graceful server restarts without affecting other servers
### Resource Limits
{/* ### Resource Limits
Control server resource usage:
@@ -403,9 +403,9 @@ Control server resource usage:
}
}
}
```
``` */}
### Dependency Management
{/* ### Dependency Management
Handle server dependencies:
@@ -439,7 +439,7 @@ Handle server dependencies:
```
</Accordion>
</AccordionGroup>
</AccordionGroup> */}
## Troubleshooting

View File

@@ -55,7 +55,7 @@ Smart Routing requires additional setup compared to basic MCPHub usage:
2. **Embedding Service**: OpenAI API or compatible service
3. **Environment Configuration**: Proper configuration variables
### Quick Setup
{/* ### Quick Setup
<Tabs>
<Tab title="Docker Compose">
@@ -265,7 +265,7 @@ EMBEDDING_BATCH_SIZE=100
```
</Accordion>
</AccordionGroup>
</AccordionGroup> */}
## Using Smart Routing
@@ -276,18 +276,93 @@ Access Smart Routing through the special `$smart` endpoint:
<Tabs>
<Tab title="HTTP MCP">
```
# Search across all servers
http://localhost:3000/mcp/$smart
# Search within a specific group
http://localhost:3000/mcp/$smart/{group}
```
</Tab>
<Tab title="SSE (Legacy)">
```
# Search across all servers
http://localhost:3000/sse/$smart
# Search within a specific group
http://localhost:3000/sse/$smart/{group}
```
</Tab>
</Tabs>
### Basic Usage
### Group-Scoped Smart Routing
Smart Routing now supports group-scoped searches, allowing you to limit tool discovery to servers within a specific group:
<AccordionGroup>
<Accordion title="Using Group-Scoped Smart Routing">
Connect your AI client to a group-specific Smart Routing endpoint:
```
http://localhost:3000/mcp/$smart/production
```
This endpoint will only search for tools within servers that belong to the "production" group.
**Benefits:**
- **Focused Results**: Only tools from relevant servers are returned
- **Better Performance**: Reduced search space for faster queries
- **Environment Isolation**: Keep development, staging, and production tools separate
- **Access Control**: Limit tool discovery based on user permissions
</Accordion>
<Accordion title="Example: Environment-Based Groups">
Create groups for different environments:
```bash
# Development environment
http://localhost:3000/mcp/$smart/development
# Staging environment
http://localhost:3000/mcp/$smart/staging
# Production environment
http://localhost:3000/mcp/$smart/production
```
Each endpoint will only return tools from servers in that specific environment group.
</Accordion>
<Accordion title="Example: Team-Based Groups">
Organize tools by team or department:
```bash
# Backend team tools
http://localhost:3000/mcp/$smart/backend-team
# Frontend team tools
http://localhost:3000/mcp/$smart/frontend-team
# DevOps team tools
http://localhost:3000/mcp/$smart/devops-team
```
This enables teams to have focused access to their relevant toolsets.
</Accordion>
<Accordion title="How It Works">
When using `$smart/{group}`:
1. The system identifies the specified group
2. Retrieves all servers belonging to that group
3. Filters the tool search to only those servers
4. Returns results scoped to the group's servers
If the group doesn't exist or has no servers, the search will return no results.
</Accordion>
</AccordionGroup>
{/* ### Basic Usage
Connect your AI client to the Smart Routing endpoint and make natural language requests:
@@ -330,9 +405,9 @@ Response:
]
}
}
```
``` */}
### Advanced Queries
{/* ### Advanced Queries
Smart Routing supports various query types:
@@ -405,9 +480,9 @@ Smart Routing supports various query types:
}'
```
</Accordion>
</AccordionGroup>
</AccordionGroup> */}
### Tool Execution
{/* ### Tool Execution
Once Smart Routing finds relevant tools, you can execute them directly:
@@ -426,9 +501,9 @@ curl -X POST http://localhost:3000/mcp/$smart \
}
}
}'
```
``` */}
## Performance Optimization
{/* ## Performance Optimization
### Embedding Cache
@@ -585,7 +660,7 @@ curl -X POST http://localhost:3000/api/smart-routing/feedback \
"successful": true,
"comments": "Perfect tool for the task"
}'
```
``` */}
## Troubleshooting

View File

@@ -1,10 +1,10 @@
---
title: MCPHub Documentation
title: MCPHub
description: 'The Unified Hub for Model Context Protocol (MCP) Servers'
---
<img className="block dark:hidden" src="/images/hero-light.png" alt="Hero Light" />
<img className="hidden dark:block" src="/images/hero-dark.png" alt="Hero Dark" />
{/* <img className="block dark:hidden" src="/images/hero-light.png" alt="Hero Light" />
<img className="hidden dark:block" src="/images/hero-dark.png" alt="Hero Dark" /> */}
# Welcome to MCPHub
@@ -16,12 +16,12 @@ MCPHub makes it easy to manage and scale multiple MCP (Model Context Protocol) s
<Card title="Unified Management" icon="server" href="/features/server-management">
Centrally manage multiple MCP servers with hot-swappable configuration
</Card>
<Card title="Smart Routing" icon="route" href="/features/smart-routing">
AI-powered tool discovery using vector semantic search
</Card>
<Card title="Group Management" icon="users" href="/features/group-management">
Organize servers into logical groups for streamlined access control
</Card>
<Card title="Smart Routing" icon="route" href="/features/smart-routing">
AI-powered tool discovery using vector semantic search
</Card>
<Card title="Real-time Monitoring" icon="chart-line" href="/features/monitoring">
Monitor server status and performance from a unified dashboard
</Card>

View File

@@ -72,7 +72,6 @@ Optional for Smart Routing:
-p 3000:3000 \
-e PORT=3000 \
-e BASE_PATH="" \
-e REQUEST_TIMEOUT=60000 \
samanhappy/mcphub:latest
```
@@ -144,12 +143,9 @@ Optional for Smart Routing:
# Run with custom port
PORT=8080 mcphub
# Run with custom config path
MCP_SETTINGS_PATH=/path/to/mcp_settings.json mcphub
```
#### 3. Local Installation
{/* #### 3. Local Installation
You can also install MCPHub locally in a project:
@@ -170,8 +166,7 @@ Optional for Smart Routing:
# Run MCPHub
./start.sh
```
``` */}
</Tab>
<Tab title="Local Development">
@@ -419,7 +414,7 @@ Smart Routing provides AI-powered tool discovery using vector semantic search.
</Tab>
</Tabs>
### Environment Configuration
{/* ### Environment Configuration
Set the following environment variables:
@@ -435,13 +430,13 @@ EMBEDDING_MODEL=text-embedding-3-small
# Optional: Enable smart routing
ENABLE_SMART_ROUTING=true
```
``` */}
## Verification
After installation, verify MCPHub is working:
### 1. Health Check
{/* ### 1. Health Check
```bash
curl http://localhost:3000/api/health
@@ -455,9 +450,9 @@ Expected response:
"version": "x.x.x",
"uptime": 123
}
```
``` */}
### 2. Dashboard Access
### Dashboard Access
Open your browser and navigate to:
@@ -465,7 +460,7 @@ Open your browser and navigate to:
http://localhost:3000
```
### 3. API Test
{/* ### 3. API Test
```bash
curl -X POST http://localhost:3000/mcp \
@@ -476,7 +471,7 @@ curl -X POST http://localhost:3000/mcp \
"method": "tools/list",
"params": {}
}'
```
``` */}
## Troubleshooting

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -0,0 +1,209 @@
# OpenAPI Schema Support in MCPHub
MCPHub now supports both OpenAPI specification URLs and complete JSON schemas for OpenAPI server configuration. This allows you to either reference an external OpenAPI specification file or embed the complete schema directly in your configuration.
## Configuration Options
### 1. Using OpenAPI Specification URL (Traditional)
```json
{
"type": "openapi",
"openapi": {
"url": "https://api.example.com/openapi.json",
"version": "3.1.0",
"security": {
"type": "apiKey",
"apiKey": {
"name": "X-API-Key",
"in": "header",
"value": "your-api-key"
}
}
}
}
```
### 2. Using Complete JSON Schema (New)
```json
{
"type": "openapi",
"openapi": {
"schema": {
"openapi": "3.1.0",
"info": {
"title": "My API",
"version": "1.0.0"
},
"servers": [
{
"url": "https://api.example.com"
}
],
"paths": {
"/users": {
"get": {
"operationId": "getUsers",
"summary": "Get all users",
"responses": {
"200": {
"description": "List of users"
}
}
}
}
}
},
"version": "3.1.0",
"security": {
"type": "apiKey",
"apiKey": {
"name": "X-API-Key",
"in": "header",
"value": "your-api-key"
}
}
}
}
```
## Benefits of JSON Schema Support
1. **Offline Development**: No need for external URLs during development
2. **Version Control**: Schema changes can be tracked in your configuration
3. **Security**: No external dependencies or network calls required
4. **Customization**: Full control over the API specification
5. **Testing**: Easy to create test configurations with mock schemas
## Frontend Form Support
The web interface now includes:
- **Input Mode Selection**: Choose between "Specification URL" or "JSON Schema"
- **URL Input**: Traditional URL input field for external specifications
- **Schema Editor**: Large text area with syntax highlighting for JSON schema input
- **Validation**: Client-side JSON validation before submission
- **Help Text**: Contextual help for schema format
## API Validation
The backend validates that:
- At least one of `url` or `schema` is provided for OpenAPI servers
- JSON schemas are properly formatted when provided
- Security configurations are valid for both input modes
- All required OpenAPI fields are present
## Migration Guide
### From URL to Schema
If you want to convert an existing URL-based configuration to schema-based:
1. Download your OpenAPI specification from the URL
2. Copy the JSON content
3. Update your configuration to use the `schema` field instead of `url`
4. Paste the JSON content as the value of the `schema` field
### Maintaining Both
You can include both `url` and `schema` in your configuration. The system will prioritize the `schema` field if both are present.
## Examples
See the `examples/openapi-schema-config.json` file for complete configuration examples showing both URL and schema-based configurations.
## Technical Implementation
- **Backend**: OpenAPI client supports both SwaggerParser.dereference() with URLs and direct schema objects
- **Frontend**: Dynamic form rendering based on selected input mode
- **Validation**: Enhanced validation logic in server controllers
- **Type Safety**: Updated TypeScript interfaces for both input modes
## Header Passthrough Support
MCPHub supports passing through specific headers from tool call requests to upstream OpenAPI endpoints. This is useful for authentication tokens, API keys, and other request-specific headers.
### Configuration
Add `passthroughHeaders` to your OpenAPI configuration:
```json
{
"type": "openapi",
"openapi": {
"url": "https://api.example.com/openapi.json",
"version": "3.1.0",
"passthroughHeaders": ["Authorization", "X-API-Key", "X-Custom-Header"],
"security": {
"type": "apiKey",
"apiKey": {
"name": "X-API-Key",
"in": "header",
"value": "your-api-key"
}
}
}
}
```
### How It Works
1. **Configuration**: List header names in the `passthroughHeaders` array
2. **Tool Calls**: When calling tools via HTTP API, include headers in the request
3. **Passthrough**: Only configured headers are forwarded to the upstream API
4. **Case Insensitive**: Header matching is case-insensitive for flexibility
### Example Usage
```bash
# Call an OpenAPI tool with passthrough headers
curl -X POST "http://localhost:3000/api/tools/myapi/createUser" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer your-token" \
-H "X-API-Key: your-api-key" \
-H "X-Custom-Header: custom-value" \
-d '{"name": "John Doe", "email": "john@example.com"}'
```
In this example:
- If `passthroughHeaders` includes `["Authorization", "X-API-Key"]`
- Only `Authorization` and `X-API-Key` headers will be forwarded
- `X-Custom-Header` will be ignored (not in passthrough list)
- `Content-Type` is handled by the OpenAPI operation definition
### Security Considerations
- **Whitelist Only**: Only explicitly configured headers are passed through
- **Sensitive Data**: Be careful with headers containing sensitive information
- **Validation**: Upstream APIs should validate all received headers
- **Logging**: Headers may appear in logs - consider this for sensitive data
## Security Considerations
When using JSON schemas:
- Ensure schemas are properly validated before use
- Be aware that large schemas increase configuration file size
- Consider using URL-based approach for frequently changing APIs
- Store sensitive information (like API keys) in environment variables, not in schemas
## Troubleshooting
### Common Issues
1. **Invalid JSON**: Ensure your schema is valid JSON format
2. **Missing Required Fields**: OpenAPI schemas must include `openapi`, `info`, and `paths` fields
3. **Schema Size**: Very large schemas may impact performance
4. **Server Configuration**: Ensure the `servers` field in your schema points to the correct endpoints
### Validation Errors
The system provides detailed error messages for:
- Malformed JSON in schema field
- Missing required OpenAPI fields
- Invalid security configurations
- Network issues with URL-based configurations

172
docs/openapi-support.md Normal file
View File

@@ -0,0 +1,172 @@
# OpenAPI Support in MCPHub
MCPHub now supports OpenAPI 3.1.1 servers as a new server type, allowing you to integrate REST APIs directly into your MCP workflow.
## Features
-**Full OpenAPI 3.1.1 Support**: Load and parse OpenAPI specifications
-**Multiple Security Types**: None, API Key, HTTP Authentication, OAuth 2.0, OpenID Connect
-**Dynamic Tool Generation**: Automatically creates MCP tools from OpenAPI operations
-**Type Safety**: Full TypeScript support with proper type definitions
-**Frontend Integration**: Easy-to-use forms for configuring OpenAPI servers
-**Internationalization**: Support for English and Chinese languages
## Configuration
### Basic Configuration
```json
{
"type": "openapi",
"openapi": {
"url": "https://api.example.com/v1/openapi.json",
"version": "3.1.0",
"security": {
"type": "none"
}
}
}
```
### With API Key Authentication
```json
{
"type": "openapi",
"openapi": {
"url": "https://api.example.com/v1/openapi.json",
"version": "3.1.0",
"security": {
"type": "apiKey",
"apiKey": {
"name": "X-API-Key",
"in": "header",
"value": "your-api-key-here"
}
}
},
"headers": {
"Accept": "application/json"
}
}
```
### With HTTP Bearer Authentication
```json
{
"type": "openapi",
"openapi": {
"url": "https://api.example.com/v1/openapi.json",
"version": "3.1.0",
"security": {
"type": "http",
"http": {
"scheme": "bearer",
"credentials": "your-bearer-token-here"
}
}
}
}
```
## Supported Security Types
1. **None**: No authentication required
2. **API Key**: API key in header or query parameter
3. **HTTP**: Basic, Bearer, or Digest authentication
4. **OAuth 2.0**: OAuth 2.0 access tokens
5. **OpenID Connect**: OpenID Connect ID tokens
## How It Works
1. **Specification Loading**: The OpenAPI client fetches and parses the OpenAPI specification
2. **Tool Generation**: Each operation in the spec becomes an MCP tool
3. **Request Handling**: Tools handle parameter validation and API calls
4. **Response Processing**: API responses are returned as tool results
## Frontend Usage
1. Navigate to the Servers page
2. Click "Add Server"
3. Select "OpenAPI" as the server type
4. Enter the OpenAPI specification URL
5. Configure security settings if needed
6. Add any additional headers
7. Save the configuration
## Testing
You can test the OpenAPI integration using the provided test scripts:
```bash
# Test OpenAPI client directly
npx tsx test-openapi.ts
# Test full integration
npx tsx test-integration.ts
```
## Example: Swagger Petstore
The Swagger Petstore API is a perfect example for testing:
```json
{
"type": "openapi",
"openapi": {
"url": "https://petstore3.swagger.io/api/v3/openapi.json",
"version": "3.1.0",
"security": {
"type": "none"
}
}
}
```
This will create tools like:
- `addPet`: Add a new pet to the store
- `findPetsByStatus`: Find pets by status
- `getPetById`: Find pet by ID
- And many more...
## Error Handling
The OpenAPI client includes comprehensive error handling:
- Network errors are properly caught and reported
- Invalid specifications are rejected with clear error messages
- API errors include response status and body information
- Type validation ensures proper parameter handling
## Limitations
- Only supports OpenAPI 3.x specifications (3.0.0 and above)
- Complex authentication flows (like OAuth 2.0 authorization code flow) require manual token management
- Large specifications may take time to parse initially
- Some advanced OpenAPI features may not be fully supported
## Contributing
To add new features or fix bugs in the OpenAPI integration:
1. Backend types: `src/types/index.ts`
2. OpenAPI client: `src/clients/openapi.ts`
3. Service integration: `src/services/mcpService.ts`
4. Frontend forms: `frontend/src/components/ServerForm.tsx`
5. Internationalization: `frontend/src/locales/`
## Troubleshooting
**Q: My OpenAPI server won't connect**
A: Check that the specification URL is accessible and returns valid JSON/YAML
**Q: Tools aren't showing up**
A: Verify that your OpenAPI specification includes valid operations with required fields
**Q: Authentication isn't working**
A: Double-check your security configuration matches the API's requirements
**Q: Getting CORS errors**
A: The API server needs to allow CORS requests from your MCPHub domain

View File

@@ -106,12 +106,10 @@ Once your servers are configured, connect your AI clients using MCPHub endpoints
Access all configured MCP servers: ``` http://localhost:3000/mcp ```
</Tab>
<Tab title="Specific Group">
Access servers in a specific group: ``` http://localhost:3000/mcp/{group - name}
```
Access servers in a specific group: ``` http://localhost:3000/mcp/{groupName} ```
</Tab>
<Tab title="Individual Server">
Access a single server: ``` http://localhost:3000/mcp/{server - name}
```
Access a single server: ``` http://localhost:3000/mcp/{serverName} ```
</Tab>
<Tab title="Smart Routing">
Use AI-powered tool discovery: ``` http://localhost:3000/mcp/$smart ```
@@ -172,7 +170,7 @@ Here are some popular MCP servers you can add:
</Accordion>
</AccordionGroup>
## Verification
{/* ## Verification
Test your setup by making a simple request:
@@ -187,7 +185,7 @@ curl -X POST http://localhost:3000/mcp \
}'
```
You should receive a list of available tools from your configured MCP servers.
You should receive a list of available tools from your configured MCP servers. */}
## Next Steps

172
docs/testing-framework.md Normal file
View File

@@ -0,0 +1,172 @@
# 测试框架和自动化测试实现报告
## 概述
本项目已成功引入现代化的测试框架和自动化测试流程。实现了基于Jest的测试环境支持TypeScript、ES模块并包含完整的CI/CD配置。
## 已实现的功能
### 1. 测试框架配置
- **Jest配置**: 使用`jest.config.cjs`配置文件支持ES模块和TypeScript
- **覆盖率报告**: 配置了代码覆盖率收集和报告
- **测试环境**: 支持Node.js环境的单元测试和集成测试
- **模块映射**: 配置了路径别名支持
### 2. 测试工具和辅助函数
创建了完善的测试工具库 (`tests/utils/testHelpers.ts`):
- **认证工具**: JWT token生成和管理
- **HTTP测试**: Supertest集成用于API测试
- **数据生成**: 测试数据工厂函数
- **响应断言**: 自定义API响应验证器
- **环境管理**: 测试环境变量配置
### 3. 测试用例实现
已实现的测试场景:
#### 基础配置测试 (`tests/basic.test.ts`)
- Jest配置验证
- 异步操作支持测试
- 自定义匹配器验证
#### 认证逻辑测试 (`tests/auth.logic.test.ts`)
- 用户登录逻辑
- 密码验证
- JWT生成和验证
- 错误处理场景
- 用户数据验证
#### 路径工具测试 (`tests/utils/pathLogic.test.ts`)
- 配置文件路径解析
- 环境变量处理
- 文件系统操作
- 错误处理和边界条件
- 跨平台路径处理
### 4. CI/CD配置
GitHub Actions配置 (`.github/workflows/ci.yml`):
- **多Node.js版本支持**: 18.x和20.x
- **自动化测试流程**:
- 代码检查 (ESLint)
- 类型检查 (TypeScript)
- 单元测试执行
- 覆盖率报告
- **构建验证**: 应用构建和产物验证
- **集成测试**: 包含数据库环境的集成测试
### 5. 测试脚本
`package.json`中添加的测试命令:
```json
{
"test": "jest",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage",
"test:verbose": "jest --verbose",
"test:ci": "jest --ci --coverage --watchAll=false"
}
```
## 测试结果
当前测试统计:
- **测试套件**: 3个
- **测试用例**: 19个
- **通过率**: 100%
- **执行时间**: ~15秒
### 测试覆盖的功能模块
1. **认证系统**: 用户登录、JWT处理、密码验证
2. **配置管理**: 文件路径解析、环境变量处理
3. **基础设施**: Jest配置、测试工具验证
## 技术特点
### 现代化特性
- **ES模块支持**: 完全支持ES2022模块语法
- **TypeScript集成**: 类型安全的测试编写
- **异步测试**: Promise和async/await支持
- **模拟系统**: Jest mock功能的深度使用
- **参数化测试**: 数据驱动的测试用例
### 最佳实践
- **测试隔离**: 每个测试用例独立运行
- **Mock管理**: 统一的mock清理和重置
- **错误处理**: 完整的错误场景测试
- **边界测试**: 输入验证和边界条件覆盖
- **文档化**: 清晰的测试用例命名和描述
## 后续扩展计划
### 短期目标
1. **API测试**: 为REST API端点添加集成测试
2. **数据库测试**: 添加数据模型和存储层测试
3. **中间件测试**: 认证和权限中间件测试
4. **服务层测试**: 核心业务逻辑测试
### 中期目标
1. **端到端测试**: 使用Playwright或Cypress
2. **性能测试**: API响应时间和负载测试
3. **安全测试**: 输入验证和安全漏洞测试
4. **契约测试**: API契约验证
### 长期目标
1. **测试数据管理**: 测试数据库和fixture管理
2. **视觉回归测试**: UI组件的视觉测试
3. **监控集成**: 生产环境测试监控
4. **自动化测试报告**: 详细的测试报告和趋势分析
## 开发指南
### 添加新测试用例
1.`tests/`目录下创建对应的测试文件
2. 使用`testHelpers.ts`中的工具函数
3. 遵循命名约定: `*.test.ts``*.spec.ts`
4. 确保测试用例具有清晰的描述和断言
### 运行测试
```bash
# 运行所有测试
pnpm test
# 监听模式
pnpm test:watch
# 生成覆盖率报告
pnpm test:coverage
# CI模式运行
pnpm test:ci
```
### Mock最佳实践
-`beforeEach`中清理所有mock
- 使用具体的mock实现而不是空函数
- 验证mock被正确调用
- 保持mock的一致性和可维护性
## 结论
本项目已成功建立了完整的现代化测试框架,具备以下优势:
1. **高度可扩展**: 易于添加新的测试用例和测试类型
2. **开发友好**: 丰富的工具函数和清晰的结构
3. **CI/CD就绪**: 完整的自动化流水线配置
4. **质量保证**: 代码覆盖率和持续测试验证
这个测试框架为项目的持续发展和质量保证提供了坚实的基础,支持敏捷开发和持续集成的最佳实践。

View File

@@ -0,0 +1,147 @@
---
title: "身份验证"
description: "管理用户和身份验证。"
---
import { Card, Cards } from 'mintlify';
<Card
title="POST /api/auth/login"
href="#login"
>
登录以获取 JWT 令牌。
</Card>
<Card
title="POST /api/auth/register"
href="#register"
>
注册一个新用户。
</Card>
<Card
title="GET /api/auth/user"
href="#get-current-user"
>
获取当前已验证的用户。
</Card>
<Card
title="POST /api/auth/change-password"
href="#change-password"
>
更改当前用户的密码。
</Card>
---
### 登录
验证用户身份并返回 JWT 令牌及用户详细信息。
- **端点**: `/api/auth/login`
- **方法**: `POST`
- **正文**:
- `username` (string, 必填): 用户名。
- `password` (string, 必填): 用户密码。
- **请求示例**:
```json
{
"username": "admin",
"password": "admin123"
}
```
- **成功响应**:
```json
{
"success": true,
"message": "登录成功",
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"user": {
"username": "admin",
"isAdmin": true,
"permissions": { ... }
}
}
```
---
### 注册
注册一个新用户并返回 JWT 令牌。
- **端点**: `/api/auth/register`
- **方法**: `POST`
- **正文**:
- `username` (string, 必填): 新的用户名。
- `password` (string, 必填): 新的用户密码 (至少6个字符)。
- `isAdmin` (boolean, 可选): 用户是否应有管理员权限。
- **请求示例**:
```json
{
"username": "newuser",
"password": "password123",
"isAdmin": false
}
```
- **成功响应**:
```json
{
"success": true,
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"user": {
"username": "newuser",
"isAdmin": false,
"permissions": { ... }
}
}
```
---
### 获取当前用户
检索当前通过身份验证的用户的个人资料。
- **端点**: `/api/auth/user`
- **方法**: `GET`
- **身份验证**: 需要承载令牌 (Bearer Token)。
- **成功响应**:
```json
{
"success": true,
"user": {
"username": "admin",
"isAdmin": true,
"permissions": { ... }
}
}
```
---
### 更改密码
允许通过身份验证的用户更改其密码。
- **端点**: `/api/auth/change-password`
- **方法**: `POST`
- **身份验证**: 需要承载令牌 (Bearer Token)。
- **正文**:
- `currentPassword` (string, 必填): 用户的当前密码。
- `newPassword` (string, 必填): 新的密码 (至少6个字符)。
- **请求示例**:
```json
{
"currentPassword": "oldpassword",
"newPassword": "newpassword123"
}
```
- **成功响应**:
```json
{
"success": true,
"message": "密码更新成功"
}
```

View File

@@ -0,0 +1,111 @@
---
title: "配置"
description: "管理和检索系统级配置。"
---
import { Card, Cards } from 'mintlify';
<Card title="PUT /api/system-config" href="#update-system-config">更新主系统配置。</Card>
<Card title="GET /api/settings" href="#get-all-settings">获取所有系统设置,包括服务器和群组。</Card>
<Card title="GET /config" href="#get-runtime-config">获取前端的公共运行时配置。</Card>
<Card title="GET /public-config" href="#get-public-config">获取公共配置以检查是否跳过身份验证。</Card>
---
### 更新系统配置
更新系统配置的各个部分。您只需提供要更新部分的键。
- **端点**: `/api/system-config`
- **方法**: `PUT`
- **正文**: 一个 JSON 对象,包含以下一个或多个顶级键:`routing`、`install`、`smartRouting`、`mcpRouter`。
#### 路由配置 (`routing`)
- `enableGlobalRoute` (boolean): 启用或禁用全局 `/api/mcp` 路由。
- `enableGroupNameRoute` (boolean): 启用或禁用基于群组的路由 (例如 `/api/mcp/group/:groupName`)。
- `enableBearerAuth` (boolean): 为 MCP 路由启用承载令牌身份验证。
- `bearerAuthKey` (string): 用于承载身份验证的密钥。
- `skipAuth` (boolean): 如果为 true则跳过所有身份验证使实例公开。
#### 安装配置 (`install`)
- `pythonIndexUrl` (string): 用于安装的 Python 包索引 (PyPI) 的基础 URL。
- `npmRegistry` (string): 用于安装的 npm 注册表 URL。
- `baseUrl` (string): 此 MCPHub 实例的公共基础 URL。
#### 智能路由配置 (`smartRouting`)
- `enabled` (boolean): 启用或禁用智能路由功能。
- `dbUrl` (string): 用于存储嵌入的数据库连接 URL。
- `openaiApiBaseUrl` (string): 用于生成嵌入的 OpenAI 兼容 API 的基础 URL。
- `openaiApiKey` (string): 嵌入服务的 API 密钥。
- `openaiApiEmbeddingModel` (string): 要使用的嵌入模型的名称。
#### MCP 路由器配置 (`mcpRouter`)
- `apiKey` (string): MCP 路由器服务的 API 密钥。
- `referer` (string): 用于 MCP 路由器请求的 referer 头。
- `title` (string): 在 MCP 路由器上显示的此实例的标题。
- `baseUrl` (string): MCP 路由器 API 的基础 URL。
- **请求示例**:
```json
{
"routing": {
"skipAuth": true
},
"smartRouting": {
"enabled": true,
"dbUrl": "postgresql://user:pass@host:port/db"
}
}
```
---
### 获取所有设置
检索实例的整个设置对象,包括所有服务器配置、群组和系统设置。这是 `mcp_settings.json` 文件的完整转储。
- **端点**: `/api/settings`
- **方法**: `GET`
---
### 获取运行时配置
检索前端应用程序所需的基本运行时配置。此端点不需要身份验证。
- **端点**: `/config`
- **方法**: `GET`
- **成功响应**:
```json
{
"success": true,
"data": {
"basePath": "",
"version": "1.0.0",
"name": "MCPHub"
}
}
```
---
### 获取公共配置
检索公共配置,主要用于检查是否跳过身份验证。这允许前端在用户登录前相应地调整其行为。此端点不需要身份验证。
- **端点**: `/public-config`
- **方法**: `GET`
- **成功响应**:
```json
{
"success": true,
"data": {
"skipAuth": false,
"permissions": {}
}
}
```

View File

@@ -1,572 +0,0 @@
---
title: '创建资源'
description: '创建新的 MCP 服务器、用户和组'
---
## 创建服务器
### 端点
```http
POST /api/servers
```
### 请求
#### 请求头
```http
Content-Type: application/json
Authorization: Bearer YOUR_JWT_TOKEN
```
#### 请求体
```json
{
"name": "文件系统服务器",
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-filesystem", "/allowed/path"],
"env": {
"NODE_ENV": "production",
"DEBUG": "mcp:*",
"MAX_FILES": "1000"
},
"cwd": "/app/workspace",
"timeout": 30000,
"retries": 3,
"enabled": true,
"description": "提供文件系统访问的 MCP 服务器",
"tags": ["filesystem", "production"],
"healthCheck": {
"enabled": true,
"interval": 30000,
"timeout": 5000,
"retries": 3,
"endpoint": "/health"
},
"resources": {
"memory": {
"limit": "512MB",
"warning": "400MB"
},
"cpu": {
"limit": "50%"
}
},
"logging": {
"level": "info",
"file": "/var/log/mcphub/server.log",
"maxSize": "100MB",
"maxFiles": 5
}
}
```
#### 必填字段
- `name` (string): 服务器唯一名称
- `command` (string): 执行命令
- `args` (array): 命令参数数组
#### 可选字段
- `env` (object): 环境变量键值对
- `cwd` (string): 工作目录
- `timeout` (number): 超时时间(毫秒)
- `retries` (number): 重试次数
- `enabled` (boolean): 是否启用(默认 true
- `description` (string): 服务器描述
- `tags` (array): 标签数组
- `healthCheck` (object): 健康检查配置
- `resources` (object): 资源限制配置
- `logging` (object): 日志配置
### 响应
#### 成功响应 (201 Created)
```json
{
"success": true,
"data": {
"id": "server-abc123",
"name": "文件系统服务器",
"status": "stopped",
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-filesystem", "/allowed/path"],
"env": {
"NODE_ENV": "production",
"DEBUG": "mcp:*",
"MAX_FILES": "1000"
},
"cwd": "/app/workspace",
"timeout": 30000,
"retries": 3,
"enabled": true,
"description": "提供文件系统访问的 MCP 服务器",
"tags": ["filesystem", "production"],
"healthCheck": {
"enabled": true,
"interval": 30000,
"timeout": 5000,
"retries": 3,
"endpoint": "/health",
"status": "unknown"
},
"resources": {
"memory": {
"limit": "512MB",
"warning": "400MB",
"current": "0MB"
},
"cpu": {
"limit": "50%",
"current": "0%"
}
},
"logging": {
"level": "info",
"file": "/var/log/mcphub/server.log",
"maxSize": "100MB",
"maxFiles": 5,
"currentSize": "0MB"
},
"createdAt": "2024-01-01T12:00:00Z",
"updatedAt": "2024-01-01T12:00:00Z",
"createdBy": "user123"
},
"message": "服务器创建成功"
}
```
#### 错误响应
**400 Bad Request - 参数错误**
```json
{
"success": false,
"error": {
"code": "VALIDATION_ERROR",
"message": "请求数据验证失败",
"details": [
{
"field": "name",
"message": "服务器名称不能为空"
},
{
"field": "command",
"message": "执行命令不能为空"
}
]
}
}
```
**409 Conflict - 名称冲突**
```json
{
"success": false,
"error": {
"code": "RESOURCE_CONFLICT",
"message": "服务器名称已存在",
"details": {
"field": "name",
"value": "文件系统服务器",
"conflictingResourceId": "server-xyz789"
}
}
}
```
### 示例
#### cURL
```bash
curl -X POST http://localhost:3000/api/servers \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_JWT_TOKEN" \
-d '{
"name": "文件系统服务器",
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-filesystem", "/data"],
"env": {
"NODE_ENV": "production"
},
"description": "生产环境文件系统服务器"
}'
```
#### JavaScript
```javascript
const response = await fetch('/api/servers', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({
name: '文件系统服务器',
command: 'npx',
args: ['-y', '@modelcontextprotocol/server-filesystem', '/data'],
env: {
NODE_ENV: 'production',
},
description: '生产环境文件系统服务器',
}),
});
const result = await response.json();
if (result.success) {
console.log('服务器创建成功:', result.data);
} else {
console.error('创建失败:', result.error);
}
```
#### Python
```python
import requests
response = requests.post(
'http://localhost:3000/api/servers',
headers={
'Content-Type': 'application/json',
'Authorization': f'Bearer {token}'
},
json={
'name': '文件系统服务器',
'command': 'npx',
'args': ['-y', '@modelcontextprotocol/server-filesystem', '/data'],
'env': {
'NODE_ENV': 'production'
},
'description': '生产环境文件系统服务器'
}
)
if response.status_code == 201:
result = response.json()
print('服务器创建成功:', result['data'])
else:
error = response.json()
print('创建失败:', error['error'])
```
## 创建用户
### 端点
```http
POST /api/users
```
### 请求体
```json
{
"username": "newuser",
"email": "user@example.com",
"password": "SecurePassword123!",
"role": "user",
"groups": ["dev-team", "qa-team"],
"profile": {
"firstName": "张",
"lastName": "三",
"department": "开发部",
"title": "软件工程师",
"phone": "+86-138-0013-8000",
"location": "北京"
},
"preferences": {
"language": "zh-CN",
"timezone": "Asia/Shanghai",
"notifications": {
"email": true,
"slack": false,
"browser": true
}
},
"enabled": true
}
```
### 响应 (201 Created)
```json
{
"success": true,
"data": {
"id": "user-abc123",
"username": "newuser",
"email": "user@example.com",
"role": "user",
"groups": [
{
"id": "dev-team",
"name": "开发团队",
"role": "member"
}
],
"profile": {
"firstName": "张",
"lastName": "三",
"fullName": "张三",
"department": "开发部",
"title": "软件工程师",
"phone": "+86-138-0013-8000",
"location": "北京",
"avatar": null
},
"preferences": {
"language": "zh-CN",
"timezone": "Asia/Shanghai",
"notifications": {
"email": true,
"slack": false,
"browser": true
}
},
"enabled": true,
"lastLoginAt": null,
"createdAt": "2024-01-01T12:00:00Z",
"updatedAt": "2024-01-01T12:00:00Z"
},
"message": "用户创建成功"
}
```
## 创建组
### 端点
```http
POST /api/groups
```
### 请求体
```json
{
"name": "dev-team",
"displayName": "开发团队",
"description": "前端和后端开发人员",
"parentGroup": null,
"permissions": {
"servers": {
"create": false,
"read": true,
"update": true,
"delete": false,
"execute": true
},
"tools": {
"filesystem": {
"read": true,
"write": true,
"paths": ["/app/data", "/tmp"]
},
"web-search": {
"enabled": true,
"maxQueries": 100
}
},
"monitoring": {
"viewLogs": true,
"viewMetrics": true,
"exportData": false
}
},
"settings": {
"autoAssign": false,
"maxMembers": 50,
"requireApproval": true,
"sessionTimeout": "8h"
},
"quotas": {
"requests": {
"daily": 1000,
"monthly": 30000
},
"storage": {
"maxSize": "10GB"
}
}
}
```
### 响应 (201 Created)
```json
{
"success": true,
"data": {
"id": "group-abc123",
"name": "dev-team",
"displayName": "开发团队",
"description": "前端和后端开发人员",
"parentGroup": null,
"permissions": {
"servers": {
"create": false,
"read": true,
"update": true,
"delete": false,
"execute": true
},
"tools": {
"filesystem": {
"read": true,
"write": true,
"paths": ["/app/data", "/tmp"]
},
"web-search": {
"enabled": true,
"maxQueries": 100
}
},
"monitoring": {
"viewLogs": true,
"viewMetrics": true,
"exportData": false
}
},
"settings": {
"autoAssign": false,
"maxMembers": 50,
"requireApproval": true,
"sessionTimeout": "8h"
},
"quotas": {
"requests": {
"daily": 1000,
"monthly": 30000
},
"storage": {
"maxSize": "10GB"
}
},
"memberCount": 0,
"serverCount": 0,
"createdAt": "2024-01-01T12:00:00Z",
"updatedAt": "2024-01-01T12:00:00Z",
"createdBy": "admin"
},
"message": "组创建成功"
}
```
## 批量创建
### 批量创建服务器
```http
POST /api/servers/bulk
```
#### 请求体
```json
{
"servers": [
{
"name": "dev-server-1",
"command": "python",
"args": ["-m", "mcp_server"],
"env": { "ENV": "development" }
},
{
"name": "dev-server-2",
"command": "node",
"args": ["server.js"],
"env": { "ENV": "development" }
}
],
"options": {
"skipExisting": true,
"validateAll": true,
"startAfterCreate": false
}
}
```
#### 响应 (201 Created)
```json
{
"success": true,
"data": {
"created": [
{
"id": "server-1",
"name": "dev-server-1",
"status": "created"
},
{
"id": "server-2",
"name": "dev-server-2",
"status": "created"
}
],
"skipped": [],
"failed": [],
"summary": {
"total": 2,
"created": 2,
"skipped": 0,
"failed": 0
}
},
"message": "批量创建完成,成功创建 2 个服务器"
}
```
## 验证
### 预验证创建请求
在实际创建资源之前验证请求:
```http
POST /api/servers/validate
```
#### 请求体
```json
{
"name": "test-server",
"command": "invalid-command",
"args": []
}
```
#### 响应
```json
{
"success": false,
"data": {
"valid": false,
"errors": [
{
"field": "command",
"message": "命令 'invalid-command' 不存在或无法执行"
}
],
"warnings": [
{
"field": "args",
"message": "参数数组为空,服务器可能无法正常启动"
}
]
}
}
```
有关更多 API 端点信息,请参阅 [获取资源](/zh/api-reference/endpoint/get)、[删除资源](/zh/api-reference/endpoint/delete) 和 [WebHooks](/zh/api-reference/endpoint/webhook) 文档。

View File

@@ -1,303 +0,0 @@
---
title: 删除资源 API
description: 删除各种资源的 API 端点,包括服务器、组和配置等
---
# 删除资源 API
本文档描述了用于删除各种资源的 API 端点。
## 删除 MCP 服务器
删除指定的 MCP 服务器配置。
### 端点
```http
DELETE /api/servers/{id}
```
### 参数
| 参数名 | 类型 | 位置 | 必需 | 描述 |
| ------ | ------ | ---- | ---- | ------------------ |
| id | string | path | 是 | 服务器的唯一标识符 |
### 请求示例
```bash
curl -X DELETE \
'https://api.mcphub.io/api/servers/mcp-server-123' \
-H 'Authorization: Bearer YOUR_API_TOKEN' \
-H 'Content-Type: application/json'
```
### 响应
#### 成功响应 (204 No Content)
```json
{
"success": true,
"message": "服务器已成功删除",
"data": {
"id": "mcp-server-123",
"deletedAt": "2024-01-15T10:30:00Z"
}
}
```
#### 错误响应
**404 Not Found**
```json
{
"error": {
"code": "SERVER_NOT_FOUND",
"message": "指定的服务器不存在",
"details": {
"serverId": "mcp-server-123"
}
}
}
```
**409 Conflict**
```json
{
"error": {
"code": "SERVER_IN_USE",
"message": "服务器正在使用中,无法删除",
"details": {
"activeConnections": 5,
"associatedGroups": ["group-1", "group-2"]
}
}
}
```
## 删除服务器组
删除指定的服务器组。
### 端点
```http
DELETE /api/groups/{id}
```
### 参数
| 参数名 | 类型 | 位置 | 必需 | 描述 |
| ------ | ------- | ----- | ---- | ------------------------------ |
| id | string | path | 是 | 组的唯一标识符 |
| force | boolean | query | 否 | 是否强制删除(包含服务器的组) |
### 请求示例
```bash
curl -X DELETE \
'https://api.mcphub.io/api/groups/production-group?force=true' \
-H 'Authorization: Bearer YOUR_API_TOKEN' \
-H 'Content-Type: application/json'
```
### 响应
#### 成功响应 (204 No Content)
```json
{
"success": true,
"message": "服务器组已成功删除",
"data": {
"id": "production-group",
"deletedServers": ["server-1", "server-2"],
"deletedAt": "2024-01-15T10:30:00Z"
}
}
```
## 删除配置项
删除指定的配置项。
### 端点
```http
DELETE /api/config/{key}
```
### 参数
| 参数名 | 类型 | 位置 | 必需 | 描述 |
| ------ | ------ | ---- | ---- | -------- |
| key | string | path | 是 | 配置键名 |
### 请求示例
```bash
curl -X DELETE \
'https://api.mcphub.io/api/config/custom-setting' \
-H 'Authorization: Bearer YOUR_API_TOKEN'
```
### 响应
#### 成功响应 (200 OK)
```json
{
"success": true,
"message": "配置项已删除",
"data": {
"key": "custom-setting",
"previousValue": "old-value",
"deletedAt": "2024-01-15T10:30:00Z"
}
}
```
## 批量删除
### 批量删除服务器
删除多个 MCP 服务器。
#### 端点
```http
DELETE /api/servers/batch
```
#### 请求体
```json
{
"serverIds": ["server-1", "server-2", "server-3"],
"force": false
}
```
#### 响应
```json
{
"success": true,
"message": "批量删除完成",
"data": {
"deleted": ["server-1", "server-3"],
"failed": [
{
"id": "server-2",
"reason": "服务器正在使用中"
}
],
"summary": {
"total": 3,
"deleted": 2,
"failed": 1
}
}
}
```
## 软删除 vs 硬删除
### 软删除
默认情况下MCPHub 使用软删除机制:
- 资源被标记为已删除但保留在数据库中
- 可以通过恢复 API 恢复删除的资源
- 删除的资源在列表 API 中默认不显示
### 硬删除
使用 `permanent=true` 参数执行硬删除:
```bash
curl -X DELETE \
'https://api.mcphub.io/api/servers/mcp-server-123?permanent=true' \
-H 'Authorization: Bearer YOUR_API_TOKEN'
```
<Warning>硬删除操作不可逆,请谨慎使用。</Warning>
## 权限要求
| 操作 | 所需权限 |
| ---------- | ------------------------ |
| 删除服务器 | `servers:delete` |
| 删除组 | `groups:delete` |
| 删除配置 | `config:delete` |
| 硬删除 | `admin:permanent_delete` |
## 错误代码
| 错误代码 | HTTP 状态码 | 描述 |
| -------------------------- | ----------- | ---------------- |
| `RESOURCE_NOT_FOUND` | 404 | 资源不存在 |
| `RESOURCE_IN_USE` | 409 | 资源正在使用中 |
| `INSUFFICIENT_PERMISSIONS` | 403 | 权限不足 |
| `VALIDATION_ERROR` | 400 | 请求参数验证失败 |
| `INTERNAL_ERROR` | 500 | 服务器内部错误 |
## 最佳实践
### 1. 删除前检查
在删除资源前,建议先检查资源的使用情况:
```bash
# 检查服务器使用情况
curl -X GET \
'https://api.mcphub.io/api/servers/mcp-server-123/usage' \
-H 'Authorization: Bearer YOUR_API_TOKEN'
```
### 2. 备份重要数据
对于重要资源,建议在删除前进行备份:
```bash
# 导出服务器配置
curl -X GET \
'https://api.mcphub.io/api/servers/mcp-server-123/export' \
-H 'Authorization: Bearer YOUR_API_TOKEN' \
> server-backup.json
```
### 3. 使用事务删除
对于复杂的删除操作,使用事务确保数据一致性:
```json
{
"transaction": true,
"operations": [
{
"type": "delete",
"resource": "server",
"id": "server-1"
},
{
"type": "delete",
"resource": "group",
"id": "group-1"
}
]
}
```
## 恢复删除的资源
软删除的资源可以通过恢复 API 恢复:
```bash
curl -X POST \
'https://api.mcphub.io/api/servers/mcp-server-123/restore' \
-H 'Authorization: Bearer YOUR_API_TOKEN'
```

View File

@@ -1,607 +0,0 @@
---
title: '获取资源'
description: '查询和检索 MCP 服务器、用户和组信息'
---
## 获取服务器列表
### 端点
```http
GET /api/servers
```
### 查询参数
| 参数 | 类型 | 描述 | 示例 |
| ---------------- | ------- | ------------------------------- | ---------------------------- |
| `page` | integer | 页码(从 1 开始) | `?page=2` |
| `limit` | integer | 每页记录数(默认 20最大 100 | `?limit=50` |
| `sort` | string | 排序字段 | `?sort=name` |
| `order` | string | 排序顺序asc/desc | `?order=desc` |
| `status` | string | 过滤服务器状态 | `?status=running` |
| `search` | string | 搜索服务器名称或描述 | `?search=python` |
| `group` | string | 过滤所属组 | `?group=dev-team` |
| `tags` | string | 过滤标签(逗号分隔) | `?tags=python,production` |
| `enabled` | boolean | 过滤启用状态 | `?enabled=true` |
| `created_after` | string | 创建时间起始 | `?created_after=2024-01-01` |
| `created_before` | string | 创建时间结束 | `?created_before=2024-01-31` |
### 响应
```json
{
"success": true,
"data": {
"items": [
{
"id": "server-abc123",
"name": "文件系统服务器",
"status": "running",
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-filesystem", "/data"],
"env": {
"NODE_ENV": "production"
},
"cwd": "/app",
"pid": 12345,
"uptime": 3600000,
"enabled": true,
"description": "提供文件系统访问的 MCP 服务器",
"tags": ["filesystem", "production"],
"health": {
"status": "healthy",
"lastCheck": "2024-01-01T12:00:00Z",
"responseTime": "45ms"
},
"resources": {
"memory": {
"used": "128MB",
"limit": "512MB",
"percentage": 25
},
"cpu": {
"used": "15%",
"limit": "50%"
}
},
"stats": {
"totalRequests": 1523,
"errorCount": 2,
"avgResponseTime": "234ms"
},
"lastRestart": "2024-01-01T08:00:00Z",
"createdAt": "2024-01-01T00:00:00Z",
"updatedAt": "2024-01-01T12:00:00Z"
}
],
"pagination": {
"page": 1,
"limit": 20,
"total": 45,
"pages": 3,
"hasNext": true,
"hasPrev": false
},
"filters": {
"status": "running",
"totalFiltered": 12
}
}
}
```
### 示例
```bash
# 获取运行中的服务器,按名称排序
curl -X GET "http://localhost:3000/api/servers?status=running&sort=name&order=asc" \
-H "Authorization: Bearer $TOKEN"
# 搜索包含 "python" 的服务器
curl -X GET "http://localhost:3000/api/servers?search=python&limit=10" \
-H "Authorization: Bearer $TOKEN"
# 获取开发团队的服务器
curl -X GET "http://localhost:3000/api/servers?group=dev-team" \
-H "Authorization: Bearer $TOKEN"
```
## 获取服务器详情
### 端点
```http
GET /api/servers/{serverId}
```
### 路径参数
- `serverId` (string): 服务器唯一标识符
### 查询参数
| 参数 | 类型 | 描述 |
| --------------- | ------ | ----------------------------------------------- |
| `include` | string | 包含额外信息(逗号分隔):`logs,metrics,events` |
| `metrics_range` | string | 指标时间范围:`1h`, `24h`, `7d` |
### 响应
```json
{
"success": true,
"data": {
"id": "server-abc123",
"name": "文件系统服务器",
"status": "running",
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-filesystem", "/data"],
"env": {
"NODE_ENV": "production",
"DEBUG": "mcp:*"
},
"cwd": "/app",
"pid": 12345,
"uptime": 3600000,
"enabled": true,
"description": "提供文件系统访问的 MCP 服务器",
"tags": ["filesystem", "production"],
"healthCheck": {
"enabled": true,
"interval": 30000,
"timeout": 5000,
"retries": 3,
"endpoint": "/health",
"status": "healthy",
"lastCheck": "2024-01-01T12:00:00Z",
"responseTime": "45ms",
"consecutiveFailures": 0
},
"resources": {
"memory": {
"used": "128MB",
"limit": "512MB",
"warning": "400MB",
"percentage": 25
},
"cpu": {
"used": "15%",
"limit": "50%",
"cores": 4
},
"network": {
"bytesIn": "1.2GB",
"bytesOut": "890MB"
}
},
"stats": {
"totalRequests": 1523,
"successfulRequests": 1521,
"errorCount": 2,
"avgResponseTime": "234ms",
"p95ResponseTime": "450ms",
"requestsPerMinute": 25,
"lastError": {
"timestamp": "2024-01-01T11:30:00Z",
"message": "Temporary connection timeout",
"count": 1
}
},
"capabilities": [
{
"type": "tool",
"name": "read_file",
"description": "读取文件内容",
"schema": {
"type": "object",
"properties": {
"path": { "type": "string" }
}
}
},
{
"type": "tool",
"name": "write_file",
"description": "写入文件内容",
"schema": {
"type": "object",
"properties": {
"path": { "type": "string" },
"content": { "type": "string" }
}
}
}
],
"groups": [
{
"id": "dev-team",
"name": "开发团队",
"permissions": ["read", "write", "execute"]
}
],
"events": [
{
"id": "event-123",
"type": "started",
"timestamp": "2024-01-01T08:00:00Z",
"message": "服务器启动成功",
"metadata": {
"pid": 12345,
"startupTime": "2.3s"
}
}
],
"lastRestart": "2024-01-01T08:00:00Z",
"createdAt": "2024-01-01T00:00:00Z",
"updatedAt": "2024-01-01T12:00:00Z",
"createdBy": "admin"
}
}
```
### 示例
```bash
# 获取服务器基本信息
curl -X GET "http://localhost:3000/api/servers/server-abc123" \
-H "Authorization: Bearer $TOKEN"
# 获取服务器详情包含日志和指标
curl -X GET "http://localhost:3000/api/servers/server-abc123?include=logs,metrics&metrics_range=24h" \
-H "Authorization: Bearer $TOKEN"
```
## 获取服务器状态
### 端点
```http
GET /api/servers/{serverId}/status
```
### 响应
```json
{
"success": true,
"data": {
"serverId": "server-abc123",
"status": "running",
"health": "healthy",
"pid": 12345,
"uptime": 3600000,
"startedAt": "2024-01-01T08:00:00Z",
"lastHealthCheck": "2024-01-01T12:00:00Z",
"resources": {
"memory": {
"rss": 134217728,
"heapTotal": 67108864,
"heapUsed": 45088768,
"external": 8388608
},
"cpu": {
"user": 1000000,
"system": 500000,
"percentage": 15.5
}
},
"connections": {
"active": 5,
"total": 127
},
"performance": {
"requestsPerSecond": 12.5,
"avgResponseTime": "234ms",
"errorRate": "0.1%"
}
}
}
```
## 获取服务器日志
### 端点
```http
GET /api/servers/{serverId}/logs
```
### 查询参数
| 参数 | 类型 | 描述 |
| -------- | ------- | ---------------------------------------------- |
| `level` | string | 日志级别过滤:`error`, `warn`, `info`, `debug` |
| `limit` | integer | 返回日志条数(默认 100最大 1000 |
| `since` | string | 开始时间ISO 8601 格式) |
| `until` | string | 结束时间ISO 8601 格式) |
| `follow` | boolean | 实时跟踪日志流 |
| `search` | string | 搜索日志内容 |
### 响应
```json
{
"success": true,
"data": {
"logs": [
{
"id": "log-123",
"timestamp": "2024-01-01T12:00:00Z",
"level": "info",
"message": "处理请求: read_file",
"source": "mcp-server",
"metadata": {
"requestId": "req-456",
"userId": "user-789",
"duration": "45ms"
}
},
{
"id": "log-124",
"timestamp": "2024-01-01T12:00:05Z",
"level": "error",
"message": "文件不存在: /nonexistent/file.txt",
"source": "filesystem",
"metadata": {
"requestId": "req-457",
"path": "/nonexistent/file.txt",
"error": "ENOENT"
}
}
],
"pagination": {
"limit": 100,
"total": 1523,
"hasMore": true,
"nextCursor": "cursor-abc123"
}
}
}
```
### 实时日志流
```bash
# 实时跟踪日志
curl -X GET "http://localhost:3000/api/servers/server-abc123/logs?follow=true" \
-H "Authorization: Bearer $TOKEN" \
-H "Accept: text/event-stream"
```
## 获取服务器指标
### 端点
```http
GET /api/servers/{serverId}/metrics
```
### 查询参数
| 参数 | 类型 | 描述 |
| ------------- | ------ | ------------------------------------------- |
| `timeRange` | string | 时间范围:`1h`, `24h`, `7d`, `30d` |
| `granularity` | string | 数据粒度:`1m`, `5m`, `1h`, `1d` |
| `metrics` | string | 指定指标(逗号分隔):`cpu,memory,requests` |
### 响应
```json
{
"success": true,
"data": {
"timeRange": "1h",
"granularity": "5m",
"metrics": {
"cpu": {
"data": [
{ "timestamp": "2024-01-01T11:00:00Z", "value": 12.5 },
{ "timestamp": "2024-01-01T11:05:00Z", "value": 15.2 }
],
"summary": {
"avg": 13.8,
"min": 8.1,
"max": 18.5,
"current": 15.2
}
},
"memory": {
"data": [
{ "timestamp": "2024-01-01T11:00:00Z", "value": 125 },
{ "timestamp": "2024-01-01T11:05:00Z", "value": 128 }
],
"summary": {
"avg": 126.5,
"min": 120,
"max": 135,
"current": 128
}
},
"requests": {
"data": [
{ "timestamp": "2024-01-01T11:00:00Z", "value": 45 },
{ "timestamp": "2024-01-01T11:05:00Z", "value": 52 }
],
"summary": {
"total": 2847,
"avg": 48.5,
"peak": 67
}
},
"responseTime": {
"data": [
{ "timestamp": "2024-01-01T11:00:00Z", "avg": 230, "p95": 450 },
{ "timestamp": "2024-01-01T11:05:00Z", "avg": 245, "p95": 480 }
],
"summary": {
"avgResponseTime": "237ms",
"p95ResponseTime": "465ms"
}
}
}
}
}
```
## 获取用户列表
### 端点
```http
GET /api/users
```
### 查询参数
| 参数 | 类型 | 描述 |
| ------------------ | ------- | ---------------- |
| `role` | string | 过滤用户角色 |
| `group` | string | 过滤所属组 |
| `enabled` | boolean | 过滤启用状态 |
| `search` | string | 搜索用户名或邮箱 |
| `last_login_after` | string | 最后登录时间起始 |
### 响应
```json
{
"success": true,
"data": {
"items": [
{
"id": "user-abc123",
"username": "zhangsan",
"email": "zhangsan@example.com",
"role": "user",
"enabled": true,
"profile": {
"firstName": "张",
"lastName": "三",
"fullName": "张三",
"department": "开发部",
"title": "软件工程师"
},
"groups": [
{
"id": "dev-team",
"name": "开发团队",
"role": "member"
}
],
"stats": {
"totalSessions": 45,
"totalRequests": 1234,
"lastRequestAt": "2024-01-01T11:30:00Z"
},
"lastLoginAt": "2024-01-01T08:00:00Z",
"createdAt": "2023-12-01T00:00:00Z"
}
],
"pagination": {
"page": 1,
"limit": 20,
"total": 89,
"pages": 5
}
}
}
```
## 获取组列表
### 端点
```http
GET /api/groups
```
### 响应
```json
{
"success": true,
"data": {
"items": [
{
"id": "group-abc123",
"name": "dev-team",
"displayName": "开发团队",
"description": "前端和后端开发人员",
"memberCount": 12,
"serverCount": 8,
"parentGroup": null,
"children": [],
"permissions": {
"servers": ["read", "write", "execute"],
"tools": ["read", "execute"]
},
"quotas": {
"requests": {
"used": 750,
"limit": 1000
}
},
"createdAt": "2023-12-01T00:00:00Z"
}
]
}
}
```
## 搜索
### 全局搜索
```http
GET /api/search
```
### 查询参数
| 参数 | 类型 | 描述 |
| ------- | ------- | ---------------------------------------------- |
| `q` | string | 搜索关键词 |
| `type` | string | 资源类型:`servers`, `users`, `groups`, `logs` |
| `limit` | integer | 每种类型的最大结果数 |
### 响应
```json
{
"success": true,
"data": {
"query": "python",
"results": {
"servers": [
{
"id": "server-1",
"name": "Python MCP Server",
"type": "server",
"relevance": 0.95
}
],
"users": [],
"groups": [
{
"id": "python-devs",
"name": "Python 开发者",
"type": "group",
"relevance": 0.8
}
],
"logs": [
{
"id": "log-123",
"message": "Starting Python server...",
"type": "log",
"relevance": 0.7
}
]
},
"total": 3
}
}
```
有关更多信息,请参阅 [创建资源](/zh/api-reference/endpoint/create)、[删除资源](/zh/api-reference/endpoint/delete) 和 [WebHooks](/zh/api-reference/endpoint/webhook) 文档。

View File

@@ -1,615 +0,0 @@
---
title: WebHooks API
description: 配置和管理 WebHook 事件通知的完整指南
---
# WebHooks API
WebHooks 允许 MCPHub 在特定事件发生时向您的应用程序发送实时通知。
## 概述
MCPHub WebHooks 系统支持以下功能:
- 实时事件通知
- 自定义过滤器
- 重试机制
- 签名验证
- 批量事件处理
## 支持的事件类型
| 事件类型 | 描述 |
| ----------------------- | -------------- |
| `server.created` | MCP 服务器创建 |
| `server.updated` | MCP 服务器更新 |
| `server.deleted` | MCP 服务器删除 |
| `server.status_changed` | 服务器状态变更 |
| `group.created` | 服务器组创建 |
| `group.updated` | 服务器组更新 |
| `group.deleted` | 服务器组删除 |
| `user.login` | 用户登录 |
| `user.logout` | 用户登出 |
| `config.changed` | 配置变更 |
| `system.error` | 系统错误 |
## 创建 WebHook
### 端点
```http
POST /api/webhooks
```
### 请求体
```json
{
"url": "https://your-app.com/webhook",
"events": ["server.created", "server.status_changed"],
"secret": "your-webhook-secret",
"active": true,
"config": {
"contentType": "application/json",
"insecureSsl": false,
"retryCount": 3,
"timeout": 30
},
"filters": {
"serverGroups": ["production", "staging"],
"serverTypes": ["ai-assistant", "data-processor"]
}
}
```
### 响应
```json
{
"success": true,
"data": {
"id": "webhook-123",
"url": "https://your-app.com/webhook",
"events": ["server.created", "server.status_changed"],
"active": true,
"secret": "your-webhook-secret",
"config": {
"contentType": "application/json",
"insecureSsl": false,
"retryCount": 3,
"timeout": 30
},
"filters": {
"serverGroups": ["production", "staging"],
"serverTypes": ["ai-assistant", "data-processor"]
},
"createdAt": "2024-01-15T10:30:00Z",
"updatedAt": "2024-01-15T10:30:00Z"
}
}
```
## 获取 WebHook 列表
### 端点
```http
GET /api/webhooks
```
### 查询参数
| 参数名 | 类型 | 描述 |
| ------ | ------- | -------------------- |
| page | integer | 页码默认1 |
| limit | integer | 每页数量默认20 |
| active | boolean | 过滤活跃状态 |
| event | string | 过滤事件类型 |
### 请求示例
```bash
curl -X GET \
'https://api.mcphub.io/api/webhooks?active=true&limit=10' \
-H 'Authorization: Bearer YOUR_API_TOKEN'
```
### 响应
```json
{
"success": true,
"data": {
"webhooks": [
{
"id": "webhook-123",
"url": "https://your-app.com/webhook",
"events": ["server.created", "server.status_changed"],
"active": true,
"lastDelivery": "2024-01-15T09:30:00Z",
"deliveryCount": 145,
"failureCount": 2,
"createdAt": "2024-01-10T10:30:00Z"
}
],
"pagination": {
"page": 1,
"limit": 10,
"total": 25,
"pages": 3
}
}
}
```
## 获取单个 WebHook
### 端点
```http
GET /api/webhooks/{id}
```
### 响应
```json
{
"success": true,
"data": {
"id": "webhook-123",
"url": "https://your-app.com/webhook",
"events": ["server.created", "server.status_changed"],
"active": true,
"secret": "your-webhook-secret",
"config": {
"contentType": "application/json",
"insecureSsl": false,
"retryCount": 3,
"timeout": 30
},
"filters": {
"serverGroups": ["production", "staging"],
"serverTypes": ["ai-assistant", "data-processor"]
},
"stats": {
"totalDeliveries": 145,
"successfulDeliveries": 143,
"failedDeliveries": 2,
"lastDelivery": "2024-01-15T09:30:00Z",
"lastSuccess": "2024-01-15T09:30:00Z",
"lastFailure": "2024-01-14T15:20:00Z"
},
"createdAt": "2024-01-10T10:30:00Z",
"updatedAt": "2024-01-15T10:30:00Z"
}
}
```
## 更新 WebHook
### 端点
```http
PUT /api/webhooks/{id}
```
### 请求体
```json
{
"url": "https://your-app.com/new-webhook",
"events": ["server.created", "server.updated", "server.deleted"],
"active": true,
"config": {
"retryCount": 5,
"timeout": 45
}
}
```
## 删除 WebHook
### 端点
```http
DELETE /api/webhooks/{id}
```
### 响应
```json
{
"success": true,
"message": "WebHook 已成功删除"
}
```
## WebHook 事件格式
### 基本结构
所有 WebHook 事件都遵循以下基本结构:
```json
{
"id": "event-123",
"type": "server.created",
"timestamp": "2024-01-15T10:30:00Z",
"version": "1.0",
"data": {
// 事件特定数据
},
"metadata": {
"source": "mcphub",
"environment": "production",
"triggeredBy": "user-456"
}
}
```
### 服务器事件示例
#### server.created
```json
{
"id": "event-123",
"type": "server.created",
"timestamp": "2024-01-15T10:30:00Z",
"version": "1.0",
"data": {
"server": {
"id": "mcp-server-123",
"name": "AI Assistant Server",
"type": "ai-assistant",
"endpoint": "https://ai-assistant.example.com",
"group": "production",
"status": "active",
"capabilities": ["chat", "completion"],
"createdAt": "2024-01-15T10:30:00Z"
}
},
"metadata": {
"source": "mcphub",
"environment": "production",
"triggeredBy": "user-456"
}
}
```
#### server.status_changed
```json
{
"id": "event-124",
"type": "server.status_changed",
"timestamp": "2024-01-15T11:30:00Z",
"version": "1.0",
"data": {
"server": {
"id": "mcp-server-123",
"name": "AI Assistant Server",
"previousStatus": "active",
"currentStatus": "inactive",
"reason": "Health check failed",
"lastHealthCheck": "2024-01-15T11:25:00Z"
}
},
"metadata": {
"source": "mcphub",
"environment": "production",
"triggeredBy": "system"
}
}
```
## 签名验证
MCPHub 使用 HMAC-SHA256 签名来验证 WebHook 的真实性。
### 签名生成
签名在 `X-MCPHub-Signature-256` 头中发送:
```
X-MCPHub-Signature-256: sha256=5757107ea39eca8e35d1e8...
```
### 验证示例
#### Node.js
```javascript
const crypto = require('crypto');
function verifySignature(payload, signature, secret) {
const expectedSignature = crypto
.createHmac('sha256', secret)
.update(payload, 'utf8')
.digest('hex');
const actualSignature = signature.replace('sha256=', '');
return crypto.timingSafeEqual(
Buffer.from(expectedSignature, 'hex'),
Buffer.from(actualSignature, 'hex'),
);
}
// Express.js 中间件示例
app.use('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
const signature = req.headers['x-mcphub-signature-256'];
const payload = req.body;
if (!verifySignature(payload, signature, process.env.WEBHOOK_SECRET)) {
return res.status(401).send('Unauthorized');
}
// 处理 WebHook 事件
const event = JSON.parse(payload);
console.log('收到事件:', event.type);
res.status(200).send('OK');
});
```
#### Python
```python
import hmac
import hashlib
def verify_signature(payload, signature, secret):
expected_signature = hmac.new(
secret.encode('utf-8'),
payload,
hashlib.sha256
).hexdigest()
actual_signature = signature.replace('sha256=', '')
return hmac.compare_digest(expected_signature, actual_signature)
# Flask 示例
from flask import Flask, request, jsonify
import json
app = Flask(__name__)
@app.route('/webhook', methods=['POST'])
def webhook():
signature = request.headers.get('X-MCPHub-Signature-256')
payload = request.get_data()
if not verify_signature(payload, signature, 'your-webhook-secret'):
return jsonify({'error': 'Unauthorized'}), 401
event = json.loads(payload)
print(f'收到事件: {event["type"]}')
return jsonify({'status': 'success'}), 200
```
## 重试机制
MCPHub 对失败的 WebHook 交付实施指数退避重试:
- **重试次数**: 可配置(默认 3 次)
- **重试间隔**: 2^n 秒n 为重试次数)
- **最大间隔**: 300 秒5 分钟)
- **超时设置**: 可配置(默认 30 秒)
### 重试时间表
| 尝试次数 | 延迟时间 |
| -------- | -------- |
| 1 | 立即 |
| 2 | 2 秒 |
| 3 | 4 秒 |
| 4 | 8 秒 |
| 5 | 16 秒 |
## 获取交付历史
### 端点
```http
GET /api/webhooks/{id}/deliveries
```
### 查询参数
| 参数名 | 类型 | 描述 |
| ---------- | ------- | ------------------------------------ |
| page | integer | 页码 |
| limit | integer | 每页数量 |
| status | string | 过滤状态success, failed, pending |
| event_type | string | 过滤事件类型 |
### 响应
```json
{
"success": true,
"data": {
"deliveries": [
{
"id": "delivery-123",
"eventId": "event-123",
"eventType": "server.created",
"url": "https://your-app.com/webhook",
"status": "success",
"responseCode": 200,
"responseTime": 145,
"attempts": 1,
"deliveredAt": "2024-01-15T10:30:15Z",
"nextRetry": null
},
{
"id": "delivery-124",
"eventId": "event-124",
"eventType": "server.status_changed",
"url": "https://your-app.com/webhook",
"status": "failed",
"responseCode": 500,
"responseTime": 30000,
"attempts": 3,
"error": "Connection timeout",
"deliveredAt": null,
"nextRetry": "2024-01-15T11:45:00Z"
}
],
"pagination": {
"page": 1,
"limit": 20,
"total": 145,
"pages": 8
}
}
}
```
## 测试 WebHook
### 端点
```http
POST /api/webhooks/{id}/test
```
### 请求体
```json
{
"eventType": "server.created",
"customData": {
"test": true,
"message": "这是一个测试事件"
}
}
```
### 响应
```json
{
"success": true,
"data": {
"deliveryId": "delivery-test-123",
"status": "delivered",
"responseCode": 200,
"responseTime": 124,
"sentAt": "2024-01-15T10:30:00Z"
}
}
```
## 最佳实践
### 1. 幂等性处理
确保您的 WebHook 端点能够处理重复事件:
```javascript
const processedEvents = new Set();
app.post('/webhook', (req, res) => {
const event = req.body;
// 检查事件是否已处理
if (processedEvents.has(event.id)) {
return res.status(200).send('Already processed');
}
// 处理事件
processEvent(event);
// 记录已处理的事件
processedEvents.add(event.id);
res.status(200).send('OK');
});
```
### 2. 异步处理
对于复杂的处理逻辑,使用异步处理避免阻塞:
```javascript
app.post('/webhook', async (req, res) => {
const event = req.body;
// 立即响应
res.status(200).send('OK');
// 异步处理事件
setImmediate(() => {
processEventAsync(event);
});
});
```
### 3. 错误处理
实施适当的错误处理和日志记录:
```javascript
app.post('/webhook', (req, res) => {
try {
const event = req.body;
processEvent(event);
res.status(200).send('OK');
} catch (error) {
console.error('WebHook 处理错误:', error);
res.status(500).send('Internal Server Error');
}
});
```
### 4. 监控和告警
监控 WebHook 的交付状态:
```bash
# 检查失败的交付
curl -X GET \
'https://api.mcphub.io/api/webhooks/webhook-123/deliveries?status=failed' \
-H 'Authorization: Bearer YOUR_API_TOKEN'
```
## 故障排除
### 常见问题
1. **签名验证失败**
- 检查密钥是否正确
- 确保使用原始请求体进行验证
- 验证 HMAC 计算实现
2. **超时错误**
- 增加 WebHook 超时设置
- 优化端点响应时间
- 使用异步处理
3. **重复事件**
- 实施幂等性检查
- 使用事件 ID 去重
- 记录处理状态
### 调试工具
使用 MCPHub 提供的调试工具:
```bash
# 查看最近的交付日志
curl -X GET \
'https://api.mcphub.io/api/webhooks/webhook-123/deliveries?limit=5' \
-H 'Authorization: Bearer YOUR_API_TOKEN'
# 重新发送失败的事件
curl -X POST \
'https://api.mcphub.io/api/webhooks/delivery-124/redeliver' \
-H 'Authorization: Bearer YOUR_API_TOKEN'
```

View File

@@ -0,0 +1,212 @@
---
title: "群组"
description: "管理服务器群组以组织和路由请求。"
---
import { Card, Cards } from 'mintlify';
<Card title="GET /api/groups" href="#get-all-groups">获取所有群组的列表。</Card>
<Card title="POST /api/groups" href="#create-a-new-group">创建一个新群组。</Card>
<Card title="GET /api/groups/:id" href="#get-a-group">获取特定群组的详细信息。</Card>
<Card title="PUT /api/groups/:id" href="#update-a-group">更新现有群组。</Card>
<Card title="DELETE /api/groups/:id" href="#delete-a-group">删除一个群组。</Card>
<Card title="POST /api/groups/:id/servers" href="#add-server-to-group">将服务器添加到群组。</Card>
<Card title="DELETE /api/groups/:id/servers/:serverName" href="#remove-server-from-group">从群组中删除服务器。</Card>
<Card title="PUT /api/groups/:id/servers/batch" href="#batch-update-group-servers">批量更新群组中的服务器。</Card>
<Card title="GET /api/groups/:id/server-configs" href="#get-group-server-configs">获取群组中详细的服务器配置。</Card>
<Card title="PUT /api/groups/:id/server-configs/:serverName/tools" href="#update-group-server-tools">更新群组中服务器的工具选择。</Card>
---
### 获取所有群组
检索所有服务器群组的列表。
- **端点**: `/api/groups`
- **方法**: `GET`
- **成功响应**:
```json
{
"success": true,
"data": [
{
"id": "group-1",
"name": "我的群组",
"description": "服务器的集合。",
"servers": ["server1", "server2"],
"owner": "admin"
}
]
}
```
---
### 创建一个新群组
创建一个新的服务器群组。
- **端点**: `/api/groups`
- **方法**: `POST`
- **正文**:
- `name` (string, 必填): 群组的名称。
- `description` (string, 可选): 群组的描述。
- `servers` (array of strings, 可选): 要包含在群组中的服务器名称列表。
- **请求示例**:
```json
{
"name": "我的新群组",
"description": "新群组的描述",
"servers": ["server1", "server2"]
}
```
---
### 获取一个群组
通过 ID 或名称检索特定群组的详细信息。
- **端点**: `/api/groups/:id`
- **方法**: `GET`
- **参数**:
- `:id` (string, 必填): 群组的 ID 或名称。
---
### 更新一个群组
更新现有群组的名称、描述或服务器列表。
- **端点**: `/api/groups/:id`
- **方法**: `PUT`
- **参数**:
- `:id` (string, 必填): 要更新的群组的 ID 或名称。
- **正文**:
- `name` (string, 可选): 群组的新名称。
- `description` (string, 可选): 群组的新描述。
- `servers` (array, 可选): 群组的新服务器列表。格式请参阅 [批量更新群组服务器](#batch-update-group-servers)。
- **请求示例**:
```json
{
"name": "更新后的群组名称",
"description": "更新后的描述"
}
```
---
### 删除一个群组
通过 ID 或名称删除一个群组。
- **端点**: `/api/groups/:id`
- **方法**: `DELETE`
- **参数**:
- `:id` (string, 必填): 要删除的群组的 ID 或名称。
---
### 将服务器添加到群组
将单个服务器添加到群组。
- **端点**: `/api/groups/:id/servers`
- **方法**: `POST`
- **参数**:
- `:id` (string, 必填): 群组的 ID 或名称。
- **正文**:
- `serverName` (string, 必填): 要添加的服务器的名称。
- **请求示例**:
```json
{
"serverName": "my-server"
}
```
---
### 从群组中删除服务器
从群组中删除单个服务器。
- **端点**: `/api/groups/:id/servers/:serverName`
- **方法**: `DELETE`
- **参数**:
- `:id` (string, 必填): 群组的 ID 或名称。
- `:serverName` (string, 必填): 要删除的服务器的名称。
---
### 批量更新群组服务器
用新的列表替换群组中的所有服务器。该列表可以是简单的字符串或详细的配置对象。
- **端点**: `/api/groups/:id/servers/batch`
- **方法**: `PUT`
- **参数**:
- `:id` (string, 必填): 群组的 ID 或名称。
- **正文**:
- `servers` (array, 必填): 服务器名称(字符串)或服务器配置对象的数组。
- **请求示例 (简单)**:
```json
{
"servers": ["server1", "server2"]
}
```
- **请求示例 (详细)**:
```json
{
"servers": [
{ "name": "server1", "tools": "all" },
{ "name": "server2", "tools": ["toolA", "toolB"] }
]
}
```
---
### 获取群组服务器配置
检索群组内所有服务器的详细配置,包括启用了哪些工具。
- **端点**: `/api/groups/:id/server-configs`
- **方法**: `GET`
- **参数**:
- `:id` (string, 必填): 群组的 ID 或名称。
- **成功响应**:
```json
{
"success": true,
"data": [
{
"name": "server1",
"tools": "all"
},
{
"name": "server2",
"tools": ["toolA", "toolB"]
}
]
}
```
---
### 更新群组服务器工具
更新群组内特定服务器的工具选择。
- **端点**: `/api/groups/:id/server-configs/:serverName/tools`
- **方法**: `PUT`
- **参数**:
- `:id` (string, 必填): 群组的 ID 或名称。
- `:serverName` (string, 必填): 要更新的服务器的名称。
- **正文**:
- `tools` (string or array of strings, 必填): 字符串 `"all"` 表示启用所有工具,或一个工具名称数组以指定启用哪些工具。
- **请求示例**:
```json
{
"tools": ["toolA", "toolC"]
}
```

View File

@@ -1,717 +1,13 @@
---
title: 'API 参考'
description: 'MCPHub REST API 完整参考文档'
title: "介绍"
description: "欢迎来到 MCPHub API 文档。"
---
## 概述
MCPHub API 提供了一整套端点来管理您的 MCP 服务器、群组、用户等。该 API 分为两个主要类别:
MCPHub 提供全面的 REST API用于管理 MCP 服务器、用户、组和监控。所有 API 端点都需要身份验证,并支持 JSON 格式的请求和响应。
- **MCP 端点**: 这些是与您的 MCP 服务器交互的主要端点。它们提供了一个统一的界面,用于向您的服务器发送请求并实时接收响应。
- **管理 API**: 这些端点用于管理 MCPHub 实例本身。这包括管理服务器、群组、用户和系统设置。
## 基础信息
所有 API 端点都在 `/api` 路径下可用。例如,获取所有服务器的端点是 `/api/servers`。
### 基础 URL
```
https://your-mcphub-instance.com/api
```
### 身份验证
所有 API 请求都需要身份验证。支持以下方法:
#### JWT 令牌认证
```bash
curl -X GET https://api.mcphub.com/servers \
-H "Authorization: Bearer YOUR_JWT_TOKEN"
```
#### API 密钥认证
```bash
curl -X GET https://api.mcphub.com/servers \
-H "X-API-Key: YOUR_API_KEY"
```
### 请求格式
- **Content-Type**: `application/json`
- **Accept**: `application/json`
- **User-Agent**: 建议包含您的应用程序名称和版本
### 响应格式
所有响应都采用 JSON 格式:
```json
{
"success": true,
"data": {
// 响应数据
},
"message": "操作成功",
"timestamp": "2024-01-01T12:00:00Z"
}
```
错误响应格式:
```json
{
"success": false,
"error": {
"code": "VALIDATION_ERROR",
"message": "请求数据无效",
"details": {
"field": "name",
"reason": "名称不能为空"
}
},
"timestamp": "2024-01-01T12:00:00Z"
}
```
## 状态码
| 状态码 | 说明 |
| ------ | -------------------- |
| 200 | 请求成功 |
| 201 | 资源创建成功 |
| 204 | 请求成功,无返回内容 |
| 400 | 请求参数错误 |
| 401 | 未授权访问 |
| 403 | 权限不足 |
| 404 | 资源不存在 |
| 409 | 资源冲突 |
| 422 | 请求数据验证失败 |
| 429 | 请求频率超限 |
| 500 | 服务器内部错误 |
## 分页
支持分页的端点使用以下参数:
- `page`: 页码(从 1 开始)
- `limit`: 每页记录数(默认 20最大 100
- `sort`: 排序字段
- `order`: 排序顺序(`asc` 或 `desc`
```bash
curl -X GET "https://api.mcphub.com/servers?page=2&limit=50&sort=name&order=asc" \
-H "Authorization: Bearer $TOKEN"
```
分页响应格式:
```json
{
"success": true,
"data": {
"items": [...],
"pagination": {
"page": 2,
"limit": 50,
"total": 234,
"pages": 5,
"hasNext": true,
"hasPrev": true
}
}
}
```
## 过滤和搜索
支持过滤的端点可以使用以下参数:
- `search`: 全文搜索
- `filter[field]`: 字段过滤
- `status`: 状态过滤
- `created_after`: 创建时间筛选
- `created_before`: 创建时间筛选
```bash
curl -X GET "https://api.mcphub.com/servers?search=python&filter[status]=running&created_after=2024-01-01" \
-H "Authorization: Bearer $TOKEN"
```
## API 端点
### 服务器管理
#### 获取服务器列表
```http
GET /api/servers
```
参数:
- `status` (可选): 过滤服务器状态 (`running`, `stopped`, `error`)
- `group` (可选): 过滤所属组
- `search` (可选): 搜索服务器名称或描述
示例响应:
```json
{
"success": true,
"data": {
"items": [
{
"id": "server-1",
"name": "文件系统服务器",
"status": "running",
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-filesystem", "/data"],
"env": {
"NODE_ENV": "production"
},
"cwd": "/app",
"pid": 12345,
"uptime": 3600000,
"lastRestart": "2024-01-01T12:00:00Z",
"createdAt": "2024-01-01T10:00:00Z",
"updatedAt": "2024-01-01T12:00:00Z"
}
]
}
}
```
#### 创建服务器
```http
POST /api/servers
```
请求体:
```json
{
"name": "新服务器",
"command": "python",
"args": ["-m", "mcp_server"],
"env": {
"API_KEY": "your-api-key",
"LOG_LEVEL": "INFO"
},
"cwd": "/app/python-server",
"enabled": true,
"description": "Python MCP 服务器",
"tags": ["python", "production"]
}
```
#### 获取服务器详情
```http
GET /api/servers/{serverId}
```
#### 更新服务器
```http
PUT /api/servers/{serverId}
```
#### 删除服务器
```http
DELETE /api/servers/{serverId}
```
#### 启动服务器
```http
POST /api/servers/{serverId}/start
```
#### 停止服务器
```http
POST /api/servers/{serverId}/stop
```
请求体(可选):
```json
{
"graceful": true,
"timeout": 30000
}
```
#### 重启服务器
```http
POST /api/servers/{serverId}/restart
```
#### 获取服务器日志
```http
GET /api/servers/{serverId}/logs
```
参数:
- `level` (可选): 日志级别过滤
- `limit` (可选): 返回日志条数
- `since` (可选): 开始时间
- `follow` (可选): 实时跟踪日志
### 用户管理
#### 获取用户列表
```http
GET /api/users
```
#### 创建用户
```http
POST /api/users
```
请求体:
```json
{
"username": "newuser",
"email": "user@example.com",
"password": "securepassword",
"role": "user",
"groups": ["dev-team"],
"profile": {
"firstName": "张",
"lastName": "三",
"department": "开发部"
}
}
```
#### 获取用户详情
```http
GET /api/users/{userId}
```
#### 更新用户
```http
PUT /api/users/{userId}
```
#### 删除用户
```http
DELETE /api/users/{userId}
```
### 组管理
#### 获取组列表
```http
GET /api/groups
```
#### 创建组
```http
POST /api/groups
```
请求体:
```json
{
"name": "dev-team",
"displayName": "开发团队",
"description": "前端和后端开发人员",
"parentGroup": null,
"permissions": {
"servers": ["read", "write", "execute"],
"tools": ["read", "execute"]
},
"settings": {
"autoAssign": false,
"maxMembers": 50,
"requireApproval": true
}
}
```
#### 添加用户到组
```http
POST /api/groups/{groupId}/members
```
请求体:
```json
{
"userId": "user123",
"role": "member"
}
```
#### 从组中移除用户
```http
DELETE /api/groups/{groupId}/members/{userId}
```
#### 分配服务器到组
```http
POST /api/groups/{groupId}/servers
```
请求体:
```json
{
"serverId": "server-1",
"permissions": ["read", "write", "execute"]
}
```
### 身份验证
#### 登录
```http
POST /api/auth/login
```
请求体:
```json
{
"username": "admin",
"password": "password",
"mfaCode": "123456"
}
```
响应:
```json
{
"success": true,
"data": {
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"refreshToken": "refresh_token_here",
"expiresIn": 86400,
"user": {
"id": "user123",
"username": "admin",
"role": "admin",
"permissions": ["*"]
}
}
}
```
#### 刷新令牌
```http
POST /api/auth/refresh
```
#### 注销
```http
POST /api/auth/logout
```
#### 验证令牌
```http
GET /api/auth/verify
```
### 监控
#### 获取系统状态
```http
GET /api/monitoring/status
```
响应:
```json
{
"success": true,
"data": {
"system": {
"uptime": 86400,
"version": "2.1.0",
"nodeVersion": "18.17.0"
},
"servers": {
"total": 12,
"running": 10,
"stopped": 1,
"error": 1
},
"performance": {
"requestsPerMinute": 85,
"avgResponseTime": "245ms",
"errorRate": "0.3%"
}
}
}
```
#### 获取性能指标
```http
GET /api/monitoring/metrics
```
参数:
- `timeRange`: 时间范围 (`1h`, `24h`, `7d`, `30d`)
- `granularity`: 数据粒度 (`1m`, `5m`, `1h`, `1d`)
- `metrics`: 指定指标名称(逗号分隔)
#### 获取日志
```http
GET /api/monitoring/logs
```
参数:
- `level`: 日志级别
- `source`: 日志源
- `limit`: 返回条数
- `since`: 开始时间
- `until`: 结束时间
### 配置管理
#### 获取系统配置
```http
GET /api/config
```
#### 更新系统配置
```http
PUT /api/config
```
请求体:
```json
{
"smtp": {
"host": "smtp.example.com",
"port": 587,
"secure": false,
"auth": {
"user": "noreply@example.com",
"pass": "password"
}
},
"notifications": {
"email": true,
"slack": true,
"webhook": "https://hooks.example.com/notifications"
}
}
```
## WebSocket API
MCPHub 支持 WebSocket 连接以获取实时更新。
### 连接
```javascript
const ws = new WebSocket('wss://api.mcphub.com/ws');
ws.onopen = function () {
// 发送认证消息
ws.send(
JSON.stringify({
type: 'auth',
token: 'YOUR_JWT_TOKEN',
}),
);
};
```
### 订阅事件
```javascript
// 订阅服务器状态更新
ws.send(
JSON.stringify({
type: 'subscribe',
channel: 'server-status',
filters: {
serverId: 'server-1',
},
}),
);
// 订阅系统监控
ws.send(
JSON.stringify({
type: 'subscribe',
channel: 'monitoring',
metrics: ['cpu', 'memory', 'requests'],
}),
);
```
### 事件类型
- `server-status`: 服务器状态变化
- `server-logs`: 实时日志流
- `monitoring`: 系统监控指标
- `alerts`: 系统警报
- `user-activity`: 用户活动事件
## 错误处理
### 错误代码
| 错误代码 | 描述 |
| ----------------------- | -------------- |
| `INVALID_REQUEST` | 请求格式无效 |
| `AUTHENTICATION_FAILED` | 身份验证失败 |
| `AUTHORIZATION_FAILED` | 权限不足 |
| `RESOURCE_NOT_FOUND` | 资源不存在 |
| `RESOURCE_CONFLICT` | 资源冲突 |
| `VALIDATION_ERROR` | 数据验证失败 |
| `RATE_LIMIT_EXCEEDED` | 请求频率超限 |
| `SERVER_ERROR` | 服务器内部错误 |
### 错误处理示例
```javascript
async function handleApiRequest() {
try {
const response = await fetch('/api/servers', {
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
},
});
const data = await response.json();
if (!data.success) {
switch (data.error.code) {
case 'AUTHENTICATION_FAILED':
// 重新登录
redirectToLogin();
break;
case 'RATE_LIMIT_EXCEEDED':
// 延迟重试
setTimeout(() => handleApiRequest(), 5000);
break;
default:
// 显示错误消息
showError(data.error.message);
}
return;
}
// 处理成功响应
handleSuccessResponse(data.data);
} catch (error) {
// 处理网络错误
console.error('网络请求失败:', error);
}
}
```
## 速率限制
API 实施速率限制以防止滥用:
- **默认限制**: 每分钟 100 请求
- **认证用户**: 每分钟 1000 请求
- **管理员**: 每分钟 5000 请求
响应头包含速率限制信息:
```
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 999
X-RateLimit-Reset: 1609459200
```
## SDK 和客户端库
### JavaScript/Node.js
```bash
npm install @mcphub/sdk
```
```javascript
import { MCPHubClient } from '@mcphub/sdk';
const client = new MCPHubClient({
baseURL: 'https://api.mcphub.com',
token: 'YOUR_JWT_TOKEN',
});
// 获取服务器列表
const servers = await client.servers.list();
// 创建服务器
const newServer = await client.servers.create({
name: '新服务器',
command: 'python',
args: ['-m', 'mcp_server'],
});
```
### Python
```bash
pip install mcphub-sdk
```
```python
from mcphub_sdk import MCPHubClient
client = MCPHubClient(
base_url='https://api.mcphub.com',
token='YOUR_JWT_TOKEN'
)
# 获取服务器列表
servers = client.servers.list()
# 创建服务器
new_server = client.servers.create(
name='新服务器',
command='python',
args=['-m', 'mcp_server']
)
```
## 最佳实践
1. **使用 HTTPS**: 始终通过 HTTPS 访问 API
2. **安全存储令牌**: 不要在客户端代码中硬编码令牌
3. **处理错误**: 实施适当的错误处理和重试逻辑
4. **遵守速率限制**: 监控速率限制并实施退避策略
5. **使用分页**: 对于大数据集使用分页参数
6. **缓存响应**: 适当缓存 API 响应以减少请求
7. **版本控制**: 使用 API 版本号以确保兼容性
有关更多信息,请参阅我们的 [SDK 文档](https://docs.mcphub.com/sdk) 和 [示例代码](https://github.com/mcphub/examples)。
大多数管理 API 端点都需要身份验证。有关更多详细信息,请参阅[身份验证](/api-reference/auth)部分。

View File

@@ -0,0 +1,81 @@
---
title: "日志"
description: "访问和管理服务器日志。"
---
import { Card, Cards } from 'mintlify';
<Card
title="GET /api/logs"
href="#get-all-logs"
>
获取所有日志。
</Card>
<Card
title="DELETE /api/logs"
href="#clear-logs"
>
清除所有日志。
</Card>
<Card
title="GET /api/logs/stream"
href="#stream-logs"
>
实时流式传输日志。
</Card>
---
### 获取所有日志
检索所有存储的日志。
- **端点**: `/api/logs`
- **方法**: `GET`
- **成功响应**:
```json
{
"success": true,
"data": [
{
"timestamp": "2023-10-27T10:00:00.000Z",
"level": "info",
"message": "服务器成功启动。",
"service": "system"
}
]
}
```
---
### 清除日志
删除所有存储的日志。
- **端点**: `/api/logs`
- **方法**: `DELETE`
- **成功响应**:
```json
{
"success": true,
"message": "日志清除成功"
}
```
---
### 流式传输日志
使用服务器发送事件 (SSE) 实时流式传输日志。连接将保持打开状态,新的日志条目将在发生时发送。
- **端点**: `/api/logs/stream`
- **方法**: `GET`
- **响应格式**: 该流发送带有包含 JSON 对象的 `data` 字段的事件。第一个事件的 `type` 为 `initial`,包含所有历史日志。后续事件的 `type` 为 `log`,包含单个新日志条目。
- **事件示例**:
```
data: {"type":"log","log":{"timestamp":"2023-10-27T10:00:05.000Z","level":"debug","message":"正在处理 /api/some-endpoint 的请求","service":"mcp-server"}}
```

View File

@@ -0,0 +1,33 @@
---
title: "MCP HTTP 端点"
description: "使用统一的 HTTP 端点连接到您的 MCP 服务器。"
---
MCPHub 为您的所有 MCP 服务器提供统一的可流式 HTTP 接口。这使您可以向任何配置的 MCP 服务器发送请求并实时接收响应。
### 统一端点
此端点提供对所有已启用的 MCP 服务器的访问。
- **端点**: `http://localhost:3000/mcp`
- **方法**: `POST`
### 特定群组的端点
要定向访问特定的服务器群组,请使用基于群组的 HTTP 端点。
- **端点**: `http://localhost:3000/mcp/{group}`
- **方法**: `POST`
- **参数**:
- `{group}`: 群组的 ID 或名称。
### 特定服务器的端点
要直接访问单个服务器,请使用特定于服务器的 HTTP 端点。
- **端点**: `http://localhost:3000/mcp/{server}`
- **方法**: `POST`
- **参数**:
- `{server}`: 服务器的名称。
> **注意**: 如果服务器名称和群组名称相同,则群组将优先。

View File

@@ -0,0 +1,25 @@
---
title: "MCP SSE 端点 (已弃用)"
description: "使用 SSE 端点连接到您的 MCP 服务器。"
---
SSE 端点已弃用,并将在未来版本中删除。请改用 [MCP HTTP 端点](/api-reference/mcp-http)。
### 统一端点
- **端点**: `http://localhost:3000/sse`
- **方法**: `GET`
### 特定群组的端点
- **端点**: `http://localhost:3000/sse/{group}`
- **方法**: `GET`
- **参数**:
- `{group}`: 群组的 ID 或名称。
### 特定服务器的端点
- **端点**: `http://localhost:3000/sse/{server}`
- **方法**: `GET`
- **参数**:
- `{server}`: 服务器的名称。

View File

@@ -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查询参数和 POSTJSON 正文)进行工具执行
- ✅ **无需身份验证**OpenAPI 端点公开,便于集成
- ✅ **完整元数据**:具有适当模式和文档的完整 OpenAPI 规范
## API 端点
### OpenAPI 规范
<CodeGroup>
```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"
```
</CodeGroup>
生成并返回所有已连接 MCP 工具的完整 OpenAPI 3.0.3 规范。
**查询参数:**
<ParamField query="title" type="string" optional>
自定义 API 标题
</ParamField>
<ParamField query="description" type="string" optional>
自定义 API 描述
</ParamField>
<ParamField query="version" type="string" optional>
自定义 API 版本
</ParamField>
<ParamField query="serverUrl" type="string" optional>
自定义服务器 URL
</ParamField>
<ParamField query="includeDisabled" type="boolean" optional default="false">
包含禁用的工具
</ParamField>
<ParamField query="servers" type="string" optional>
要包含的服务器名称列表(逗号分隔)
</ParamField>
### 可用服务器
<CodeGroup>
```bash GET /api/openapi/servers
curl "http://localhost:3000/api/openapi/servers"
```
</CodeGroup>
返回已连接的 MCP 服务器名称列表。
<ResponseExample>
```json 示例响应
{
"success": true,
"data": ["amap", "playwright", "slack"]
}
```
</ResponseExample>
### 工具统计
<CodeGroup>
```bash GET /api/openapi/stats
curl "http://localhost:3000/api/openapi/stats"
```
</CodeGroup>
返回有关可用工具和服务器的统计信息。
<ResponseExample>
```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"}
]
}
}
```
</ResponseExample>
### 工具执行
<CodeGroup>
```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"}'
```
</CodeGroup>
通过 OpenAPI 兼容端点执行 MCP 工具。
**路径参数:**
<ParamField path="serverName" type="string" required>
MCP 服务器的名称
</ParamField>
<ParamField path="toolName" type="string" required>
要执行的工具名称
</ParamField>
## OpenWebUI 集成
要将 MCPHub 与 OpenWebUI 集成:
<Steps>
<Step title="启动 MCPHub">
确保 MCPHub 正在运行,并且已配置 MCP 服务器
</Step>
<Step title="获取 OpenAPI 规范">
```bash
curl http://localhost:3000/api/openapi.json > mcphub-api.json
```
</Step>
<Step title="添加到 OpenWebUI">
在 OpenWebUI 中导入 OpenAPI 规范文件或直接指向 URL
</Step>
</Steps>
### 配置示例
在 OpenWebUI 中,您可以通过以下方式将 MCPHub 添加为 OpenAPI 工具:
<CardGroup cols={2}>
<Card title="OpenAPI URL" icon="link">
`http://localhost:3000/api/openapi.json`
</Card>
<Card title="基础 URL" icon="server">
`http://localhost:3000/api`
</Card>
</CardGroup>
## 生成的 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 的优势
<CardGroup cols={2}>
<Card title="直接集成" icon="plug">
无需中间代理
</Card>
<Card title="实时更新" icon="refresh">
OpenAPI 规范随着 MCP 服务器连接/断开自动更新
</Card>
<Card title="更好的性能" icon="bolt">
直接工具执行,无代理开销
</Card>
<Card title="简化架构" icon="layer-group">
减少一个需要管理的组件
</Card>
</CardGroup>
## 故障排除
<AccordionGroup>
<Accordion title="OpenAPI 规范显示没有工具">
确保 MCP 服务器已连接。检查 `/api/openapi/stats` 查看服务器状态。
</Accordion>
<Accordion title="工具执行失败">
验证工具名称和参数是否与 OpenAPI 规范匹配。检查服务器日志以获取详细信息。
</Accordion>
<Accordion title="OpenWebUI 无法连接">
确保 MCPHub 可从 OpenWebUI 访问,并且 OpenAPI URL 正确。
</Accordion>
<Accordion title="规范中缺少工具">
检查您的 MCP 服务器配置中是否启用了工具。使用 `includeDisabled=true` 查看所有工具。
</Accordion>
</AccordionGroup>

View File

@@ -0,0 +1,209 @@
---
title: "服务器"
description: "管理您的 MCP 服务器。"
---
import { Card, Cards } from 'mintlify';
<Card
title="GET /api/servers"
href="#get-all-servers"
>
获取所有 MCP 服务器的列表。
</Card>
<Card
title="POST /api/servers"
href="#create-a-new-server"
>
创建一个新的 MCP 服务器。
</Card>
<Card
title="PUT /api/servers/:name"
href="#update-a-server"
>
更新现有的 MCP 服务器。
</Card>
<Card
title="DELETE /api/servers/:name"
href="#delete-a-server"
>
删除一个 MCP 服务器。
</Card>
<Card
title="POST /api/servers/:name/toggle"
href="#toggle-a-server"
>
切换服务器的启用状态。
</Card>
<Card
title="POST /api/servers/:serverName/tools/:toolName/toggle"
href="#toggle-a-tool"
>
切换工具的启用状态。
</Card>
<Card
title="PUT /api/servers/:serverName/tools/:toolName/description"
href="#update-tool-description"
>
更新工具的描述。
</Card>
---
### 获取所有服务器
检索所有已配置的 MCP 服务器的列表,包括其状态和可用工具。
- **端点**: `/api/servers`
- **方法**: `GET`
- **响应**:
```json
{
"success": true,
"data": [
{
"name": "example-server",
"status": "connected",
"tools": [
{
"name": "tool1",
"description": "工具1的描述"
}
],
"config": {
"type": "stdio",
"command": "node",
"args": ["server.js"]
}
}
]
}
```
---
### 创建一个新服务器
将一个新的 MCP 服务器添加到配置中。
- **端点**: `/api/servers`
- **方法**: `POST`
- **正文**:
```json
{
"name": "my-new-server",
"config": {
"type": "stdio",
"command": "python",
"args": ["-u", "my_script.py"],
"owner": "admin"
}
}
```
- `name` (string, 必填): 服务器的唯一名称。
- `config` (object, 必填): 服务器配置对象。
- `type` (string): `stdio`、`sse`、`streamable-http` 或 `openapi`。
- `command` (string): `stdio` 类型要执行的命令。
- `args` (array of strings): 命令的参数。
- `url` (string): `sse`、`streamable-http` 或 `openapi` 类型的 URL。
- `openapi` (object): OpenAPI 配置。
- `url` (string): OpenAPI 模式的 URL。
- `schema` (object): OpenAPI 模式对象本身。
- `headers` (object): `sse`、`streamable-http` 和 `openapi` 类型请求要发送的标头。
- `keepAliveInterval` (number): `sse` 类型的保持活动间隔(毫秒)。默认为 60000。
- `owner` (string): 服务器的所有者。默认为当前用户或“admin”。
---
### 更新一个服务器
更新现有 MCP 服务器的配置。
- **端点**: `/api/servers/:name`
- **方法**: `PUT`
- **参数**:
- `:name` (string, 必填): 要更新的服务器的名称。
- **正文**:
```json
{
"config": {
"type": "stdio",
"command": "node",
"args": ["new_server.js"]
}
}
```
- `config` (object, 必填): 更新后的服务器配置对象。详情请参阅“创建一个新服务器”。
---
### 删除一个服务器
从配置中删除一个 MCP 服务器。
- **端点**: `/api/servers/:name`
- **方法**: `DELETE`
- **参数**:
- `:name` (string, 必填): 要删除的服务器的名称。
---
### 切换一个服务器
启用或禁用一个 MCP 服务器。
- **端点**: `/api/servers/:name/toggle`
- **方法**: `POST`
- **参数**:
- `:name` (string, 必填): 要切换的服务器的名称。
- **正文**:
```json
{
"enabled": true
}
```
- `enabled` (boolean, 必填): `true` 启用服务器,`false` 禁用服务器。
---
### 切换一个工具
启用或禁用服务器上的特定工具。
- **端点**: `/api/servers/:serverName/tools/:toolName/toggle`
- **方法**: `POST`
- **参数**:
- `:serverName` (string, 必填): 服务器的名称。
- `:toolName` (string, 必填): 工具的名称。
- **正文**:
```json
{
"enabled": true
}
```
- `enabled` (boolean, 必填): `true` 启用工具,`false` 禁用工具。
---
### 更新工具描述
更新特定工具的描述。
- **端点**: `/api/servers/:serverName/tools/:toolName/description`
- **方法**: `PUT`
- **参数**:
- `:serverName` (string, 必填): 服务器的名称。
- `:toolName` (string, 必填): 工具的名称。
- **正文**:
```json
{
"description": "新的工具描述"
}
```
- `description` (string, 必填): 工具的新描述。

View File

@@ -0,0 +1,29 @@
---
title: "智能路由"
description: "使用向量语义搜索进行智能工具发现。"
---
智能路由是 MCPHub 的智能工具发现系统,它使用向量语义搜索来自动为任何给定任务找到最相关的工具。
### HTTP 端点
- **端点**: `http://localhost:3000/mcp/$smart`
- **方法**: `POST`
### SSE 端点 (已弃用)
- **端点**: `http://localhost:3000/sse/$smart`
- **方法**: `GET`
### 工作原理
1. **工具索引**: 所有 MCP 工具都会自动转换为向量嵌入并存储在带有 pgvector 的 PostgreSQL 中。
2. **语义搜索**: 用户查询被转换为向量,并使用余弦相似度与工具嵌入进行匹配。
3. **智能过滤**: 动态阈值可确保相关结果而无噪音。
4. **精确执行**: 找到的工具可以通过适当的参数验证直接执行。
### 设置要求
- 带有 pgvector 扩展的 PostgreSQL
- OpenAI API 密钥(或兼容的嵌入服务)
- 在 MCPHub 设置中启用智能路由

View File

@@ -41,6 +41,50 @@ docker run -d \
mcphub:local
```
### 构建扩展功能版本
Docker 镜像支持 `INSTALL_EXT` 构建参数以包含额外工具:
```bash
# 构建扩展功能版本(包含 Docker 引擎、Chrome/Playwright
docker build --build-arg INSTALL_EXT=true -t mcphub:extended .
# 方式 1: 使用自动 Docker-in-Docker需要特权模式
docker run -d \
--name mcphub \
--privileged \
-p 3000:3000 \
-v $(pwd)/mcp_settings.json:/app/mcp_settings.json \
mcphub:extended
# 方式 2: 挂载 Docker socket使用宿主机的 Docker 守护进程)
docker run -d \
--name mcphub \
-p 3000:3000 \
-v $(pwd)/mcp_settings.json:/app/mcp_settings.json \
-v /var/run/docker.sock:/var/run/docker.sock \
mcphub:extended
# 验证 Docker 可用
docker exec mcphub docker --version
docker exec mcphub docker ps
```
<Note>
**INSTALL_EXT=true 包含的功能:**
- **Docker 引擎**:完整的 Docker 守护进程和 CLI用于容器管理。在特权模式下运行时守护进程会自动启动。
- **Chrome/Playwright**(仅 amd64用于浏览器自动化任务
扩展镜像较大,但为高级用例提供了额外功能。
</Note>
<Warning>
**Docker-in-Docker 安全注意事项:**
- **特权模式**`--privileged`):容器内启动 Docker 守护进程需要此权限。这会授予容器在宿主机上的提升权限。
- **Docker socket 挂载**`/var/run/docker.sock`):使容器可以访问宿主机的 Docker 守护进程。两种方式都应仅在可信环境中使用。
- 生产环境建议使用 Docker socket 挂载而非特权模式,以提高安全性。
</Warning>
## Docker Compose 设置
### 基本配置

View File

@@ -1,271 +1,44 @@
---
title: '环境变量配置'
title: '环境变量'
description: '使用环境变量配置 MCPHub'
---
# 环境变量配置
# 环境变量
MCPHub 使用环境变量进行配置。本指南涵盖所有可用变量及其用法。
MCPHub 使用环境变量进行配置。本指南涵盖所有可用变量及其用法。
## 核心应用设置
### 服务器配置
| 变量 | 默认值 | 描述 |
| ----------- | ------------- | ----------------------------------------------- |
| `PORT` | `3000` | HTTP 服务器端口号 |
| `HOST` | `0.0.0.0` | 服务器绑定的主机地址 |
| `NODE_ENV` | `development` | 应用环境(`development`、`production`、`test` |
| `LOG_LEVEL` | `info` | 日志级别(`error`、`warn`、`info`、`debug` |
| 变量 | 默认值 | 描述 |
| --- | --- | --- |
| `PORT` | `3000` | HTTP 服务器端口号 |
| `INIT_TIMEOUT` | `300000` | 应用程序的初始超时时间 |
| `BASE_PATH` | `''` | 应用程序的基本路径 |
| `READONLY` | `false` | 设置为 `true` 以启用只读模式 |
| `MCPHUB_SETTING_PATH` | | MCPHub 设置文件的路径 |
| `NODE_ENV` | `development` | 应用程序环境 (`development`, `production`, `test`) |
```env
PORT=3000
HOST=0.0.0.0
INIT_TIMEOUT=300000
BASE_PATH=/api
READONLY=true
MCPHUB_SETTING_PATH=/path/to/settings
NODE_ENV=production
LOG_LEVEL=info
```
### 数据库配置
| 变量 | 默认值 | 描述 |
| -------------- | ----------- | --------------------- |
| `DATABASE_URL` | - | PostgreSQL 连接字符串 |
| `DB_HOST` | `localhost` | 数据库主机 |
| `DB_PORT` | `5432` | 数据库端口 |
| `DB_NAME` | `mcphub` | 数据库名称 |
| `DB_USER` | `mcphub` | 数据库用户名 |
| `DB_PASSWORD` | - | 数据库密码 |
| `DB_SSL` | `false` | 启用数据库 SSL 连接 |
| `DB_POOL_MIN` | `2` | 最小数据库连接池大小 |
| `DB_POOL_MAX` | `10` | 最大数据库连接池大小 |
```env
# 选项 1完整连接字符串
DATABASE_URL=postgresql://username:password@localhost:5432/mcphub
# 选项 2单独组件
DB_HOST=localhost
DB_PORT=5432
DB_NAME=mcphub
DB_USER=mcphub
DB_PASSWORD=your-password
DB_SSL=false
```
## 认证与安全
### JWT 配置
| 变量 | 默认值 | 描述 |
| ------------------------ | ------- | ------------------------ |
| `JWT_SECRET` | - | JWT 令牌签名密钥必需 |
| `JWT_EXPIRES_IN` | `24h` | JWT 令牌过期时间 |
| `JWT_REFRESH_EXPIRES_IN` | `7d` | 刷新令牌过期时间 |
| `JWT_ALGORITHM` | `HS256` | JWT 签名算法 |
| 变量 | 默认值 | 描述 |
| --- | --- | --- |
| `JWT_SECRET` | - | 用于 JWT 令牌签名密钥 (必需) |
```env
JWT_SECRET=your-super-secret-key-change-this-in-production
JWT_EXPIRES_IN=24h
JWT_REFRESH_EXPIRES_IN=7d
```
### 会话与安全
| 变量 | 默认值 | 描述 |
| ------------------- | ------ | -------------------- |
| `SESSION_SECRET` | - | 会话加密密钥 |
| `BCRYPT_ROUNDS` | `12` | bcrypt 哈希轮数 |
| `RATE_LIMIT_WINDOW` | `15` | 速率限制窗口(分钟) |
| `RATE_LIMIT_MAX` | `100` | 每个窗口最大请求数 |
| `CORS_ORIGIN` | `*` | 允许的 CORS 来源 |
```env
SESSION_SECRET=your-session-secret
BCRYPT_ROUNDS=12
RATE_LIMIT_WINDOW=15
RATE_LIMIT_MAX=100
CORS_ORIGIN=https://your-domain.com,https://admin.your-domain.com
```
## 外部服务
### OpenAI 配置
| 变量 | 默认值 | 描述 |
| ------------------------ | ------------------------ | ------------------------------- |
| `OPENAI_API_KEY` | - | OpenAI API 密钥(用于智能路由) |
| `OPENAI_MODEL` | `gpt-3.5-turbo` | OpenAI 嵌入模型 |
| `OPENAI_EMBEDDING_MODEL` | `text-embedding-ada-002` | 向量嵌入模型 |
| `OPENAI_MAX_TOKENS` | `1000` | 每个请求最大令牌数 |
| `OPENAI_TEMPERATURE` | `0.1` | AI 响应温度 |
```env
OPENAI_API_KEY=sk-your-openai-api-key
OPENAI_MODEL=gpt-3.5-turbo
OPENAI_EMBEDDING_MODEL=text-embedding-ada-002
OPENAI_MAX_TOKENS=1000
OPENAI_TEMPERATURE=0.1
```
### Redis 配置(可选)
| 变量 | 默认值 | 描述 |
| ---------------- | ----------- | ---------------- |
| `REDIS_URL` | - | Redis 连接字符串 |
| `REDIS_HOST` | `localhost` | Redis 主机 |
| `REDIS_PORT` | `6379` | Redis 端口 |
| `REDIS_PASSWORD` | - | Redis 密码 |
| `REDIS_DB` | `0` | Redis 数据库编号 |
| `REDIS_PREFIX` | `mcphub:` | Redis 键前缀 |
```env
# 选项 1完整连接字符串
REDIS_URL=redis://username:password@localhost:6379/0
# 选项 2单独组件
REDIS_HOST=localhost
REDIS_PORT=6379
REDIS_PASSWORD=your-redis-password
REDIS_DB=0
REDIS_PREFIX=mcphub:
```
## MCP 服务器配置
### 默认设置
| 变量 | 默认值 | 描述 |
| ------------------- | ------------------- | ---------------------------- |
| `MCP_SETTINGS_FILE` | `mcp_settings.json` | MCP 设置文件路径 |
| `MCP_SERVERS_FILE` | `servers.json` | 服务器配置文件路径 |
| `MCP_TIMEOUT` | `30000` | MCP 操作默认超时(毫秒) |
| `MCP_MAX_RETRIES` | `3` | 失败操作最大重试次数 |
| `MCP_RESTART_DELAY` | `5000` | 重启失败服务器的延迟(毫秒) |
```env
MCP_SETTINGS_FILE=./config/mcp_settings.json
MCP_SERVERS_FILE=./config/servers.json
MCP_TIMEOUT=30000
MCP_MAX_RETRIES=3
MCP_RESTART_DELAY=5000
```
### 智能路由
| 变量 | 默认值 | 描述 |
| --------------------------- | ------ | ---------------------- |
| `SMART_ROUTING_ENABLED` | `true` | 启用 AI 驱动的智能路由 |
| `SMART_ROUTING_THRESHOLD` | `0.7` | 路由相似度阈值 |
| `SMART_ROUTING_MAX_RESULTS` | `5` | 返回的最大工具数 |
| `VECTOR_CACHE_TTL` | `3600` | 向量缓存 TTL |
```env
SMART_ROUTING_ENABLED=true
SMART_ROUTING_THRESHOLD=0.7
SMART_ROUTING_MAX_RESULTS=5
VECTOR_CACHE_TTL=3600
```
## 文件存储与上传
| 变量 | 默认值 | 描述 |
| -------------------- | ---------------- | -------------------------------- |
| `UPLOAD_DIR` | `./uploads` | 文件上传目录 |
| `MAX_FILE_SIZE` | `10485760` | 最大文件大小字节10MB |
| `ALLOWED_FILE_TYPES` | `image/*,text/*` | 允许的 MIME 类型 |
| `STORAGE_TYPE` | `local` | 存储类型(`local`、`s3`、`gcs` |
```env
UPLOAD_DIR=./data/uploads
MAX_FILE_SIZE=10485760
ALLOWED_FILE_TYPES=image/*,text/*,application/json
STORAGE_TYPE=local
```
### S3 存储(可选)
| 变量 | 默认值 | 描述 |
| ---------------------- | ----------- | -------------- |
| `S3_BUCKET` | - | S3 存储桶名称 |
| `S3_REGION` | `us-east-1` | S3 区域 |
| `S3_ACCESS_KEY_ID` | - | S3 访问密钥 |
| `S3_SECRET_ACCESS_KEY` | - | S3 密钥 |
| `S3_ENDPOINT` | - | 自定义 S3 端点 |
```env
S3_BUCKET=mcphub-uploads
S3_REGION=us-east-1
S3_ACCESS_KEY_ID=your-access-key
S3_SECRET_ACCESS_KEY=your-secret-key
```
## 监控与日志
### 应用监控
| 变量 | 默认值 | 描述 |
| ------------------------ | ------- | -------------------- |
| `METRICS_ENABLED` | `true` | 启用指标收集 |
| `METRICS_PORT` | `9090` | 指标端点端口 |
| `HEALTH_CHECK_INTERVAL` | `30000` | 健康检查间隔(毫秒) |
| `PERFORMANCE_MONITORING` | `false` | 启用性能监控 |
```env
METRICS_ENABLED=true
METRICS_PORT=9090
HEALTH_CHECK_INTERVAL=30000
PERFORMANCE_MONITORING=true
```
### 日志配置
| 变量 | 默认值 | 描述 |
| ------------------ | ------------ | -------------------------------- |
| `LOG_FORMAT` | `json` | 日志格式(`json`、`text` |
| `LOG_FILE` | - | 日志文件路径(如果启用文件日志) |
| `LOG_MAX_SIZE` | `10m` | 最大日志文件大小 |
| `LOG_MAX_FILES` | `5` | 最大日志文件数 |
| `LOG_DATE_PATTERN` | `YYYY-MM-DD` | 日志轮换日期模式 |
```env
LOG_FORMAT=json
LOG_FILE=./logs/mcphub.log
LOG_MAX_SIZE=10m
LOG_MAX_FILES=5
LOG_DATE_PATTERN=YYYY-MM-DD
```
## 开发与调试
| 变量 | 默认值 | 描述 |
| ------------------------ | ------- | ------------------------------- |
| `DEBUG` | - | 调试命名空间(例如 `mcphub:*` |
| `DEV_TOOLS_ENABLED` | `false` | 启用开发工具 |
| `HOT_RELOAD` | `true` | 在开发中启用热重载 |
| `MOCK_EXTERNAL_SERVICES` | `false` | 模拟外部 API 调用 |
```env
DEBUG=mcphub:*
DEV_TOOLS_ENABLED=true
HOT_RELOAD=true
MOCK_EXTERNAL_SERVICES=false
```
## 生产优化
| 变量 | 默认值 | 描述 |
| ------------------ | ------- | ---------------------- |
| `CLUSTER_MODE` | `false` | 启用集群模式 |
| `WORKER_PROCESSES` | `0` | 工作进程数0 = 自动) |
| `MEMORY_LIMIT` | - | 每个进程内存限制 |
| `CPU_LIMIT` | - | 每个进程 CPU 限制 |
| `GC_OPTIMIZE` | `false` | 启用垃圾回收优化 |
```env
CLUSTER_MODE=true
WORKER_PROCESSES=4
MEMORY_LIMIT=512M
GC_OPTIMIZE=true
```
## 配置示例
@@ -276,22 +49,9 @@ GC_OPTIMIZE=true
# .env.development
NODE_ENV=development
PORT=3000
LOG_LEVEL=debug
# 数据库
DATABASE_URL=postgresql://mcphub:password@localhost:5432/mcphub_dev
# 认证
JWT_SECRET=dev-secret-key
JWT_EXPIRES_IN=24h
# OpenAI开发时可选
# OPENAI_API_KEY=your-dev-key
# 调试
DEBUG=mcphub:*
DEV_TOOLS_ENABLED=true
HOT_RELOAD=true
```
### 生产环境
@@ -300,30 +60,9 @@ HOT_RELOAD=true
# .env.production
NODE_ENV=production
PORT=3000
LOG_LEVEL=info
LOG_FORMAT=json
# 数据库
DATABASE_URL=postgresql://mcphub:secure-password@db.example.com:5432/mcphub
DB_SSL=true
DB_POOL_MAX=20
# 安全
JWT_SECRET=your-super-secure-production-secret
SESSION_SECRET=your-session-secret
BCRYPT_ROUNDS=14
# 外部服务
OPENAI_API_KEY=your-production-openai-key
REDIS_URL=redis://redis.example.com:6379
# 监控
METRICS_ENABLED=true
PERFORMANCE_MONITORING=true
# 优化
CLUSTER_MODE=true
GC_OPTIMIZE=true
```
### Docker 环境
@@ -331,21 +70,10 @@ GC_OPTIMIZE=true
```env
# .env.docker
NODE_ENV=production
HOST=0.0.0.0
PORT=3000
# 使用 Docker 网络的服务名
DATABASE_URL=postgresql://mcphub:password@postgres:5432/mcphub
REDIS_URL=redis://redis:6379
# 安全
JWT_SECRET_FILE=/run/secrets/jwt_secret
DB_PASSWORD_FILE=/run/secrets/db_password
# 容器中的文件路径
MCP_SETTINGS_FILE=/app/mcp_settings.json
UPLOAD_DIR=/app/data/uploads
LOG_FILE=/app/logs/mcphub.log
```
## 环境变量加载
@@ -353,8 +81,8 @@ LOG_FILE=/app/logs/mcphub.log
MCPHub 按以下顺序加载环境变量:
1. 系统环境变量
2. `.env.local`被 git 忽略
3. `.env.{NODE_ENV}`例如 `.env.production`
2. `.env.local` (被 git 忽略)
3. `.env.{NODE_ENV}` (例如, `.env.production`)
4. `.env`
### 使用 dotenv-expand
@@ -364,26 +92,13 @@ MCPHub 支持变量扩展:
```env
BASE_URL=https://api.example.com
API_ENDPOINT=${BASE_URL}/v1
DATABASE_URL=postgresql://${DB_USER}:${DB_PASSWORD}@${DB_HOST}:${DB_PORT}/${DB_NAME}
```
## 安全最佳实践
1. **永远不要提交密钥**到版本控制
2. **为生产使用强唯一密钥**
1. **永远不要将密钥提交**到版本控制
2. **为生产环境使用强大、独特的密钥**
3. **定期轮换密钥**
4. **使用特定于环境的文件**
5. **在启动时验证所有环境变量**
6. **为容器部署使用 Docker 密钥**
## 验证
MCPHub 在启动时验证环境变量。无效配置将阻止应用程序启动并提供有用的错误消息。
生产环境必需变量:
- `JWT_SECRET`
- `DATABASE_URL` 或单独的数据库组件
- `OPENAI_API_KEY`(如果启用智能路由)
这个全面的环境配置确保 MCPHub 可以为任何部署场景正确配置。

File diff suppressed because it is too large Load Diff

141
docs/zh/features/oauth.mdx Normal file
View File

@@ -0,0 +1,141 @@
# OAuth 支持
## 核心亮点
- 覆盖上游 MCP 服务器的 OAuth 2.0 授权码PKCE全流程。
- 支持从 `WWW-Authenticate` 响应和 RFC 8414 元数据自动发现。
- 实现动态客户端注册RFC 7591以及资源指示RFC 8707
- 会将客户端凭据与令牌持久化到 `mcp_settings.json`,重启后直接复用。
## MCPHub 何时启用 OAuth
1. MCPHub 调用需要授权的 MCP 服务器并收到 `401 Unauthorized`。
2. 响应通过 `WWW-Authenticate` header 暴露受保护资源的元数据(`authorization_server` 或 `as_uri`)。
3. MCPHub 自动发现授权服务器、按需注册客户端,并引导用户完成一次授权。
4. 回调处理完成后MCPHub 使用新令牌重新连接并继续请求。
> MCPHub 会在服务器详情视图和后端日志中记录发现、注册、授权链接、换取令牌等关键步骤。
## 按服务器类型快速上手
### 支持动态注册的服务器
部分服务器会公开完整的 OAuth 元数据,并允许客户端动态注册。例如 Vercel 与 Linear 的 MCP 服务器只需配置 SSE 地址即可:
```json
{
"mcpServers": {
"vercel": {
"type": "sse",
"url": "https://mcp.vercel.com"
},
"linear": {
"type": "sse",
"url": "https://mcp.linear.app/mcp"
}
}
}
```
- MCPHub 会自动发现授权服务器、完成注册,并处理整个 PKCE 流程。
- 所有凭据与令牌会写入 `mcp_settings.json`,无须在控制台额外配置。
### 需要手动配置客户端的服务器
另有一些服务端并不支持动态注册。GitHub 的 MCP 端点(`https://api.githubcopilot.com/mcp/`)就是典型例子,接入步骤如下:
1. 在服务提供商控制台创建 OAuth 应用GitHub 路径为 **Settings → Developer settings → OAuth Apps**)。
2. 将回调/重定向地址设置为 `http://localhost:3000/oauth/callback`(或你的部署域名)。
3. 复制生成的 Client ID 与 Client Secret。
4. 通过 MCPHub 控制台或直接编辑 `mcp_settings.json` 写入如下配置。
```json
{
"mcpServers": {
"github": {
"type": "sse",
"url": "https://api.githubcopilot.com/mcp/",
"oauth": {
"clientId": "${GITHUB_OAUTH_APP_ID}",
"clientSecret": "${GITHUB_OAUTH_APP_SECRET}",
"scopes": ["replace-with-provider-scope"],
"resource": "https://api.githubcopilot.com"
}
}
}
}
```
- MCPHub 会跳过动态注册,直接使用你提供的凭据完成授权流程。
- 凭据轮换时需要同步更新控制台或配置文件。
- 将 `scopes` 替换为服务端要求的具体 Scope。
## 配置方式
大多数场景可依赖自动检测,也可以在 `mcp_settings.json` 中显式声明 OAuth 配置。只填写确实需要的字段。
### 自动检测(最小配置)
```json
{
"mcpServers": {
"secured-sse": {
"type": "sse",
"url": "https://mcp.example.com/sse",
"oauth": {
"scopes": ["mcp.tools", "mcp.prompts"],
"resource": "https://mcp.example.com"
}
}
}
}
```
- MCPHub 会根据挑战头自动发现授权服务器,并引导用户完成授权。
- 令牌(包含刷新令牌)会写入磁盘,重启后自动复用。
### 静态客户端凭据(自带 Client
```json
{
"oauth": {
"clientId": "mcphub-client",
"clientSecret": "replace-me-if-required",
"authorizationEndpoint": "https://auth.example.com/oauth/authorize",
"tokenEndpoint": "https://auth.example.com/oauth/token",
"redirectUri": "http://localhost:3000/oauth/callback"
}
}
```
- 适用于需要手动注册客户端的授权服务器。
- `redirectUri` 默认是 `http://localhost:3000/oauth/callback`,如在自定义域部署请同步更新。
### 动态客户端注册RFC 7591
```json
{
"oauth": {
"dynamicRegistration": {
"enabled": true,
"issuer": "https://auth.example.com",
"metadata": {
"client_name": "MCPHub",
"redirect_uris": [
"http://localhost:3000/oauth/callback",
"https://mcphub.example.com/oauth/callback"
],
"scope": "mcp.tools mcp.prompts",
"grant_types": ["authorization_code", "refresh_token"]
},
"initialAccessToken": "optional-token-if-required"
},
"scopes": ["mcp.tools", "mcp.prompts"],
"resource": "https://mcp.example.com"
}
}
```
- MCPHub 会通过 `issuer` 发现端点、完成注册,并持久化下发的 `client_id`/`client_secret`。
- 只有当注册端点受保护时才需要提供 `initialAccessToken`。
## 授权流程
1. **初始化**:启动时遍历服务器配置,发现元数据并在启用 `dynamicRegistration` 时注册客户端。
2. **用户授权**:建立连接时自动打开系统浏览器,携带 PKCE 参数访问授权页。
3. **回调处理**:内置路径 `/oauth/callback` 校验 `state`、完成换取令牌,并通过 MCP SDK 保存结果。
4. **令牌生命周期**:访问令牌与刷新令牌会缓存于内存,自动刷新,并写回 `mcp_settings.json`。
## 提示与排障
- 确保授权过程中使用的回调地址与已注册的 `redirect_uris` 完全一致。
- 若部署在 HTTPS 域名下,请对外暴露 `/oauth/callback` 或通过反向代理转发。
- 如无法完成自动发现,可显式提供 `authorizationEndpoint` 与 `tokenEndpoint`。
- 授权服务器吊销令牌后,可手动清理 `mcp_settings.json` 中的旧令牌MCPHub 会在下一次请求时重新触发授权。

View File

@@ -49,448 +49,369 @@ curl -X POST http://localhost:3000/api/servers \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_JWT_TOKEN" \
-d '{
"name": "my-server",
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-filesystem", "/path/to/files"],
"env": {
"NODE_ENV": "production"
},
"cwd": "/app"
"name": "fetch-server",
"command": "uvx",
"args": ["mcp-server-fetch"],
"env": {}
}'
```
## 服务器配置
## 流行的 MCP 服务器示例
### 通用配置选项
<AccordionGroup>
<Accordion title="Web 抓取服务器">
提供网页抓取和 HTTP 请求功能:
```json
{
"name": "filesystem-server",
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-filesystem", "/allowed/path"],
"env": {
"NODE_ENV": "production",
"DEBUG": "mcp:*",
"MAX_FILES": "1000"
},
"cwd": "/app/workspace",
"timeout": 30000,
"retries": 3,
"enabled": true
}
```
```json
{
"fetch": {
"command": "uvx",
"args": ["mcp-server-fetch"]
}
}
```
### Python 服务器示例
**可用工具:**
- `fetch`: 发起 HTTP 请求
- `fetch_html`: 抓取网页
- `fetch_json`: 从 API 获取 JSON 数据
```json
{
"name": "python-server",
"command": "python",
"args": ["-m", "mcp_server", "--config", "config.json"],
"env": {
"PYTHONPATH": "/app/python",
"API_KEY": "${API_KEY}",
"LOG_LEVEL": "INFO"
},
"cwd": "/app/python-server"
}
```
</Accordion>
### Node.js 服务器示例
<Accordion title="Playwright 浏览器自动化">
用于网页交互的浏览器自动化:
```json
{
"name": "node-server",
"command": "node",
"args": ["server.js", "--port", "3001"],
"env": {
"NODE_ENV": "production",
"PORT": "3001",
"DATABASE_URL": "${DATABASE_URL}"
},
"cwd": "/app/node-server"
}
```
```json
{
"playwright": {
"command": "npx",
"args": ["@playwright/mcp@latest", "--headless"]
}
}
```
**可用工具:**
- `playwright_navigate`: 导航到网页
- `playwright_screenshot`: 截取屏幕截图
- `playwright_click`: 点击元素
- `playwright_fill`: 填写表单
</Accordion>
<Accordion title="文件系统操作">
文件和目录管理:
```json
{
"filesystem": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-filesystem", "/allowed/path"]
}
}
```
**可用工具:**
- `read_file`: 读取文件内容
- `write_file`: 写入文件
- `create_directory`: 创建目录
- `list_directory`: 列出目录内容
</Accordion>
<Accordion title="SQLite 数据库">
数据库操作:
```json
{
"sqlite": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-sqlite", "/path/to/database.db"]
}
}
```
**可用工具:**
- `execute_query`: 执行 SQL 查询
- `describe_tables`: 获取表结构
- `create_table`: 创建新表
</Accordion>
<Accordion title="Slack 集成">
Slack 工作空间集成:
```json
{
"slack": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-slack"],
"env": {
"SLACK_BOT_TOKEN": "xoxb-your-bot-token",
"SLACK_TEAM_ID": "T1234567890"
}
}
}
```
**可用工具:**
- `send_slack_message`: 发送消息到频道
- `list_slack_channels`: 列出可用频道
- `get_slack_thread`: 获取线程消息
</Accordion>
<Accordion title="GitHub 集成">
GitHub 仓库操作:
```json
{
"github": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-github"],
"env": {
"GITHUB_PERSONAL_ACCESS_TOKEN": "ghp_your_token"
}
}
}
```
**可用工具:**
- `create_or_update_file`: 创建/更新仓库文件
- `search_repositories`: 搜索 GitHub 仓库
- `create_issue`: 创建问题
- `create_pull_request`: 创建拉取请求
</Accordion>
<Accordion title="Google Drive">
Google Drive 文件操作:
```json
{
"gdrive": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-gdrive"],
"env": {
"GDRIVE_CLIENT_ID": "your-client-id",
"GDRIVE_CLIENT_SECRET": "your-client-secret",
"GDRIVE_REDIRECT_URI": "your-redirect-uri"
}
}
}
```
**可用工具:**
- `gdrive_search`: 搜索文件和文件夹
- `gdrive_read`: 读取文件内容
- `gdrive_create`: 创建新文件
</Accordion>
<Accordion title="高德地图(中国)">
中国地图和位置服务:
```json
{
"amap": {
"command": "npx",
"args": ["-y", "@amap/amap-maps-mcp-server"],
"env": {
"AMAP_MAPS_API_KEY": "your-api-key"
}
}
}
```
**可用工具:**
- `search_location`: 搜索位置
- `get_directions`: 获取路线指引
- `reverse_geocode`: 将坐标转换为地址
</Accordion>
</AccordionGroup>
## 服务器生命周期管理
### 启动服务器
```bash
# 启动特定服务器
curl -X POST http://localhost:3000/api/servers/my-server/start \
-H "Authorization: Bearer $TOKEN"
服务器会在以下情况下自动启动:
# 启动所有服务器
curl -X POST http://localhost:3000/api/servers/start-all \
-H "Authorization: Bearer $TOKEN"
```
- MCPHub 启动时
- 通过仪表板或 API 添加服务器时
- 服务器配置更新时
- 手动重启已停止的服务器时
### 停止服务器
```bash
# 停止特定服务器
curl -X POST http://localhost:3000/api/servers/my-server/stop \
-H "Authorization: Bearer $TOKEN"
您可以通过以下方式停止服务器:
# 优雅停止(等待当前请求完成)
curl -X POST http://localhost:3000/api/servers/my-server/stop \
-H "Authorization: Bearer $TOKEN" \
-d '{"graceful": true, "timeout": 30000}'
```
- **通过仪表板**: 切换服务器状态开关
- **通过 API**: 发送 POST 请求到 `/api/servers/{name}/toggle`
- **自动停止**: 服务器崩溃或遇到错误时会自动停止
### 重启服务器
```bash
# 重启服务器
curl -X POST http://localhost:3000/api/servers/my-server/restart \
-H "Authorization: Bearer $TOKEN"
```
服务器会在以下情况下自动重启:
## 热配置重载
### 更新服务器配置
无需重启即可更新配置:
```bash
curl -X PUT http://localhost:3000/api/servers/my-server/config \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $TOKEN" \
-d '{
"env": {
"DEBUG": "mcp:verbose",
"NEW_SETTING": "value"
},
"args": ["--verbose", "--new-flag"]
}'
```
### 批量配置更新
```bash
curl -X PUT http://localhost:3000/api/servers/bulk-update \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $TOKEN" \
-d '{
"servers": ["server1", "server2"],
"config": {
"env": {
"LOG_LEVEL": "DEBUG"
}
}
}'
```
- 配置更改时
- 环境变量更新后
- 通过仪表板或 API 手动触发时
## 服务器状态监控
### 检查服务器状态
### 状态指示器
```bash
# 获取所有服务器状态
curl -X GET http://localhost:3000/api/servers/status \
-H "Authorization: Bearer $TOKEN"
每个服务器都显示状态指示器:
# 获取特定服务器状态
curl -X GET http://localhost:3000/api/servers/my-server/status \
-H "Authorization: Bearer $TOKEN"
```
- 🟢 **运行中**: 服务器处于活动状态并响应
- 🟡 **启动中**: 服务器正在初始化
- 🔴 **已停止**: 服务器未运行
- ⚠️ **错误**: 服务器遇到错误
响应示例:
### 实时日志
```json
{
"name": "my-server",
"status": "running",
"pid": 12345,
"uptime": 3600000,
"memory": {
"rss": 123456789,
"heapTotal": 98765432,
"heapUsed": 87654321
},
"cpu": {
"user": 1000000,
"system": 500000
},
"lastRestart": "2024-01-01T12:00:00.000Z"
}
```
实时查看服务器日志:
1. **仪表板日志**: 点击服务器查看其日志
2. **API 日志**: 通过 `/api/logs` 端点访问日志
3. **流式日志**: 通过 WebSocket 订阅日志流
### 健康检查
配置自动健康检查:
MCPHub 自动执行健康检查:
- **初始化检查**: 验证服务器成功启动
- **工具发现**: 确认检测到可用工具
- **响应检查**: 测试服务器响应性
- **资源监控**: 跟踪 CPU 和内存使用情况
## 配置管理
### 环境变量
服务器可以使用环境变量进行配置:
```json
{
"name": "my-server",
"command": "node",
"args": ["server.js"],
"healthCheck": {
"enabled": true,
"interval": 30000,
"timeout": 5000,
"retries": 3,
"endpoint": "/health",
"expectedStatus": 200
}
}
```
## 负载均衡
### 配置多实例
```json
{
"name": "load-balanced-server",
"instances": 3,
"command": "node",
"args": ["server.js"],
"loadBalancer": {
"strategy": "round-robin",
"healthCheck": true,
"stickySession": false
},
"env": {
"PORT": "${PORT}"
}
}
```
### 负载均衡策略
- **round-robin**: 轮询分发请求
- **least-connections**: 分发到连接数最少的实例
- **weighted**: 基于权重分发
- **ip-hash**: 基于客户端 IP 的一致性哈希
## 资源限制
### 设置资源限制
```json
{
"name": "resource-limited-server",
"command": "python",
"args": ["server.py"],
"resources": {
"memory": {
"limit": "512MB",
"warning": "400MB"
},
"cpu": {
"limit": "50%",
"priority": "normal"
},
"processes": {
"max": 10
"server-name": {
"command": "python",
"args": ["server.py"],
"env": {
"API_KEY": "${YOUR_API_KEY}",
"DEBUG": "true",
"MAX_CONNECTIONS": "10"
}
}
}
```
### 监控资源使用
**环境变量展开:**
```bash
# 获取资源使用统计
curl -X GET http://localhost:3000/api/servers/my-server/resources \
-H "Authorization: Bearer $TOKEN"
```
- `${VAR_NAME}`: 展开为环境变量值
- `${VAR_NAME:-default}`: 如果变量未设置则使用默认值
- `${VAR_NAME:+value}`: 如果变量已设置则使用指定值
## 日志管理
### 命令变体
### 配置日志记录
指定服务器命令的不同方式:
```json
{
"name": "my-server",
"command": "node",
"args": ["server.js"],
"logging": {
"level": "info",
"file": "/var/log/mcphub/my-server.log",
"maxSize": "100MB",
"maxFiles": 5,
"rotate": true,
"format": "json"
}
}
```
### 查看日志
```bash
# 获取实时日志
curl -X GET http://localhost:3000/api/servers/my-server/logs \
-H "Authorization: Bearer $TOKEN"
# 获取带过滤器的日志
curl -X GET "http://localhost:3000/api/servers/my-server/logs?level=error&limit=100" \
-H "Authorization: Bearer $TOKEN"
```
## 环境变量管理
### 动态环境变量
```json
{
"name": "dynamic-server",
"command": "python",
"args": ["server.py"],
"env": {
"API_KEY": "${secrets:api_key}",
"DATABASE_URL": "${vault:db_url}",
"CURRENT_TIME": "${time:iso}",
"SERVER_ID": "${server:id}",
"HOSTNAME": "${system:hostname}"
}
}
```
### 环境变量模板
支持的模板变量:
- `${secrets:key}`: 从密钥存储获取
- `${vault:path}`: 从 Vault 获取
- `${env:VAR}`: 从系统环境变量获取
- `${time:format}`: 当前时间戳
- `${server:property}`: 服务器属性
- `${system:property}`: 系统属性
## 服务发现
### 自动服务发现
```json
{
"serviceDiscovery": {
"enabled": true,
"provider": "consul",
"config": {
"host": "localhost",
"port": 8500,
"serviceName": "mcp-server",
"tags": ["mcp", "ai", "api"]
}
}
}
```
### 注册服务
```bash
# 手动注册服务
curl -X POST http://localhost:3000/api/servers/my-server/register \
-H "Authorization: Bearer $TOKEN" \
-d '{
"service": {
"name": "my-mcp-service",
"tags": ["mcp", "production"],
"port": 3001,
"check": {
"http": "http://localhost:3001/health",
"interval": "30s"
<Tabs>
<Tab title="npm/npx">
```json
{
"npm-server": {
"command": "npx",
"args": ["-y", "package-name", "--option", "value"]
}
}
}'
```
```
</Tab>
<Tab title="Python/uvx">
```json
{
"python-server": {
"command": "uvx",
"args": ["package-name", "--config", "config.json"]
}
}
```
</Tab>
</Tabs>
## 高级功能
### 热重载
MCPHub 支持服务器配置的热重载:
1. **仪表板更新**: 立即应用通过 Web 界面进行的更改
2. **API 更新**: 通过 REST API 调用进行实时更新
3. **零停机时间**: 优雅的服务器重启,不影响其他服务器
## 故障排除
### 常见问题
<AccordionGroup>
<Accordion title="服务器无法启动">
**检查以下项目:**
- 命令在 PATH 中可用
- 已设置所有必需的环境变量
- 工作目录存在且可访问
- 网络端口未被阻塞
- 依赖项已安装
1. **服务器启动失败**
**调试步骤:**
1. 在仪表板中检查服务器日志
2. 在终端中手动测试命令
3. 验证环境变量展开
4. 检查文件权限
```bash
# 检查服务器日志
curl -X GET http://localhost:3000/api/servers/my-server/logs?level=error \
-H "Authorization: Bearer $TOKEN"
```
</Accordion>
2. **配置无效**
<Accordion title="服务器持续崩溃">
**常见原因:**
- 无效的配置参数
- 缺少 API 密钥或凭据
- 超出资源限制
- 依赖项冲突
```bash
# 验证配置
curl -X POST http://localhost:3000/api/servers/validate \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $TOKEN" \
-d @server-config.json
```
**解决方案:**
1. 查看服务器日志中的错误消息
2. 使用最小配置进行测试
3. 验证所有凭据和 API 密钥
4. 检查系统资源可用性
3. **性能问题**
```bash
# 获取性能指标
curl -X GET http://localhost:3000/api/servers/my-server/metrics \
-H "Authorization: Bearer $TOKEN"
```
</Accordion>
### 调试模式
<Accordion title="工具未显示">
**可能的问题:**
- 服务器未完全初始化
- 工具发现超时
- 通信协议不匹配
- 服务器报告错误
启用详细调试:
**调试步骤:**
1. 等待服务器初始化完成
2. 检查服务器日志中的工具注册消息
3. 测试与服务器的直接通信
4. 验证 MCP 协议兼容性
```json
{
"name": "debug-server",
"command": "node",
"args": ["--inspect=0.0.0.0:9229", "server.js"],
"env": {
"DEBUG": "*",
"LOG_LEVEL": "debug",
"NODE_ENV": "development"
},
"debugging": {
"enabled": true,
"port": 9229,
"breakOnStart": false
}
}
```
</Accordion>
</AccordionGroup>
## 高级配置
## 下一步
### 自定义钩子
```json
{
"name": "hooked-server",
"command": "node",
"args": ["server.js"],
"hooks": {
"beforeStart": ["./scripts/setup.sh"],
"afterStart": ["./scripts/notify.sh"],
"beforeStop": ["./scripts/cleanup.sh"],
"onError": ["./scripts/alert.sh"]
}
}
```
### 配置模板
```json
{
"templates": {
"python-server": {
"command": "python",
"args": ["-m", "mcp_server"],
"env": {
"PYTHONPATH": "/app/python",
"LOG_LEVEL": "INFO"
}
}
},
"servers": {
"my-python-server": {
"extends": "python-server",
"args": ["-m", "mcp_server", "--config", "custom.json"],
"env": {
"API_KEY": "custom-key"
}
}
}
}
```
有关更多配置选项,请参阅 [MCP 设置配置](/zh/configuration/mcp-settings) 和 [环境变量](/zh/configuration/environment-variables) 文档。
<CardGroup cols={2}>
<Card title="分组管理" icon="users" href="/zh/features/group-management">
将服务器组织成逻辑分组
</Card>
<Card title="智能路由" icon="route" href="/zh/features/smart-routing">
设置 AI 驱动的工具发现
</Card>
<Card title="API 参考" icon="code" href="/zh/api-reference/servers">
服务器管理 API 文档
</Card>
<Card title="配置指南" icon="cog" href="/zh/configuration/mcp-settings">
详细配置选项
</Card>
</CardGroup>

View File

@@ -1,691 +1,367 @@
---
title: '智能路由'
description: '自动负载均衡和请求路由到最佳的 MCP 服务器实例'
description: '使用向量语义搜索的 AI 工具发现系统'
---
## 概述
MCPHub 的智能路由系统自动将传入请求路由到最适合的 MCP 服务器实例。系统考虑服务器负载、响应时间、功能可用性和业务规则来做出路由决策
智能路由是 MCPHub 的智能工具发现系统它使用向量语义搜索来自动找到与任何给定任务最相关的工具。AI 客户端无需手动指定使用哪些工具,只需描述他们想要完成的任务,智能路由就会识别并提供对最合适工具的访问
## 路由策略
## 智能路由的工作原理
### 轮询路由
### 1. 工具索引
最简单的路由策略,按顺序分发请求
当服务器启动时,智能路由会自动
```json
{
"routing": {
"strategy": "round-robin",
"targets": [
{
"serverId": "server-1",
"weight": 1,
"enabled": true
},
{
"serverId": "server-2",
"weight": 1,
"enabled": true
},
{
"serverId": "server-3",
"weight": 1,
"enabled": true
}
]
}
}
```
- 从 MCP 服务器发现所有可用工具
- 提取工具元数据(名称、描述、参数)
- 将工具信息转换为向量嵌入
- 使用 pgvector 将嵌入存储在 PostgreSQL 中
### 加权轮询
### 2. 语义搜索
基于服务器容量分配不同权重
当进行查询时
```json
{
"routing": {
"strategy": "weighted-round-robin",
"targets": [
{
"serverId": "high-performance-server",
"weight": 3,
"specs": {
"cpu": "8 cores",
"memory": "32GB"
}
},
{
"serverId": "standard-server-1",
"weight": 2,
"specs": {
"cpu": "4 cores",
"memory": "16GB"
}
},
{
"serverId": "standard-server-2",
"weight": 1,
"specs": {
"cpu": "2 cores",
"memory": "8GB"
}
}
]
}
}
```
- 用户查询被转换为向量嵌入
- 相似性搜索使用余弦相似度找到匹配的工具
- 动态阈值过滤掉不相关的结果
- 结果按相关性得分排序
### 最少连接数
### 3. 智能过滤
将请求路由到当前连接数最少的服务器:
智能路由应用多个过滤器:
```json
{
"routing": {
"strategy": "least-connections",
"balancingMode": "dynamic",
"healthCheck": {
"enabled": true,
"interval": 10000
}
}
}
```
- **相关性阈值**:只返回高于相似性阈值的工具
- **上下文感知**:考虑对话上下文
- **工具可用性**:确保工具当前可访问
- **权限过滤**:尊重用户访问权限
### 基于响应时间
### 4. 工具执行
路由到响应时间最短的服务器
找到的工具可以直接执行
```json
{
"routing": {
"strategy": "fastest-response",
"metrics": {
"measurementWindow": "5m",
"sampleSize": 100,
"excludeSlowRequests": true,
"slowRequestThreshold": "5s"
}
}
}
```
- 参数验证确保正确的工具使用
- 错误处理提供有用的反馈
- 响应格式保持一致性
- 日志记录跟踪工具使用情况进行分析
## 基于功能的路由
## 前置条件
### 工具特定路由
智能路由需要比基础 MCPHub 使用更多的设置:
根据请求的工具类型路由到专门的服务器:
### 必需组件
```json
{
"routing": {
"strategy": "capability-based",
"rules": [
{
"condition": {
"tool": "filesystem"
},
"targets": ["filesystem-server-1", "filesystem-server-2"],
"strategy": "least-connections"
},
{
"condition": {
"tool": "web-search"
},
"targets": ["search-server-1", "search-server-2"],
"strategy": "round-robin"
},
{
"condition": {
"tool": "database"
},
"targets": ["db-server"],
"strategy": "single"
}
],
"fallback": {
"targets": ["general-server-1", "general-server-2"],
"strategy": "round-robin"
}
}
}
```
1. **带有 pgvector 的 PostgreSQL**:用于嵌入存储的向量数据库
2. **嵌入服务**OpenAI API 或兼容服务
3. **环境配置**:正确的配置变量
### 内容感知路由
## 使用智能路由
基于请求内容进行智能路由
### 智能路由端点
```json
{
"routing": {
"strategy": "content-aware",
"rules": [
{
"condition": {
"content.language": "python"
},
"targets": ["python-specialized-server"],
"reason": "Python代码分析专用服务器"
},
{
"condition": {
"content.size": "> 1MB"
},
"targets": ["high-memory-server"],
"reason": "大文件处理专用服务器"
},
{
"condition": {
"content.type": "image"
},
"targets": ["image-processing-server"],
"reason": "图像处理专用服务器"
}
]
}
}
```
通过特殊的 `$smart` 端点访问智能路由:
## 地理位置路由
<Tabs>
<Tab title="HTTP MCP">
```
http://localhost:3000/mcp/$smart
```
</Tab>
### 基于客户端位置
<Tab title="SSE (Legacy)">
```
http://localhost:3000/sse/$smart
```
</Tab>
</Tabs>
根据客户端地理位置路由到最近的服务器:
{/* ## 性能优化
```json
{
"routing": {
"strategy": "geo-location",
"regions": [
{
"name": "北美",
"countries": ["US", "CA", "MX"],
"servers": ["us-east-1", "us-west-1", "ca-central-1"],
"strategy": "least-latency"
},
{
"name": "欧洲",
"countries": ["DE", "FR", "UK", "NL"],
"servers": ["eu-west-1", "eu-central-1"],
"strategy": "round-robin"
},
{
"name": "亚太",
"countries": ["CN", "JP", "KR", "SG"],
"servers": ["ap-southeast-1", "ap-northeast-1"],
"strategy": "fastest-response"
}
],
"fallback": {
"servers": ["global-server-1"],
"strategy": "single"
}
}
}
```
### 嵌入缓存
### 延迟优化
智能路由缓存嵌入以提高性能:
```bash
# 配置延迟监控
curl -X PUT http://localhost:3000/api/routing/latency-config \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $TOKEN" \
-d '{
# 配置缓存设置
EMBEDDING_CACHE_TTL=3600 # 缓存 1 小时
EMBEDDING_CACHE_SIZE=10000 # 最多缓存 10k 个嵌入
EMBEDDING_CACHE_CLEANUP=300 # 每 5 分钟清理一次
```
### 批处理
工具批量索引以提高效率:
```bash
# 嵌入生成的批大小
EMBEDDING_BATCH_SIZE=100
# 并发嵌入请求
EMBEDDING_CONCURRENCY=5
# 索引更新频率
INDEX_UPDATE_INTERVAL=3600 # 每小时重新索引
```
### 数据库优化
为向量操作优化 PostgreSQL
```sql
-- 创建索引以获得更好的性能
CREATE INDEX ON tool_embeddings USING hnsw (embedding vector_cosine_ops);
-- 调整 PostgreSQL 设置
ALTER SYSTEM SET shared_preload_libraries = 'vector';
ALTER SYSTEM SET max_connections = 200;
ALTER SYSTEM SET shared_buffers = '256MB';
ALTER SYSTEM SET effective_cache_size = '1GB';
```
## 监控和分析
### 智能路由指标
监控智能路由性能:
```bash
# 获取智能路由统计信息
curl http://localhost:3000/api/smart-routing/stats \
-H "Authorization: Bearer YOUR_JWT_TOKEN"
```
响应包括:
- 查询计数和频率
- 平均响应时间
- 嵌入缓存命中率
- 最受欢迎的工具
- 查询模式
### 工具使用分析
跟踪哪些工具被发现和使用:
```bash
# 获取工具使用分析
curl http://localhost:3000/api/smart-routing/analytics \
-H "Authorization: Bearer YOUR_JWT_TOKEN"
```
指标包括:
- 工具发现率
- 执行成功率
- 用户满意度评分
- 查询到执行的转换率
### 性能监控
监控系统性能:
```bash
# 数据库性能
curl http://localhost:3000/api/smart-routing/db-stats \
-H "Authorization: Bearer YOUR_JWT_TOKEN"
# 嵌入服务状态
curl http://localhost:3000/api/smart-routing/embedding-stats \
-H "Authorization: Bearer YOUR_JWT_TOKEN"
```
## 高级功能
### 自定义嵌入
使用自定义嵌入模型:
```bash
# Hugging Face 模型
EMBEDDING_SERVICE=huggingface
HUGGINGFACE_MODEL=sentence-transformers/all-MiniLM-L6-v2
HUGGINGFACE_API_KEY=your_api_key
# 本地嵌入服务
EMBEDDING_SERVICE=local
EMBEDDING_SERVICE_URL=http://localhost:8080/embeddings
```
### 查询增强
增强查询以获得更好的结果:
```json
{
"queryEnhancement": {
"enabled": true,
"measurementInterval": 30000,
"regions": [
{"id": "us-east", "endpoint": "ping.us-east.example.com"},
{"id": "eu-west", "endpoint": "ping.eu-west.example.com"},
{"id": "ap-southeast", "endpoint": "ping.ap-southeast.example.com"}
],
"routing": {
"preferLowLatency": true,
"maxLatencyThreshold": "200ms",
"fallbackOnTimeout": true
}
}'
```
## 负载感知路由
### 实时负载监控
```json
{
"routing": {
"strategy": "load-aware",
"loadMetrics": {
"cpu": {
"threshold": 80,
"weight": 0.4
},
"memory": {
"threshold": 85,
"weight": 0.3
},
"connections": {
"threshold": 1000,
"weight": 0.2
},
"responseTime": {
"threshold": "2s",
"weight": 0.1
}
},
"adaptation": {
"enabled": true,
"adjustmentInterval": 60000,
"emergencyThreshold": 95
}
"expandAcronyms": true,
"addSynonyms": true,
"contextualExpansion": true
}
}
```
### 预测性负载均衡
### 结果过滤
基于条件过滤结果:
```json
{
"routing": {
"strategy": "predictive",
"prediction": {
"algorithm": "linear-regression",
"trainingWindow": "7d",
"predictionHorizon": "1h",
"factors": ["historical_load", "time_of_day", "day_of_week", "seasonal_patterns"]
},
"adaptation": {
"preemptiveScaling": true,
"scaleUpThreshold": 70,
"scaleDownThreshold": 30
}
"resultFiltering": {
"minRelevanceScore": 0.7,
"maxResults": 10,
"preferredServers": ["fetch", "playwright"],
"excludeServers": ["deprecated-server"]
}
}
```
## 故障转移和恢复
### 反馈学习
### 自动故障转移
```json
{
"routing": {
"strategy": "high-availability",
"failover": {
"enabled": true,
"detection": {
"healthCheckFailures": 3,
"timeoutThreshold": "10s",
"checkInterval": 5000
},
"recovery": {
"automaticRecovery": true,
"recoveryChecks": 5,
"recoveryInterval": 30000
}
},
"clusters": [
{
"name": "primary",
"servers": ["server-1", "server-2"],
"priority": 1
},
{
"name": "secondary",
"servers": ["backup-server-1", "backup-server-2"],
"priority": 2
}
]
}
}
```
### 断路器模式
```json
{
"routing": {
"circuitBreaker": {
"enabled": true,
"failureThreshold": 10,
"timeWindow": 60000,
"halfOpenRetries": 3,
"fallback": {
"type": "cached-response",
"ttl": 300000
}
}
}
}
```
## 会话亲和性
### 粘性会话
保持用户会话与特定服务器的关联:
```json
{
"routing": {
"strategy": "session-affinity",
"affinity": {
"type": "cookie",
"cookieName": "mcphub-server-id",
"ttl": 3600000,
"fallbackOnUnavailable": true
},
"sessionStore": {
"type": "redis",
"config": {
"host": "localhost",
"port": 6379,
"db": 1
}
}
}
}
```
### 基于用户 ID 的路由
```json
{
"routing": {
"strategy": "user-based",
"userRouting": {
"algorithm": "consistent-hashing",
"hashFunction": "sha256",
"virtualNodes": 100,
"replicationFactor": 2
}
}
}
```
## 动态路由配置
### 运行时配置更新
基于用户反馈改进结果:
```bash
# 更新路由配置
curl -X PUT http://localhost:3000/api/routing/config \
# 对搜索结果提供反馈
curl -X POST http://localhost:3000/api/smart-routing/feedback \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $TOKEN" \
-H "Authorization: Bearer YOUR_JWT_TOKEN" \
-d '{
"strategy": "weighted-round-robin",
"weights": {
"server-1": 3,
"server-2": 2,
"server-3": 1
},
"applyImmediately": true
"queryId": "search-123",
"toolName": "fetch_html",
"rating": 5,
"successful": true,
"comments": "完美适合这个任务的工具"
}'
```
### A/B 测试路由
```json
{
"routing": {
"strategy": "ab-testing",
"experiments": [
{
"name": "new-algorithm-test",
"enabled": true,
"trafficSplit": {
"control": 70,
"variant": 30
},
"rules": {
"control": {
"strategy": "round-robin",
"servers": ["stable-server-1", "stable-server-2"]
},
"variant": {
"strategy": "ai-optimized",
"servers": ["experimental-server-1"]
}
},
"metrics": ["response_time", "error_rate", "user_satisfaction"]
}
]
}
}
```
## 路由分析和监控
### 实时路由指标
```bash
# 获取路由统计
curl -X GET http://localhost:3000/api/routing/metrics \
-H "Authorization: Bearer $TOKEN"
```
响应示例:
```json
{
"timestamp": "2024-01-01T12:00:00Z",
"totalRequests": 15420,
"routingDistribution": {
"server-1": { "requests": 6168, "percentage": 40 },
"server-2": { "requests": 4626, "percentage": 30 },
"server-3": { "requests": 3084, "percentage": 20 },
"backup-server": { "requests": 1542, "percentage": 10 }
},
"performance": {
"avgResponseTime": "245ms",
"p95ResponseTime": "580ms",
"errorRate": "0.3%"
},
"failovers": {
"total": 2,
"byServer": {
"server-2": 1,
"server-3": 1
}
}
}
```
### 路由决策日志
```bash
# 启用路由决策日志
curl -X PUT http://localhost:3000/api/routing/logging \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $TOKEN" \
-d '{
"enabled": true,
"level": "info",
"includeDecisionFactors": true,
"sampleRate": 0.1
}'
```
## 自定义路由规则
### 基于业务逻辑的路由
```json
{
"routing": {
"strategy": "custom-rules",
"rules": [
{
"name": "premium-users",
"priority": 1,
"condition": "user.tier === 'premium'",
"action": {
"targetServers": ["premium-server-1", "premium-server-2"],
"strategy": "least-connections",
"qos": {
"maxResponseTime": "1s",
"priority": "high"
}
}
},
{
"name": "high-volume-requests",
"priority": 2,
"condition": "request.size > 10MB",
"action": {
"targetServers": ["high-capacity-server"],
"strategy": "single",
"timeout": "60s"
}
},
{
"name": "batch-processing",
"priority": 3,
"condition": "request.type === 'batch'",
"action": {
"targetServers": ["batch-server-1", "batch-server-2"],
"strategy": "queue-based",
"queueConfig": {
"maxSize": 1000,
"timeout": "5m"
}
}
}
]
}
}
```
### JavaScript 路由函数
```javascript
// 自定义路由函数
function customRouting(request, servers, metrics) {
const { user, content, timestamp } = request;
// 工作时间优先使用高性能服务器
const isBusinessHours =
new Date(timestamp).getHours() >= 9 && new Date(timestamp).getHours() <= 17;
if (isBusinessHours && user.priority === 'high') {
return servers.filter((s) => s.tags.includes('high-performance'));
}
// 基于内容类型的特殊路由
if (content.type === 'code-analysis') {
return servers.filter((s) => s.capabilities.includes('code-analysis'));
}
// 默认负载均衡
return servers.sort((a, b) => a.currentLoad - b.currentLoad);
}
```
## 路由优化
### 机器学习优化
```json
{
"routing": {
"strategy": "ml-optimized",
"mlConfig": {
"algorithm": "reinforcement-learning",
"rewardFunction": "response_time_weighted",
"trainingData": {
"features": [
"server_load",
"response_time_history",
"request_complexity",
"user_pattern",
"time_of_day"
],
"targetMetric": "overall_satisfaction"
},
"updateFrequency": "hourly",
"explorationRate": 0.1
}
}
}
```
### 缓存感知路由
```json
{
"routing": {
"strategy": "cache-aware",
"caching": {
"enabled": true,
"levels": [
{
"type": "local",
"ttl": 300,
"maxSize": "100MB"
},
{
"type": "distributed",
"provider": "redis",
"ttl": 3600,
"maxSize": "1GB"
}
],
"routing": {
"preferCachedServers": true,
"cacheHitBonus": 0.3,
"cacheMissThreshold": 0.8
}
}
}
}
```
``` */}
## 故障排除
### 路由调试
<AccordionGroup>
<Accordion title="数据库连接问题">
**症状:**
- 智能路由不可用
- 数据库连接错误
- 嵌入存储失败
```bash
# 调试特定请求的路由决策
curl -X POST http://localhost:3000/api/routing/debug \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $TOKEN" \
-d '{
"request": {
"userId": "user123",
"tool": "filesystem",
"content": {"type": "read", "path": "/data/file.txt"}
},
"traceRoute": true
}'
```
**解决方案:**
1. 验证 PostgreSQL 是否正在运行
2. 检查 DATABASE_URL 格式
3. 确保安装了 pgvector 扩展
4. 手动测试连接:
```bash
psql $DATABASE_URL -c "SELECT 1;"
```
### 路由性能分析
</Accordion>
```bash
# 获取路由性能报告
curl -X GET http://localhost:3000/api/routing/performance \
-H "Authorization: Bearer $TOKEN" \
-G -d "timeRange=1h" -d "detailed=true"
```
<Accordion title="嵌入服务问题">
**症状:**
- 工具索引失败
- 查询处理错误
- API 速率限制错误
### 常见问题
**解决方案:**
1. 验证 API 密钥有效性
2. 检查网络连接
3. 监控速率限制
4. 测试嵌入服务:
```bash
curl -X POST https://api.openai.com/v1/embeddings \
-H "Authorization: Bearer $OPENAI_API_KEY" \
-H "Content-Type: application/json" \
-d '{"input": "test", "model": "text-embedding-3-small"}'
```
1. **不均匀的负载分布**
</Accordion>
- 检查服务器权重配置
- 验证健康检查设置
- 分析请求模式
<Accordion title="搜索结果不佳">
**症状:**
- 返回不相关的工具
- 相关性得分低
- 缺少预期的工具
2. **频繁的故障转移**
**解决方案:**
1. 调整相似性阈值
2. 使用更好的描述重新索引工具
3. 使用更具体的查询
4. 检查工具元数据质量
```bash
# 重新索引所有工具
curl -X POST http://localhost:3000/api/smart-routing/reindex \
-H "Authorization: Bearer YOUR_JWT_TOKEN"
```
- 调整健康检查阈值
- 检查网络连接稳定性
- 优化服务器资源
</Accordion>
3. **路由延迟过高**
- 简化路由规则
- 优化路由算法
- 使用缓存加速决策
<Accordion title="性能问题">
**症状:**
- 查询响应缓慢
- 数据库负载高
- 内存使用激增
有关更多信息,请参阅 [监控](/zh/features/monitoring) 和 [服务器管理](/zh/features/server-management) 文档。
**解决方案:**
1. 优化数据库配置
2. 增加缓存大小
3. 减少批处理大小
4. 监控系统资源
```bash
# 检查系统性能
curl http://localhost:3000/api/smart-routing/performance \
-H "Authorization: Bearer YOUR_JWT_TOKEN"
```
</Accordion>
</AccordionGroup>
## 最佳实践
### 查询编写
<Tip>
**要具体描述**:在查询中使用具体、描述性的语言以获得更好的工具匹配。
</Tip>
<Tip>
**包含上下文**:提供有关您的任务或领域的相关上下文以获得更准确的结果。
</Tip>
<Tip>**使用自然语言**:像向人类描述任务一样编写查询。</Tip>
### 工具描述
<Warning>
**质量元数据**:确保 MCP 服务器提供高质量的工具描述和元数据。
</Warning>
<Warning>**定期更新**:随着功能的发展保持工具描述的最新状态。</Warning>
<Warning>
**一致的命名**:在工具和服务器中使用一致的命名约定。
</Warning>
### 系统维护
<Info>**定期重新索引**:定期重新索引工具以确保嵌入质量。</Info>
<Info>**监控性能**:跟踪查询模式并根据使用情况进行优化。</Info>
<Info>
**更新模型**:随着新嵌入模型的出现,考虑更新到更新的模型。
</Info>
## 下一步
<CardGroup cols={2}>
<Card title="身份验证" icon="shield" href="/zh/features/authentication">
用户管理和访问控制
</Card>
<Card title="监控" icon="chart-line" href="/zh/features/monitoring">
系统监控和分析
</Card>
<Card title="API 参考" icon="code" href="/zh/api-reference/smart-routing">
完整的智能路由 API 文档
</Card>
<Card title="配置" icon="cog" href="/zh/configuration/environment-variables">
高级配置选项
</Card>
</CardGroup>

View File

@@ -1,23 +1,21 @@
---
title: '欢迎使用 MCPHub'
description: 'MCPHub 是一个强大的 Model Context Protocol (MCP) 服务器管理平台,提供智能路由、负载均衡和实时监控功能'
title: '欢迎使用'
description: 'MCPHub 是一个强大的 Model Context Protocol (MCP) 服务器管理平台,提供分组管理、智能路由和实时监控功能'
---
<img className="block dark:hidden" src="/images/hero-light.png" alt="MCPHub Hero Light" />
<img className="hidden dark:block" src="/images/hero-dark.png" alt="MCPHub Hero Dark" />
{/* <img className="block dark:hidden" src="/images/hero-light.png" alt="MCPHub Hero Light" />
<img className="hidden dark:block" src="/images/hero-dark.png" alt="MCPHub Hero Dark" /> */}
## 什么是 MCPHub
MCPHub 是一个现代化的 Model Context Protocol (MCP) 服务器管理平台,旨在简化 AI 模型服务的部署、管理和监控。通过智能路由和负载均衡技术MCPHub 帮助您构建高可用、可扩展的 AI 服务架构。
MCPHub 是一个现代化的 Model Context Protocol (MCP) 服务器管理平台,旨在简化 AI 模型服务的部署、管理和监控。通过分组管理和智能路由技术MCPHub 帮助您构建高可用、可扩展的 AI 服务架构。
### 核心功能
- **🚀 智能路由** - 基于负载、延迟和健康状态的智能请求分发
- **⚖️ 负载均衡** - 多种负载均衡策略,确保最优性能
- **🏗️ 分组管理** - 灵活的服务器分组和配置管理
- **🚀 智能路由** - 基于语义检索的智能路由分发
- **📊 实时监控** - 全面的性能指标和健康检查
- **🔐 安全认证** - 企业级身份认证和访问控制
- **🏗️ 服务器组管理** - 灵活的服务器分组和配置管理
- **🔄 故障转移** - 自动故障检测和流量切换
- **🔐 安全认证** - 身份认证和访问控制
## 快速开始

570
docs/zh/installation.mdx Normal file
View File

@@ -0,0 +1,570 @@
---
title: '安装指南'
description: '各种平台的详细安装说明'
---
## 先决条件
在安装 MCPHub 之前,请确保您具备以下先决条件:
- **Node.js** 18+ (用于本地开发)
- **Docker** (推荐用于生产环境)
- **pnpm** (用于本地开发)
智能路由的可选要求:
- **PostgreSQL** 带 pgvector 扩展
- **OpenAI API Key** 或兼容的嵌入服务
## 安装方法
<Tabs>
<Tab title="Docker (推荐)">
### Docker 安装
Docker 是在生产环境中部署 MCPHub 的推荐方式。
#### 1. 基础安装
```bash
# 拉取最新镜像
docker pull samanhappy/mcphub:latest
# 使用默认设置运行
docker run -d \
--name mcphub \
-p 3000:3000 \
samanhappy/mcphub:latest
```
#### 2. 使用自定义配置
```bash
# 创建您的配置文件
cat > mcp_settings.json << 'EOF'
{
"mcpServers": {
"fetch": {
"command": "uvx",
"args": ["mcp-server-fetch"]
},
"playwright": {
"command": "npx",
"args": ["@playwright/mcp@latest", "--headless"]
}
}
}
EOF
# 使用挂载的配置运行
docker run -d \
--name mcphub \
-p 3000:3000 \
-v $(pwd)/mcp_settings.json:/app/mcp_settings.json \
samanhappy/mcphub:latest
```
#### 3. 使用环境变量
```bash
docker run -d \
--name mcphub \
-p 3000:3000 \
-e PORT=3000 \
-e BASE_PATH="" \
samanhappy/mcphub:latest
```
#### 4. Docker Compose
创建 `docker-compose.yml` 文件:
```yaml
version: '3.8'
services:
mcphub:
image: samanhappy/mcphub:latest
ports:
- "3000:3000"
volumes:
- ./mcp_settings.json:/app/mcp_settings.json
environment:
- PORT=3000
- BASE_PATH=""
- REQUEST_TIMEOUT=60000
restart: unless-stopped
# 可选:用于智能路由的 PostgreSQL
postgres:
image: pgvector/pgvector:pg16
environment:
POSTGRES_DB: mcphub
POSTGRES_USER: mcphub
POSTGRES_PASSWORD: mcphub_password
volumes:
- postgres_data:/var/lib/postgresql/data
ports:
- "5432:5432"
volumes:
postgres_data:
```
运行命令:
```bash
docker-compose up -d
```
</Tab>
<Tab title="npm 包">
### npm 包安装
将 MCPHub 安装为全局 npm 包:
#### 1. 全局安装
```bash
# 全局安装
npm install -g @samanhappy/mcphub
# 或使用 yarn
yarn global add @samanhappy/mcphub
# 或使用 pnpm
pnpm add -g @samanhappy/mcphub
```
#### 2. 运行 MCPHub
```bash
# 使用默认设置运行
mcphub
# 使用自定义端口运行
PORT=8080 mcphub
```
{/* #### 3. 本地安装
您也可以在项目中本地安装 MCPHub
```bash
# 创建新目录
mkdir my-mcphub
cd my-mcphub
# 初始化 package.json
npm init -y
# 本地安装 MCPHub
npm install @samanhappy/mcphub
# 创建启动脚本
echo '#!/bin/bash\nnpx mcphub' > start.sh
chmod +x start.sh
# 运行 MCPHub
./start.sh
``` */}
</Tab>
<Tab title="本地开发">
### 本地开发环境设置
用于开发、自定义或贡献:
#### 1. 克隆仓库
```bash
# 克隆仓库
git clone https://github.com/samanhappy/mcphub.git
cd mcphub
```
#### 2. 安装依赖
```bash
# 使用 pnpm 安装依赖(推荐)
pnpm install
# 或使用 npm
npm install
# 或使用 yarn
yarn install
```
#### 3. 开发模式
```bash
# 在开发模式下同时启动后端和前端
pnpm dev
# 这将启动:
# - 后端在 http://localhost:3001
# - 前端在 http://localhost:5173
# - 前端代理 API 调用到后端
```
#### 4. 生产构建
```bash
# 构建后端和前端
pnpm build
# 启动生产服务器
pnpm start
```
#### 5. 开发脚本
```bash
# 仅后端(用于 API 开发)
pnpm backend:dev
# 仅前端(当后端单独运行时)
pnpm frontend:dev
# 运行测试
pnpm test
# 代码检查
pnpm lint
# 代码格式化
pnpm format
```
<Note>
在 Windows 上,您可能需要分别运行后端和前端:
```bash
# 终端 1后端
pnpm backend:dev
# 终端 2前端
pnpm frontend:dev
```
</Note>
</Tab>
<Tab title="Kubernetes">
### Kubernetes 部署
使用这些清单在 Kubernetes 上部署 MCPHub
#### 1. 设置的 ConfigMap
```yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: mcphub-config
data:
mcp_settings.json: |
{
"mcpServers": {
"fetch": {
"command": "uvx",
"args": ["mcp-server-fetch"]
},
"playwright": {
"command": "npx",
"args": ["@playwright/mcp@latest", "--headless"]
}
}
}
```
#### 2. 部署
```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: mcphub
spec:
replicas: 1
selector:
matchLabels:
app: mcphub
template:
metadata:
labels:
app: mcphub
spec:
containers:
- name: mcphub
image: samanhappy/mcphub:latest
ports:
- containerPort: 3000
env:
- name: PORT
value: "3000"
volumeMounts:
- name: config
mountPath: /app/mcp_settings.json
subPath: mcp_settings.json
volumes:
- name: config
configMap:
name: mcphub-config
```
#### 3. 服务
```yaml
apiVersion: v1
kind: Service
metadata:
name: mcphub-service
spec:
selector:
app: mcphub
ports:
- port: 80
targetPort: 3000
type: ClusterIP
```
#### 4. Ingress (可选)
```yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: mcphub-ingress
annotations:
nginx.ingress.kubernetes.io/proxy-buffering: "off"
spec:
rules:
- host: mcphub.yourdomain.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: mcphub-service
port:
number: 80
```
部署命令:
```bash
kubectl apply -f mcphub-configmap.yaml
kubectl apply -f mcphub-deployment.yaml
kubectl apply -f mcphub-service.yaml
kubectl apply -f mcphub-ingress.yaml
```
</Tab>
</Tabs>
## 智能路由设置 (可选)
智能路由使用向量语义搜索提供 AI 驱动的工具发现。
### 先决条件
1. **PostgreSQL 带 pgvector 扩展**
2. **OpenAI API Key** (或兼容的嵌入服务)
### 数据库设置
<Tabs>
<Tab title="Docker PostgreSQL">
```bash
# 运行带 pgvector 的 PostgreSQL
docker run -d \
--name mcphub-postgres \
-e POSTGRES_DB=mcphub \
-e POSTGRES_USER=mcphub \
-e POSTGRES_PASSWORD=your_password \
-p 5432:5432 \
pgvector/pgvector:pg16
```
</Tab>
<Tab title="现有 PostgreSQL">
如果您有现有的 PostgreSQL 实例:
```sql
-- 连接到您的 PostgreSQL 实例
-- 创建数据库
CREATE DATABASE mcphub;
-- 连接到 mcphub 数据库
\c mcphub;
-- 启用 pgvector 扩展
CREATE EXTENSION IF NOT EXISTS vector;
```
</Tab>
<Tab title="云 PostgreSQL">
对于云提供商AWS RDS、Google Cloud SQL 等):
1. 在您的云提供商控制台中启用 pgvector 扩展
2. 创建名为 `mcphub` 的数据库
3. 记下连接详细信息
</Tab>
</Tabs>
{/* ### 环境配置
设置以下环境变量:
```bash
# 数据库连接
DATABASE_URL=postgresql://mcphub:your_password@localhost:5432/mcphub
# 用于嵌入的 OpenAI API
OPENAI_API_KEY=your_openai_api_key
# 可选:自定义嵌入模型
EMBEDDING_MODEL=text-embedding-3-small
# 可选:启用智能路由
ENABLE_SMART_ROUTING=true
``` */}
## 验证
安装后,验证 MCPHub 是否正常工作:
{/* ### 1. 健康检查
```bash
curl http://localhost:3000/api/health
```
预期响应:
```json
{
"status": "ok",
"version": "x.x.x",
"uptime": 123
}
``` */}
### 控制台访问
打开浏览器并导航到:
```
http://localhost:3000
```
{/* ### 3. API 测试
```bash
curl -X POST http://localhost:3000/mcp \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/list",
"params": {}
}'
``` */}
## 故障排除
<AccordionGroup>
<Accordion title="Docker 问题">
**端口已被使用:**
```bash
# 检查是什么在使用端口 3000
lsof -i :3000
# 使用不同的端口
docker run -p 8080:3000 samanhappy/mcphub
```
**容器无法启动:**
```bash
# 检查容器日志
docker logs mcphub
# 交互式运行以进行调试
docker run -it --rm samanhappy/mcphub /bin/bash
```
</Accordion>
<Accordion title="npm 安装问题">
**权限错误:**
```bash
# 使用 npx 而不是全局安装
npx @samanhappy/mcphub
# 或修复 npm 权限
npm config set prefix ~/.npm-global
export PATH=~/.npm-global/bin:$PATH
```
**Node 版本问题:**
```bash
# 检查 Node 版本
node --version
# 使用 nvm 安装 Node 18+
nvm install 18
nvm use 18
```
</Accordion>
<Accordion title="网络问题">
**无法访问控制台:**
- 检查 MCPHub 是否在运行:`ps aux | grep mcphub`
- 验证端口绑定:`netstat -tlnp | grep 3000`
- 检查防火墙设置
- 尝试通过 `127.0.0.1:3000` 而不是 `localhost:3000` 访问
**AI 客户端无法连接:**
- 确保端点 URL 正确
- 检查 MCPHub 是否在代理后面
- 验证 Kubernetes/Docker 环境中的网络策略
</Accordion>
<Accordion title="智能路由问题">
**数据库连接失败:**
```bash
# 测试数据库连接
psql $DATABASE_URL -c "SELECT 1;"
# 检查是否安装了 pgvector
psql $DATABASE_URL -c "CREATE EXTENSION IF NOT EXISTS vector;"
```
**嵌入服务错误:**
- 验证 OpenAI API 密钥是否有效
- 检查互联网连接
- 监控速率限制
</Accordion>
</AccordionGroup>
## 下一步
<CardGroup cols={2}>
<Card title="配置" icon="cog" href="/zh/configuration/mcp-settings">
配置您的 MCP 服务器和设置
</Card>
<Card title="快速开始" icon="rocket" href="/zh/quickstart">
5分钟内启动并运行
</Card>
<Card title="服务器管理" icon="server" href="/zh/features/server-management">
了解如何管理您的 MCP 服务器
</Card>
<Card title="API 参考" icon="code" href="/zh/api-reference/introduction">
探索完整的 API 文档
</Card>
</CardGroup>

View File

@@ -1,304 +1,212 @@
---
title: '快速开始'
description: '5 分钟内部署 MCPHub 并连接您的第一个 MCP 服务器'
title: '快速开始指南'
description: '5 分钟内运行 MCPHub'
---
## 欢迎使用 MCPHub
## 安装
本指南将帮助您在 5 分钟内完成 MCPHub 的部署和配置,并连接您的第一个 MCP 服务器。
## 前提条件
在开始之前,请确保您的系统满足以下要求:
<AccordionGroup>
<Accordion icon="desktop" title="系统要求">
- **操作系统**: Linux、macOS 或 Windows
- **内存**: 最少 2GB RAM推荐 4GB+
- **存储**: 至少 1GB 可用空间
- **网络**: 稳定的互联网连接
</Accordion>
<Accordion icon="code" title="软件依赖">
- **Node.js**: 18.0+ 版本
- **Docker**: 最新版本(可选,用于容器化部署)
- **Git**: 用于代码管理
检查版本:
```bash
node --version # 应该 >= 18.0.0
npm --version # 应该 >= 8.0.0
docker --version # 可选
```
</Accordion>
</AccordionGroup>
## 安装 MCPHub
### 方式一:使用 npm推荐
<AccordionGroup>
<Accordion icon="download" title="安装 MCPHub CLI">
首先安装 MCPHub 命令行工具:
<Tabs>
<Tab title="Docker推荐">
使用 Docker 是最快的开始方式:
```bash
npm install -g @mcphub/cli
# 使用默认配置运行
docker run -p 3000:3000 samanhappy/mcphub
```
验证安装
或者挂载自定义配置
```bash
mcphub --version
# 使用自定义 MCP 设置运行
docker run -p 3000:3000 \
-v $(pwd)/mcp_settings.json:/app/mcp_settings.json \
samanhappy/mcphub
```
</Accordion>
<Accordion icon="folder-plus" title="创建新项目">
创建一个新的 MCPHub 项目:
</Tab>
<Tab title="本地开发">
用于开发或自定义:
```bash
# 创建项目
mcphub init my-mcphub-project
cd my-mcphub-project
# 克隆仓库
git clone https://github.com/samanhappy/mcphub.git
cd mcphub
# 安装依赖
npm install
pnpm install
# 启动开发服务器
pnpm dev
```
</Accordion>
这会同时启动后端(端口 3001和前端端口 5173的开发模式。
<Accordion icon="gear" title="配置环境">
复制并编辑环境变量文件:
</Tab>
<Tab title="npm 包">
将 MCPHub 安装为全局包:
```bash
cp .env.example .env
# 全局安装
npm install -g @samanhappy/mcphub
# 运行 MCPHub
mcphub
```
编辑 `.env` 文件,设置基本配置:
```bash
# 服务器配置
PORT=3000
NODE_ENV=development
</Tab>
</Tabs>
# 数据库配置(使用内置 SQLite
DATABASE_URL=sqlite:./data/mcphub.db
## 初始设置
# JWT 密钥(请更改为安全的随机字符串)
JWT_SECRET=your-super-secret-jwt-key-change-me
### 1. 访问控制面板
# 管理员账户
ADMIN_EMAIL=admin@example.com
ADMIN_PASSWORD=admin123
```
打开浏览器并导航到:
</Accordion>
</AccordionGroup>
### 方式二:使用 Docker
<AccordionGroup>
<Accordion icon="docker" title="Docker 快速部署">
使用 Docker Compose 一键部署:
```bash
# 下载配置文件
curl -O https://raw.githubusercontent.com/mcphub/mcphub/main/docker-compose.yml
# 启动服务
docker-compose up -d
```
或者直接运行 Docker 容器:
```bash
docker run -d \
--name mcphub \
-p 3000:3000 \
-e NODE_ENV=production \
-e JWT_SECRET=your-secret-key \
mcphub/server:latest
```
</Accordion>
</AccordionGroup>
## 启动 MCPHub
### 开发模式启动
```bash
# 初始化数据库
npm run db:setup
# 启动开发服务器
npm run dev
```
http://localhost:3000
```
### 生产模式启动
### 2. 登录
```bash
# 构建应用
npm run build
使用默认凭据:
# 启动生产服务器
npm start
```
<Note>开发模式下MCPHub 会在 `http://localhost:3000` 启动,并具有热重载功能。</Note>
## 首次访问和配置
### 1. 访问管理界面
打开浏览器,访问 `http://localhost:3000`,您将看到 MCPHub 的欢迎页面。
### 2. 登录管理员账户
使用您在 `.env` 文件中设置的管理员凭据登录:
- **邮箱**: `admin@example.com`
- **用户名**: `admin`
- **密码**: `admin123`
<Warning>首次登录后,请立即更改默认密码以确保安全!</Warning>
<Warning>为了安全起见,请在首次登录后立即更改这些默认凭据。</Warning>
### 3. 完成初始配置
### 3. 配置您的第一个 MCP 服务器
登录后,系统会引导您完成初始配置:
1. 在控制面板中点击 **"添加服务器"**
2. 输入服务器详细信息:
- **名称**: 唯一标识符(例如 `fetch`
- **命令**: 可执行命令(`uvx`
- **参数**: 命令参数(`["mcp-server-fetch"]`
- **环境**: 任何所需的环境变量
1. **更改管理员密码**
2. **设置组织信息**
3. **配置基本设置**
fetch 服务器的示例配置:
## 添加您的第一个 MCP 服务器
### 1. 准备 MCP 服务器
如果您还没有 MCP 服务器,可以使用我们的示例服务器进行测试:
```bash
# 克隆示例服务器
git clone https://github.com/mcphub/example-mcp-server.git
cd example-mcp-server
# 安装依赖并启动
npm install
npm start
```json
{
"name": "fetch",
"command": "uvx",
"args": ["mcp-server-fetch"],
"env": {}
}
```
示例服务器将在 `http://localhost:3001` 启动。
## 基本使用
### 2. 在 MCPHub 中添加服务器
### 连接 AI 客户端
在 MCPHub 管理界面中
一旦配置了服务器,使用 MCPHub 端点连接您的 AI 客户端
1. 点击 **"添加服务器"** 按钮
2. 填写服务器信息:
```
名称: Example MCP Server
端点: http://localhost:3001
描述: 示例 MCP 服务器用于测试
```
3. 选择功能类型chat、completion、analysis
4. 点击 **"测试连接"** 验证服务器可达性
5. 点击 **"保存"** 完成添加
<Tabs>
<Tab title="所有服务器">
访问所有已配置的 MCP 服务器:``` http://localhost:3000/mcp ```
</Tab>
<Tab title="特定组">
访问特定组中的服务器:``` http://localhost:3000/mcp/{groupName} ```
</Tab>
<Tab title="单个服务器">
访问单个服务器:``` http://localhost:3000/mcp/{serverName} ```
</Tab>
<Tab title="智能路由">
使用 AI 驱动的工具发现:``` http://localhost:3000/mcp/$smart ```
<Info>智能路由需要使用 pgvector 的 PostgreSQL 和 OpenAI API 密钥。</Info>
</Tab>
</Tabs>
### 3. 验证服务器状态
### 示例:添加热门 MCP 服务器
添加成功后,您应该能在服务器列表中看到新添加的服务器,状态显示为 **"活跃"**(绿色)。
以下是一些您可以添加的热门 MCP 服务器:
## 测试路由功能
### 发送测试请求
使用 cURL 或其他 HTTP 客户端测试路由功能:
```bash
# 发送聊天请求
curl -X POST http://localhost:3000/api/chat \
-H "Content-Type: application/json" \
-d '{
"messages": [
{
"role": "user",
"content": "Hello, this is a test message!"
<AccordionGroup>
<Accordion title="Web Fetch 服务器">
```json
{
"name": "fetch",
"command": "uvx",
"args": ["mcp-server-fetch"]
}
```
</Accordion>
<Accordion title="Playwright 浏览器自动化">
```json
{
"name": "playwright",
"command": "npx",
"args": ["@playwright/mcp@latest", "--headless"]
}
```
</Accordion>
<Accordion title="高德地图(需要 API 密钥)">
```json
{
"name": "amap",
"command": "npx",
"args": ["-y", "@amap/amap-maps-mcp-server"],
"env": {
"AMAP_MAPS_API_KEY": "your-api-key-here"
}
]
}'
```
### 查看请求日志
在 MCPHub 管理界面的 **"监控"** 页面中,您可以实时查看:
- 请求数量和响应时间
- 服务器健康状态
- 错误日志和统计
}
```
</Accordion>
<Accordion title="Slack 集成">
```json
{
"name": "slack",
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-slack"],
"env": {
"SLACK_BOT_TOKEN": "your-bot-token",
"SLACK_TEAM_ID": "your-team-id"
}
}
```
</Accordion>
</AccordionGroup>
## 后续步骤
恭喜!您已经成功部署了 MCPHub 并添加了第一个 MCP 服务器。接下来您可以:
<CardGroup cols={2}>
<Card title="配置负载均衡" icon="balance-scale" href="/zh/features/smart-routing">
学习如何配置智能路由和负载均衡策略
<Card title="服务器管理" icon="server" href="/zh/features/server-management">
学习高级服务器配置和管理
</Card>
<Card title="添加更多服务器" icon="plus" href="/zh/features/server-management">
了解服务器管理的高级功能
<Card title="组管理" icon="users" href="/zh/features/group-management">
服务器组织成逻辑组
</Card>
<Card title="设置监控告警" icon="bell" href="/zh/features/monitoring">
配置性能监控和告警通知
<Card title="智能路由" icon="route" href="/zh/features/smart-routing">
设置 AI 驱动的工具发现
</Card>
<Card title="API 集成" icon="code" href="/zh/api-reference/introduction">
将 MCPHub 集成到您的应用程序中
<Card title="API 参考" icon="code" href="/zh/api-reference/introduction">
探索完整的 API 文档
</Card>
</CardGroup>
## 常见问题
## 故障排除
<AccordionGroup>
<Accordion icon="question" title="无法连接到 MCP 服务器">
**可能原因**
- 服务器地址错误或服务器未启动
- 防火墙阻止连接
- 网络配置问题
**解决方案**
1. 验证服务器是否正在运行:`curl http://localhost:3001/health`
2. 检查防火墙设置
3. 确认网络连接正常
<Accordion title="服务器无法启动">
- 检查 MCP 服务器命令是否在您的 PATH 中可访问
- 验证环境变量是否正确设置
- 检查 MCPHub 日志以获取详细错误信息
</Accordion>
<Accordion icon="question" title="服务器状态显示为离线">
**可能原因**
- 健康检查失败
- 服务器响应超时
- 服务器崩溃或重启
**解决方案**
1. 检查服务器日志
2. 调整健康检查间隔
3. 重启服务器进程
<Accordion title="无法从 AI 客户端连接">
- 确保 MCPHub 在正确的端口上运行
- 检查防火墙设置
- 验证端点 URL 格式
</Accordion>
<Accordion icon="question" title="忘记管理员密码">
**解决方案**
```bash
# 重置管理员密码
npm run reset-admin-password
```
或者删除数据库文件重新初始化:
```bash
rm data/mcphub.db
npm run db:setup
```
<Accordion title="身份验证问题">
- 验证凭据是否正确
- 检查 JWT 令牌是否有效
- 尝试清除浏览器缓存和 cookie
</Accordion>
</AccordionGroup>
## 获取帮助
如果您在设置过程中遇到问题:
- 📖 查看 [完整文档](/zh/development/getting-started)
- 🐛 在 [GitHub](https://github.com/mcphub/mcphub/issues) 上报告问题
- 💬 加入 [Discord 社区](https://discord.gg/mcphub) 获取实时帮助
- 📧 发送邮件至 support@mcphub.io
需要更多帮助?加入我们的 [Discord 社区](https://discord.gg/qMKNsn5Q) 获取支持!

View File

@@ -4,7 +4,7 @@ NPM_REGISTRY=${NPM_REGISTRY:-https://registry.npmjs.org/}
echo "Setting npm registry to ${NPM_REGISTRY}"
npm config set registry "$NPM_REGISTRY"
# 处理 HTTP_PROXY HTTPS_PROXY 环境变量
# Handle HTTP_PROXY and HTTPS_PROXY environment variables
if [ -n "$HTTP_PROXY" ]; then
echo "Setting HTTP proxy to ${HTTP_PROXY}"
npm config set proxy "$HTTP_PROXY"
@@ -19,4 +19,33 @@ fi
echo "Using REQUEST_TIMEOUT: $REQUEST_TIMEOUT"
# Auto-start Docker daemon if Docker is installed
if command -v dockerd >/dev/null 2>&1; then
echo "Docker daemon detected, starting dockerd..."
# Create docker directory if it doesn't exist
mkdir -p /var/lib/docker
# Start dockerd in the background
dockerd --host=unix:///var/run/docker.sock --storage-driver=vfs > /var/log/dockerd.log 2>&1 &
# Wait for Docker daemon to be ready
echo "Waiting for Docker daemon to be ready..."
TIMEOUT=15
ELAPSED=0
while ! docker info >/dev/null 2>&1; do
if [ $ELAPSED -ge $TIMEOUT ]; then
echo "WARNING: Docker daemon failed to start within ${TIMEOUT} seconds"
echo "Check /var/log/dockerd.log for details"
break
fi
sleep 1
ELAPSED=$((ELAPSED + 1))
done
if docker info >/dev/null 2>&1; then
echo "Docker daemon started successfully"
fi
fi
exec "$@"

View File

@@ -0,0 +1,80 @@
{
"mcpServers": {
"example-sse-server": {
"type": "sse",
"url": "${MCP_SERVER_URL}",
"headers": {
"Authorization": "Bearer ${API_TOKEN}",
"X-Custom-Header": "${CUSTOM_HEADER_VALUE}"
},
"enabled": true
},
"example-streamable-http": {
"type": "streamable-http",
"url": "https://${SERVER_HOST}/mcp",
"headers": {
"API-Key": "${API_KEY}"
}
},
"example-stdio-server": {
"type": "stdio",
"command": "${PYTHON_PATH}",
"args": [
"-m",
"${MODULE_NAME}",
"--config",
"${CONFIG_PATH}"
],
"env": {
"API_KEY": "${MY_API_KEY}",
"DEBUG": "${DEBUG_MODE}",
"DATABASE_URL": "${DATABASE_URL}"
}
},
"example-openapi-server": {
"type": "openapi",
"openapi": {
"url": "${OPENAPI_SPEC_URL}",
"security": {
"type": "apiKey",
"apiKey": {
"name": "X-API-Key",
"in": "header",
"value": "${OPENAPI_API_KEY}"
}
}
},
"headers": {
"User-Agent": "MCPHub/${VERSION}"
}
},
"example-oauth-server": {
"type": "sse",
"url": "${OAUTH_SERVER_URL}",
"oauth": {
"clientId": "${OAUTH_CLIENT_ID}",
"clientSecret": "${OAUTH_CLIENT_SECRET}",
"accessToken": "${OAUTH_ACCESS_TOKEN}",
"scopes": ["read", "write"]
}
}
},
"users": [
{
"username": "admin",
"password": "${ADMIN_PASSWORD_HASH}",
"isAdmin": true
}
],
"systemConfig": {
"install": {
"pythonIndexUrl": "${PYTHON_INDEX_URL}",
"npmRegistry": "${NPM_REGISTRY}"
},
"mcpRouter": {
"apiKey": "${MCPROUTER_API_KEY}",
"referer": "${MCPROUTER_REFERER}",
"baseUrl": "${MCPROUTER_BASE_URL}"
}
}
}

View File

@@ -0,0 +1,256 @@
{
"mcpServers": {
"example-api-url": {
"type": "openapi",
"openapi": {
"url": "https://api.example.com/openapi.json",
"version": "3.1.0",
"security": {
"type": "apiKey",
"apiKey": {
"name": "X-API-Key",
"in": "header",
"value": "your-api-key-here"
}
}
},
"headers": {
"User-Agent": "MCPHub/1.0"
},
"enabled": true
},
"example-api-schema": {
"type": "openapi",
"openapi": {
"schema": {
"openapi": "3.1.0",
"info": {
"title": "Example API",
"version": "1.0.0",
"description": "A sample API for demonstration"
},
"servers": [
{
"url": "https://api.example.com",
"description": "Production server"
}
],
"paths": {
"/users": {
"get": {
"operationId": "listUsers",
"summary": "List all users",
"description": "Retrieve a list of all users in the system",
"parameters": [
{
"name": "limit",
"in": "query",
"description": "Maximum number of users to return",
"required": false,
"schema": {
"type": "integer",
"minimum": 1,
"maximum": 100,
"default": 10
}
},
{
"name": "offset",
"in": "query",
"description": "Number of users to skip",
"required": false,
"schema": {
"type": "integer",
"minimum": 0,
"default": 0
}
}
],
"responses": {
"200": {
"description": "List of users",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"users": {
"type": "array",
"items": {
"$ref": "#/components/schemas/User"
}
},
"total": {
"type": "integer",
"description": "Total number of users"
}
}
}
}
}
}
}
},
"post": {
"operationId": "createUser",
"summary": "Create a new user",
"description": "Create a new user in the system",
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/CreateUserRequest"
}
}
}
},
"responses": {
"201": {
"description": "User created successfully",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/User"
}
}
}
}
}
}
},
"/users/{userId}": {
"get": {
"operationId": "getUserById",
"summary": "Get user by ID",
"description": "Retrieve a specific user by their ID",
"parameters": [
{
"name": "userId",
"in": "path",
"required": true,
"description": "ID of the user to retrieve",
"schema": {
"type": "integer",
"minimum": 1
}
}
],
"responses": {
"200": {
"description": "User details",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/User"
}
}
}
},
"404": {
"description": "User not found"
}
}
}
}
},
"components": {
"schemas": {
"User": {
"type": "object",
"properties": {
"id": {
"type": "integer",
"description": "Unique identifier for the user"
},
"name": {
"type": "string",
"description": "Full name of the user"
},
"email": {
"type": "string",
"format": "email",
"description": "Email address of the user"
},
"createdAt": {
"type": "string",
"format": "date-time",
"description": "Timestamp when the user was created"
},
"status": {
"type": "string",
"enum": [
"active",
"inactive",
"suspended"
],
"description": "Current status of the user"
}
},
"required": [
"id",
"name",
"email"
]
},
"CreateUserRequest": {
"type": "object",
"properties": {
"name": {
"type": "string",
"minLength": 1,
"maxLength": 100,
"description": "Full name of the user"
},
"email": {
"type": "string",
"format": "email",
"description": "Email address of the user"
},
"status": {
"type": "string",
"enum": [
"active",
"inactive"
],
"default": "active",
"description": "Initial status of the user"
}
},
"required": [
"name",
"email"
]
}
},
"securitySchemes": {
"ApiKeyAuth": {
"type": "apiKey",
"in": "header",
"name": "X-API-Key"
}
}
},
"security": [
{
"ApiKeyAuth": []
}
]
},
"version": "3.1.0",
"security": {
"type": "apiKey",
"apiKey": {
"name": "X-API-Key",
"in": "header",
"value": "your-api-key-here"
}
}
},
"headers": {
"User-Agent": "MCPHub/1.0"
},
"enabled": true
}
}
}

View File

@@ -1,13 +1,19 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>MCP Hub Dashboard</title>
<title>MCPHub Dashboard</title>
<link rel="icon" type="image/x-icon" href="/favicon.ico">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
</head>
<body class="bg-gray-100">
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

View File

@@ -1,24 +1,33 @@
import React from 'react';
import { BrowserRouter as Router, Route, Routes, Navigate } from 'react-router-dom';
import { BrowserRouter as Router, Route, Routes, Navigate, useParams } from 'react-router-dom';
import { AuthProvider } from './contexts/AuthContext';
import { ToastProvider } from './contexts/ToastContext';
import { ThemeProvider } from './contexts/ThemeContext';
import { ServerProvider } from './contexts/ServerContext';
import MainLayout from './layouts/MainLayout';
import ProtectedRoute from './components/ProtectedRoute';
import LoginPage from './pages/LoginPage';
import DashboardPage from './pages/Dashboard';
import ServersPage from './pages/ServersPage';
import GroupsPage from './pages/GroupsPage';
import UsersPage from './pages/UsersPage';
import SettingsPage from './pages/SettingsPage';
import MarketPage from './pages/MarketPage';
import LogsPage from './pages/LogsPage';
import { getBasePath } from './utils/runtime';
// Helper component to redirect cloud server routes to market
const CloudRedirect: React.FC = () => {
const { serverName } = useParams<{ serverName: string }>();
return <Navigate to={`/market/${serverName}?tab=cloud`} replace />;
};
function App() {
const basename = getBasePath();
return (
<ThemeProvider>
<AuthProvider>
<ServerProvider>
<ToastProvider>
<Router basename={basename}>
<Routes>
@@ -31,8 +40,15 @@ function App() {
<Route path="/" element={<DashboardPage />} />
<Route path="/servers" element={<ServersPage />} />
<Route path="/groups" element={<GroupsPage />} />
<Route path="/users" element={<UsersPage />} />
<Route path="/market" element={<MarketPage />} />
<Route path="/market/:serverName" element={<MarketPage />} />
{/* Legacy cloud routes redirect to market with cloud tab */}
<Route path="/cloud" element={<Navigate to="/market?tab=cloud" replace />} />
<Route
path="/cloud/:serverName"
element={<CloudRedirect />}
/>
<Route path="/logs" element={<LogsPage />} />
<Route path="/settings" element={<SettingsPage />} />
</Route>
@@ -43,6 +59,7 @@ function App() {
</Routes>
</Router>
</ToastProvider>
</ServerProvider>
</AuthProvider>
</ThemeProvider>
);

View File

@@ -2,8 +2,8 @@ import { useState, useEffect } from 'react'
import { useTranslation } from 'react-i18next'
import { useGroupData } from '@/hooks/useGroupData'
import { useServerData } from '@/hooks/useServerData'
import { GroupFormData, Server } from '@/types'
import { ToggleGroup } from './ui/ToggleGroup'
import { GroupFormData, Server, IGroupServerConfig } from '@/types'
import { ServerToolConfig } from './ServerToolConfig'
interface AddGroupFormProps {
onAdd: () => void
@@ -21,7 +21,7 @@ const AddGroupForm = ({ onAdd, onCancel }: AddGroupFormProps) => {
const [formData, setFormData] = useState<GroupFormData>({
name: '',
description: '',
servers: []
servers: [] as IGroupServerConfig[]
})
useEffect(() => {
@@ -50,9 +50,8 @@ const AddGroupForm = ({ onAdd, onCancel }: AddGroupFormProps) => {
}
const result = await createGroup(formData.name, formData.description, formData.servers)
if (!result) {
setError(t('groups.createError'))
if (!result || !result.success) {
setError(result?.message || t('groups.createError'))
setIsSubmitting(false)
return
}
@@ -66,64 +65,68 @@ const AddGroupForm = ({ onAdd, onCancel }: AddGroupFormProps) => {
return (
<div className="fixed inset-0 bg-black/50 z-50 flex items-center justify-center p-4">
<div className="bg-white rounded-lg shadow-lg max-w-md w-full">
<div className="p-6">
<div className="bg-white rounded-lg shadow-lg max-w-2xl w-full max-h-[90vh] flex flex-col">
<div className="p-6 flex-shrink-0">
<h2 className="text-xl font-semibold text-gray-800 mb-4">{t('groups.addNew')}</h2>
{error && (
<div className="mb-4 p-3 bg-red-100 text-red-700 rounded">
<div className="mb-4 p-3 bg-red-100 text-red-700 rounded-md border border-gray-200">
{error}
</div>
)}
<form onSubmit={handleSubmit}>
<div className="mb-4">
<label className="block text-gray-700 text-sm font-bold mb-2" htmlFor="name">
{t('groups.name')} *
</label>
<input
type="text"
id="name"
name="name"
value={formData.name}
onChange={handleChange}
className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
placeholder={t('groups.namePlaceholder')}
required
/>
</div>
<ToggleGroup
className="mb-6"
label={t('groups.servers')}
noOptionsText={t('groups.noServerOptions')}
values={formData.servers}
options={availableServers.map(server => ({
value: server.name,
label: server.name
}))}
onChange={(servers) => setFormData(prev => ({ ...prev, servers }))}
/>
<div className="flex justify-end space-x-3">
<button
type="button"
onClick={onCancel}
className="px-4 py-2 text-gray-600 hover:text-gray-800"
disabled={isSubmitting}
>
{t('common.cancel')}
</button>
<button
type="submit"
className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 disabled:opacity-50"
disabled={isSubmitting}
>
{isSubmitting ? t('common.submitting') : t('common.create')}
</button>
</div>
</form>
</div>
<form onSubmit={handleSubmit} className="flex flex-col flex-1 min-h-0">
<div className="flex-1 overflow-y-auto px-6">
<div className="space-y-4">
<div>
<label className="block text-gray-700 text-sm font-bold mb-2" htmlFor="name">
{t('groups.name')} *
</label>
<input
type="text"
id="name"
name="name"
value={formData.name}
onChange={handleChange}
className="w-full border border-gray-300 rounded-md px-3 py-2 text-gray-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
placeholder={t('groups.namePlaceholder')}
required
/>
</div>
<div>
<label className="block text-gray-700 text-sm font-bold mb-2">
{t('groups.configureTools')}
</label>
<ServerToolConfig
servers={availableServers}
value={formData.servers as IGroupServerConfig[]}
onChange={(servers) => setFormData(prev => ({ ...prev, servers }))}
className="border border-gray-200 rounded-lg p-4 bg-gray-50"
/>
</div>
</div>
</div>
<div className="flex justify-end space-x-3 p-6 pt-4 border-t border-gray-200 flex-shrink-0">
<button
type="button"
onClick={onCancel}
className="px-4 py-2 text-gray-600 hover:text-gray-800 border border-gray-300 rounded-md hover:bg-gray-50 transition-colors"
disabled={isSubmitting}
>
{t('common.cancel')}
</button>
<button
type="submit"
className="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 disabled:opacity-50 transition-colors"
disabled={isSubmitting}
>
{isSubmitting ? t('common.submitting') : t('common.create')}
</button>
</div>
</form>
</div>
</div>
)

View File

@@ -1,7 +1,8 @@
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import ServerForm from './ServerForm'
import { getApiUrl } from '../utils/runtime';
import { apiPost } from '../utils/fetchInterceptor'
import { detectVariables } from '../utils/variableDetection'
interface AddServerFormProps {
onAdd: () => void
@@ -11,35 +12,34 @@ const AddServerForm = ({ onAdd }: AddServerFormProps) => {
const { t } = useTranslation()
const [modalVisible, setModalVisible] = useState(false)
const [error, setError] = useState<string | null>(null)
const [confirmationVisible, setConfirmationVisible] = useState(false)
const [pendingPayload, setPendingPayload] = useState<any>(null)
const [detectedVariables, setDetectedVariables] = useState<string[]>([])
const toggleModal = () => {
setModalVisible(!modalVisible)
setError(null) // Clear any previous errors when toggling modal
setConfirmationVisible(false) // Close confirmation dialog
setPendingPayload(null) // Clear pending payload
}
const handleSubmit = async (payload: any) => {
const handleConfirmSubmit = async () => {
if (pendingPayload) {
await submitServer(pendingPayload)
setConfirmationVisible(false)
setPendingPayload(null)
}
}
const submitServer = async (payload: any) => {
try {
setError(null)
const token = localStorage.getItem('mcphub_token');
const response = await fetch(getApiUrl('/servers'), {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-auth-token': token || ''
},
body: JSON.stringify(payload),
})
const result = await apiPost('/servers', payload)
const result = await response.json()
if (!response.ok) {
if (!result.success) {
// Use specific error message from the response if available
if (result && result.message) {
setError(result.message)
} else if (response.status === 400) {
setError(t('server.invalidData'))
} else if (response.status === 409) {
setError(t('server.alreadyExists', { serverName: payload.name }))
} else {
setError(t('server.addError'))
}
@@ -65,11 +65,31 @@ const AddServerForm = ({ onAdd }: AddServerFormProps) => {
}
}
const handleSubmit = async (payload: any) => {
try {
// Check for variables in the payload
const variables = detectVariables(payload)
if (variables.length > 0) {
// Show confirmation dialog
setDetectedVariables(variables)
setPendingPayload(payload)
setConfirmationVisible(true)
} else {
// Submit directly if no variables found
await submitServer(payload)
}
} catch (err) {
console.error('Error processing server submission:', err)
setError(t('errors.serverAdd'))
}
}
return (
<div>
<button
onClick={toggleModal}
className="w-full bg-blue-100 text-blue-800 rounded hover:bg-blue-200 py-2 px-4 flex items-center justify-center"
className="w-full bg-blue-100 text-blue-800 rounded hover:bg-blue-200 py-2 px-4 flex items-center justify-center btn-primary"
>
<svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4 mr-2" viewBox="0 0 20 20" fill="currentColor">
<path fillRule="evenodd" d="M10 3a1 1 0 011 1v5h5a1 1 0 110 2h-5v5a1 1 0 11-2 0v-5H4a1 1 0 110-2h5V4a1 1 0 011-1z" clipRule="evenodd" />
@@ -87,6 +107,60 @@ const AddServerForm = ({ onAdd }: AddServerFormProps) => {
/>
</div>
)}
{confirmationVisible && (
<div className="fixed inset-0 bg-black/50 z-[60] flex items-center justify-center p-4">
<div className="bg-white rounded-lg p-6 w-full max-w-md">
<h3 className="text-lg font-semibold text-gray-900 mb-4">
{t('server.confirmVariables')}
</h3>
<p className="text-gray-600 mb-4">
{t('server.variablesDetected')}
</p>
<div className="bg-yellow-50 border border-yellow-200 rounded p-3 mb-4">
<div className="flex items-start">
<div className="flex-shrink-0">
<svg className="h-5 w-5 text-yellow-400" viewBox="0 0 20 20" fill="currentColor">
<path fillRule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clipRule="evenodd" />
</svg>
</div>
<div className="ml-3">
<h4 className="text-sm font-medium text-yellow-800">
{t('server.detectedVariables')}:
</h4>
<ul className="mt-1 text-sm text-yellow-700">
{detectedVariables.map((variable, index) => (
<li key={index} className="font-mono">
${`{${variable}}`}
</li>
))}
</ul>
</div>
</div>
</div>
<p className="text-gray-600 text-sm mb-6">
{t('server.confirmVariablesMessage')}
</p>
<div className="flex justify-end space-x-3">
<button
onClick={() => {
setConfirmationVisible(false)
setPendingPayload(null)
}}
className="px-4 py-2 text-gray-600 border border-gray-300 rounded hover:bg-gray-50 btn-secondary"
>
{t('common.cancel')}
</button>
<button
onClick={handleConfirmSubmit}
className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 btn-primary"
>
{t('server.confirmAndAdd')}
</button>
</div>
</div>
</div>
)}
</div>
)
}

View File

@@ -0,0 +1,153 @@
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useUserData } from '@/hooks/useUserData';
import { UserFormData } from '@/types';
interface AddUserFormProps {
onAdd: () => void;
onCancel: () => void;
}
const AddUserForm = ({ onAdd, onCancel }: AddUserFormProps) => {
const { t } = useTranslation();
const { createUser } = useUserData();
const [error, setError] = useState<string | null>(null);
const [isSubmitting, setIsSubmitting] = useState(false);
const [formData, setFormData] = useState<UserFormData>({
username: '',
password: '',
isAdmin: false,
});
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setError(null);
if (!formData.username.trim()) {
setError(t('users.usernameRequired'));
return;
}
if (!formData.password.trim()) {
setError(t('users.passwordRequired'));
return;
}
if (formData.password.length < 6) {
setError(t('users.passwordTooShort'));
return;
}
setIsSubmitting(true);
try {
const result = await createUser(formData);
if (result?.success) {
onAdd();
} else {
setError(result?.message || t('users.createError'));
}
} catch (err) {
setError(err instanceof Error ? err.message : t('users.createError'));
} finally {
setIsSubmitting(false);
}
};
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value, type, checked } = e.target;
setFormData(prev => ({
...prev,
[name]: type === 'checkbox' ? checked : value
}));
};
return (
<div className="fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full flex items-center justify-center z-50">
<div className="bg-white p-8 rounded-lg shadow-xl max-w-md w-full mx-4">
<form onSubmit={handleSubmit}>
<h2 className="text-xl font-semibold text-gray-800 mb-4">{t('users.addNew')}</h2>
{error && (
<div className="bg-red-100 border-l-4 border-red-500 text-red-700 p-3 mb-4">
<p className="text-sm">{error}</p>
</div>
)}
<div className="space-y-4">
<div>
<label htmlFor="username" className="block text-sm font-medium text-gray-700 mb-1">
{t('users.username')} *
</label>
<input
type="text"
id="username"
name="username"
value={formData.username}
onChange={handleInputChange}
placeholder={t('users.usernamePlaceholder')}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
required
disabled={isSubmitting}
/>
</div>
<div>
<label htmlFor="password" className="block text-sm font-medium text-gray-700 mb-1">
{t('users.password')} *
</label>
<input
type="password"
id="password"
name="password"
value={formData.password}
onChange={handleInputChange}
placeholder={t('users.passwordPlaceholder')}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
required
disabled={isSubmitting}
minLength={6}
/>
</div>
<div className="flex items-center">
<input
type="checkbox"
id="isAdmin"
name="isAdmin"
checked={formData.isAdmin}
onChange={handleInputChange}
className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"
disabled={isSubmitting}
/>
<label htmlFor="isAdmin" className="ml-2 block text-sm text-gray-700">
{t('users.adminRole')}
</label>
</div>
</div>
<div className="flex justify-end space-x-3 mt-6">
<button
type="button"
onClick={onCancel}
className="px-4 py-2 text-gray-700 bg-gray-200 rounded hover:bg-gray-300 transition-colors duration-200"
disabled={isSubmitting}
>
{t('common.cancel')}
</button>
<button
type="submit"
className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 transition-colors duration-200 disabled:opacity-50 disabled:cursor-not-allowed"
disabled={isSubmitting}
>
{isSubmitting ? t('common.creating') : t('users.create')}
</button>
</div>
</form>
</div>
</div>
);
};
export default AddUserForm;

View File

@@ -2,6 +2,7 @@ import React, { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { ChangePasswordCredentials } from '../types';
import { changePassword } from '../services/authService';
import { validatePasswordStrength } from '../utils/passwordValidation';
interface ChangePasswordFormProps {
onSuccess?: () => void;
@@ -18,6 +19,7 @@ const ChangePasswordForm: React.FC<ChangePasswordFormProps> = ({ onSuccess, onCa
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [success, setSuccess] = useState(false);
const [passwordErrors, setPasswordErrors] = useState<string[]>([]);
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target;
@@ -25,23 +27,37 @@ const ChangePasswordForm: React.FC<ChangePasswordFormProps> = ({ onSuccess, onCa
setConfirmPassword(value);
} else {
setFormData(prev => ({ ...prev, [name]: value }));
// Validate password strength on change for new password
if (name === 'newPassword') {
const validation = validatePasswordStrength(value);
setPasswordErrors(validation.errors);
}
}
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setError(null);
// Validate password strength
const validation = validatePasswordStrength(formData.newPassword);
if (!validation.isValid) {
setError(t('auth.passwordStrengthError'));
setPasswordErrors(validation.errors);
return;
}
// Validate passwords match
if (formData.newPassword !== confirmPassword) {
setError(t('auth.passwordsNotMatch'));
return;
}
setIsLoading(true);
try {
const response = await changePassword(formData);
if (response.success) {
setSuccess(true);
if (onSuccess) {
@@ -60,7 +76,7 @@ const ChangePasswordForm: React.FC<ChangePasswordFormProps> = ({ onSuccess, onCa
return (
<div className="p-6 bg-white rounded-lg shadow-md">
<h2 className="text-xl font-bold mb-4">{t('auth.changePassword')}</h2>
{success ? (
<div className="bg-green-100 border border-green-400 text-green-700 px-4 py-3 rounded mb-4">
{t('auth.changePasswordSuccess')}
@@ -72,7 +88,7 @@ const ChangePasswordForm: React.FC<ChangePasswordFormProps> = ({ onSuccess, onCa
{error}
</div>
)}
<div className="mb-4">
<label className="block text-gray-700 text-sm font-bold mb-2" htmlFor="currentPassword">
{t('auth.currentPassword')}
@@ -81,13 +97,13 @@ const ChangePasswordForm: React.FC<ChangePasswordFormProps> = ({ onSuccess, onCa
type="password"
id="currentPassword"
name="currentPassword"
className="w-full p-2 border rounded focus:outline-none focus:ring-2 focus:ring-blue-500"
className="w-full p-2 border rounded focus:outline-none focus:ring-2 focus:ring-blue-500 form-input"
value={formData.currentPassword}
onChange={handleChange}
required
/>
</div>
<div className="mb-4">
<label className="block text-gray-700 text-sm font-bold mb-2" htmlFor="newPassword">
{t('auth.newPassword')}
@@ -96,14 +112,30 @@ const ChangePasswordForm: React.FC<ChangePasswordFormProps> = ({ onSuccess, onCa
type="password"
id="newPassword"
name="newPassword"
className="w-full p-2 border rounded focus:outline-none focus:ring-2 focus:ring-blue-500"
className="w-full p-2 border rounded focus:outline-none focus:ring-2 focus:ring-blue-500 form-input"
value={formData.newPassword}
onChange={handleChange}
required
minLength={6}
minLength={8}
/>
{/* Password strength hints */}
{formData.newPassword && passwordErrors.length > 0 && (
<div className="mt-2 text-sm text-gray-600">
<p className="font-semibold mb-1">{t('auth.passwordStrengthHint')}</p>
<ul className="list-disc list-inside space-y-1">
{passwordErrors.map((errorKey) => (
<li key={errorKey} className="text-red-600">
{t(`auth.${errorKey}`)}
</li>
))}
</ul>
</div>
)}
{formData.newPassword && passwordErrors.length === 0 && (
<p className="mt-2 text-sm text-green-600"> {t('auth.passwordStrengthHint')}</p>
)}
</div>
<div className="mb-6">
<label className="block text-gray-700 text-sm font-bold mb-2" htmlFor="confirmPassword">
{t('auth.confirmPassword')}
@@ -112,14 +144,14 @@ const ChangePasswordForm: React.FC<ChangePasswordFormProps> = ({ onSuccess, onCa
type="password"
id="confirmPassword"
name="confirmPassword"
className="w-full p-2 border rounded focus:outline-none focus:ring-2 focus:ring-blue-500"
className="w-full p-2 border rounded focus:outline-none focus:ring-2 focus:ring-blue-500 form-input"
value={confirmPassword}
onChange={handleChange}
required
minLength={6}
minLength={8}
/>
</div>
<div className="flex justify-end space-x-2">
{onCancel && (
<button
@@ -134,7 +166,7 @@ const ChangePasswordForm: React.FC<ChangePasswordFormProps> = ({ onSuccess, onCa
<button
type="submit"
disabled={isLoading}
className="py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
className="py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 btn-primary"
>
{isLoading ? (
<span className="flex items-center">

View File

@@ -0,0 +1,144 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import { CloudServer } from '@/types';
interface CloudServerCardProps {
server: CloudServer;
onClick: (server: CloudServer) => void;
}
const CloudServerCard: React.FC<CloudServerCardProps> = ({ server, onClick }) => {
const { t } = useTranslation();
const handleClick = () => {
onClick(server);
};
// Extract a brief description from content if description is too long
const getDisplayDescription = () => {
if (server.description && server.description.length <= 150) {
return server.description;
}
// Try to extract a summary from content
if (server.content) {
const lines = server.content.split('\n').filter(line => line.trim());
for (const line of lines) {
if (line.length > 50 && line.length <= 150) {
return line;
}
}
}
return server.description ?
server.description.slice(0, 150) + '...' :
t('cloud.noDescription');
};
// Format date for display
const formatDate = (dateString: string) => {
try {
const date = new Date(dateString);
const year = date.getFullYear();
const month = (date.getMonth() + 1).toString().padStart(2, '0');
const day = date.getDate().toString().padStart(2, '0');
return `${year}/${month}/${day}`;
} catch {
return '';
}
};
// Get initials for avatar
const getAuthorInitials = (name: string) => {
return name
.split(' ')
.map(word => word.charAt(0))
.join('')
.toUpperCase()
.slice(0, 2);
};
return (
<div
className="bg-white border border-gray-200 rounded-xl p-4 hover:shadow-lg hover:border-blue-400 hover:-translate-y-1 transition-all duration-300 cursor-pointer group relative overflow-hidden h-full flex flex-col"
onClick={handleClick}
>
{/* Background gradient overlay on hover */}
<div className="absolute inset-0 bg-gradient-to-br from-blue-50/0 to-purple-50/0 group-hover:from-blue-50/30 group-hover:to-purple-50/30 transition-all duration-300 pointer-events-none" />
{/* Server Header */}
<div className="relative z-10 flex-1 flex flex-col">
<div className="flex items-start justify-between mb-3">
<div className="flex-1">
<h3 className="text-lg font-bold text-gray-900 group-hover:text-blue-600 transition-colors duration-200 mb-2 line-clamp-2">
{server.title || server.name}
</h3>
{/* Author Section */}
<div className="flex items-center space-x-2 mb-2">
<div className="w-7 h-7 bg-gradient-to-br from-blue-500 to-purple-600 rounded-full flex items-center justify-center text-white text-xs font-semibold">
{getAuthorInitials(server.author_name)}
</div>
<div>
<p className="text-sm font-medium text-gray-700">{server.author_name}</p>
{server.updated_at && (
<p className="text-xs text-gray-500">
{t('cloud.updated')} {formatDate(server.updated_at)}
</p>
)}
</div>
</div>
</div>
{/* Server Type Badge */}
<div className="flex flex-col items-end space-y-2">
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800">
MCP Server
</span>
</div>
</div>
{/* Description */}
<div className="mb-3 flex-1">
<p className="text-gray-600 text-sm leading-relaxed line-clamp-2">
{getDisplayDescription()}
</p>
</div>
{/* Tools Info */}
{server.tools && server.tools.length > 0 && (
<div className="mb-3">
<div className="flex items-center space-x-2">
<svg className="w-4 h-4 text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
<span className="text-sm text-gray-600 font-medium">
{server.tools.length} {server.tools.length === 1 ? t('cloud.tool') : t('cloud.tools')}
</span>
</div>
</div>
)}
{/* Footer - 固定在底部 */}
<div className="flex items-center justify-between pt-3 border-t border-gray-100 mt-auto">
<div className="flex items-center space-x-2 text-xs text-gray-500">
<svg className="w-3 h-3" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M6 2a1 1 0 00-1 1v1H4a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V6a2 2 0 00-2-2h-1V3a1 1 0 10-2 0v1H7V3a1 1 0 00-1-1zm0 5a1 1 0 000 2h8a1 1 0 100-2H6z" clipRule="evenodd" />
</svg>
<span>{formatDate(server.created_at)}</span>
</div>
<div className="flex items-center text-blue-600 text-sm font-medium group-hover:text-blue-700 transition-colors">
<span>{t('cloud.viewDetails')}</span>
<svg className="w-4 h-4 ml-1 transform group-hover:translate-x-1 transition-transform" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
</svg>
</div>
</div>
</div>
</div>
);
};
export default CloudServerCard;

View File

@@ -0,0 +1,573 @@
import React, { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { CloudServer, CloudServerTool, ServerConfig } from '@/types';
import { apiGet } from '@/utils/fetchInterceptor';
import { useSettingsData } from '@/hooks/useSettingsData';
import MCPRouterApiKeyError from './MCPRouterApiKeyError';
import ServerForm from './ServerForm';
interface CloudServerDetailProps {
serverName: string;
onBack: () => void;
onCallTool?: (serverName: string, toolName: string, args: Record<string, any>) => Promise<any>;
fetchServerTools?: (serverName: string) => Promise<CloudServerTool[]>;
onInstall?: (server: CloudServer, config: ServerConfig) => void;
installing?: boolean;
isInstalled?: boolean;
}
const CloudServerDetail: React.FC<CloudServerDetailProps> = ({
serverName,
onBack,
onCallTool,
fetchServerTools,
onInstall,
installing = false,
isInstalled = false
}) => {
const { t } = useTranslation();
const { mcpRouterConfig } = useSettingsData();
const [server, setServer] = useState<CloudServer | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [tools, setTools] = useState<CloudServerTool[]>([]);
const [loadingTools, setLoadingTools] = useState(false);
const [toolsApiKeyError, setToolsApiKeyError] = useState(false);
const [toolCallLoading, setToolCallLoading] = useState<string | null>(null);
const [toolCallResults, setToolCallResults] = useState<Record<string, any>>({});
const [toolArgs, setToolArgs] = useState<Record<string, Record<string, any>>>({});
const [expandedSchemas, setExpandedSchemas] = useState<Record<string, boolean>>({});
const [modalVisible, setModalVisible] = useState(false);
const [installError, setInstallError] = useState<string | null>(null);
// Helper function to check if error is MCPRouter API key not configured
const isMCPRouterApiKeyError = (errorMessage: string) => {
console.error('Checking for MCPRouter API key error:', errorMessage);
return errorMessage === 'MCPROUTER_API_KEY_NOT_CONFIGURED' ||
errorMessage.toLowerCase().includes('mcprouter api key not configured');
};
// Helper function to determine button state for install
const getInstallButtonProps = () => {
if (isInstalled) {
return {
className: "bg-green-600 cursor-default px-4 py-2 rounded text-sm font-medium text-white",
disabled: true,
text: t('market.installed')
};
} else if (installing) {
return {
className: "bg-gray-400 cursor-not-allowed px-4 py-2 rounded text-sm font-medium text-white",
disabled: true,
text: t('market.installing')
};
} else {
return {
className: "bg-blue-600 hover:bg-blue-700 px-4 py-2 rounded text-sm font-medium text-white transition-colors",
disabled: false,
text: t('market.install')
};
}
};
// Handle install button click
const handleInstall = () => {
if (!isInstalled && onInstall) {
setModalVisible(true);
setInstallError(null);
}
};
// Handle modal close
const handleModalClose = () => {
setModalVisible(false);
setInstallError(null);
};
// Handle install form submission
const handleInstallSubmit = async (payload: any) => {
try {
if (!server || !onInstall) return;
setInstallError(null);
onInstall(server, payload.config);
setModalVisible(false);
} catch (err) {
console.error('Error installing server:', err);
setInstallError(t('errors.serverInstall'));
}
};
// Load server details
useEffect(() => {
const loadServerDetails = async () => {
try {
setLoading(true);
setError(null);
const response = await apiGet(`/cloud/servers/${serverName}`);
if (response && response.success && response.data) {
setServer(response.data);
setTools(response.data.tools || []);
} else {
setError(t('cloud.serverNotFound'));
}
} catch (err) {
console.error('Failed to load server details:', err);
setError(err instanceof Error ? err.message : String(err));
} finally {
setLoading(false);
}
};
loadServerDetails();
}, [serverName, t]);
// Load tools if not already loaded
useEffect(() => {
const loadTools = async () => {
if (server && (!server.tools || server.tools.length === 0) && fetchServerTools) {
setLoadingTools(true);
setToolsApiKeyError(false);
try {
const fetchedTools = await fetchServerTools(server.name);
setTools(fetchedTools);
} catch (error) {
console.error('Failed to load tools:', error);
const errorMessage = error instanceof Error ? error.message : String(error);
if (isMCPRouterApiKeyError(errorMessage)) {
setToolsApiKeyError(true);
}
} finally {
setLoadingTools(false);
}
}
};
loadTools();
}, [server?.name, server?.tools, fetchServerTools]);
// Format creation date
const formatDate = (dateStr: string) => {
try {
const date = new Date(dateStr);
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
const hours = String(date.getHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0');
const seconds = String(date.getSeconds()).padStart(2, '0');
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
} catch {
return dateStr;
}
};
// Handle tool argument changes
const handleArgChange = (toolName: string, argName: string, value: any) => {
setToolArgs(prev => ({
...prev,
[toolName]: {
...prev[toolName],
[argName]: value
}
}));
};
// Handle tool call
const handleCallTool = async (toolName: string) => {
if (!onCallTool || !server) return;
setToolCallLoading(toolName);
try {
const args = toolArgs[toolName] || {};
const result = await onCallTool(server.server_key, toolName, args);
setToolCallResults(prev => ({
...prev,
[toolName]: result
}));
} catch (error) {
console.error('Tool call failed:', error);
const errorMessage = error instanceof Error ? error.message : String(error);
setToolCallResults(prev => ({
...prev,
[toolName]: { error: errorMessage }
}));
} finally {
setToolCallLoading(null);
}
};
// Toggle schema visibility
const toggleSchema = (toolName: string) => {
setExpandedSchemas(prev => ({
...prev,
[toolName]: !prev[toolName]
}));
};
// Render tool input field based on schema
const renderToolInput = (tool: CloudServerTool, propName: string, propSchema: any) => {
const currentValue = toolArgs[tool.name]?.[propName] || '';
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>) => {
let value: any = e.target.value;
// Convert value based on schema type
if (propSchema.type === 'number' || propSchema.type === 'integer') {
value = value === '' ? undefined : Number(value);
} else if (propSchema.type === 'boolean') {
value = e.target.value === 'true';
}
handleArgChange(tool.name, propName, value);
};
if (propSchema.type === 'boolean') {
return (
<select
value={currentValue === true ? 'true' : currentValue === false ? 'false' : ''}
onChange={handleChange}
className="w-full border rounded-md px-3 py-2 border-gray-300 focus:outline-none focus:ring-2 focus:ring-blue-500 form-input"
>
<option value=""></option>
<option value="true">{t('common.true')}</option>
<option value="false">{t('common.false')}</option>
</select>
);
} else if (propSchema.type === 'number' || propSchema.type === 'integer') {
return (
<input
type="number"
step={propSchema.type === 'integer' ? '1' : 'any'}
value={currentValue || ''}
onChange={handleChange}
className="w-full border rounded-md px-3 py-2 border-gray-300 focus:outline-none focus:ring-2 focus:ring-blue-500 form-input"
/>
);
} else {
return (
<input
type="text"
value={currentValue || ''}
onChange={handleChange}
className="w-full border rounded-md px-3 py-2 border-gray-300 focus:outline-none focus:ring-2 focus:ring-blue-500 form-input"
/>
);
}
};
return (
<div className="bg-white rounded-lg shadow-md p-6">
<div className="mb-6">
<button
onClick={onBack}
className="inline-flex items-center text-gray-600 hover:text-gray-900 transition-colors group"
>
<svg className="h-5 w-5 mr-2 transform group-hover:-translate-x-1 transition-transform" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
<path fillRule="evenodd" d="M9.707 16.707a1 1 0 01-1.414 0l-6-6a1 1 0 010-1.414l6-6a1 1 0 011.414 1.414L5.414 9H17a1 1 0 110 2H5.414l4.293 4.293a1 1 0 010 1.414z" clipRule="evenodd" />
</svg>
{t('cloud.backToList')}
</button>
</div>
{loading ? (
<div className="bg-white rounded-xl shadow-sm p-12">
<div className="flex flex-col items-center">
<svg className="animate-spin h-12 w-12 text-blue-500 mb-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
<p className="text-gray-600 text-lg">{t('app.loading')}</p>
</div>
</div>
) : error && !isMCPRouterApiKeyError(error) ? (
<div className="bg-white rounded-xl shadow-sm p-6">
<div className="bg-red-50 border border-red-200 rounded-lg p-4">
<div className="flex items-center">
<svg className="h-5 w-5 text-red-400 mr-3" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clipRule="evenodd" />
</svg>
<p className="text-red-700">{error}</p>
</div>
</div>
</div>
) : !server ? (
<div className="bg-white rounded-xl shadow-sm p-12">
<div className="text-center">
<svg className="h-12 w-12 text-gray-400 mx-auto mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1} d="M9.172 16.172a4 4 0 015.656 0M9 12h6m-6-4h6m2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" />
</svg>
<p className="text-gray-600 text-lg">{t('cloud.serverNotFound')}</p>
</div>
</div>
) : (
<div className="space-y-6">
{/* Server Header Card */}
<div className="bg-white rounded-xl shadow-sm overflow-hidden">
<div className="bg-gradient-to-r from-gray-100 to-gray-200 px-6 py-4">
<div className="flex justify-between items-end">
<div className="flex-1">
<h1 className="text-2xl font-bold text-gray-800 mb-2">
{server.title || server.name}
</h1>
<div className="flex flex-wrap items-center gap-4 text-gray-600">
<span className="text-sm bg-white/60 text-gray-700 px-3 py-1 rounded-full">
{server.name}
</span>
<div className="flex items-center">
<svg className="h-4 w-4 mr-1" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M10 9a3 3 0 100-6 3 3 0 000 6zm-7 9a7 7 0 1114 0H3z" clipRule="evenodd" />
</svg>
{t('cloud.by')} {server.author_name}
</div>
</div>
</div>
<div className="text-right flex flex-col items-end gap-3">
<div className="text-xs text-gray-500">
{t('cloud.updated')}: {formatDate(server.updated_at)}
</div>
{onInstall && !isMCPRouterApiKeyError(error || '') && !toolsApiKeyError && (
<button
onClick={handleInstall}
disabled={getInstallButtonProps().disabled}
className={getInstallButtonProps().className}
>
{getInstallButtonProps().text}
</button>
)}
</div>
</div>
</div>
</div>
{/* Description Card */}
<div className="bg-white rounded-xl shadow-sm p-6">
<h2 className="text-xl font-semibold text-gray-900 mb-4 flex items-center">
<svg className="h-5 w-5 text-gray-500 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
{t('cloud.description')}
</h2>
<p className="text-gray-700 leading-relaxed">{server.description}</p>
</div>
{/* Content Card */}
{server.content && (
<div className="bg-white rounded-xl shadow-sm p-6">
<h2 className="text-xl font-semibold text-gray-900 mb-4 flex items-center">
<svg className="h-5 w-5 text-gray-500 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12h6m-6 4h6m2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" />
</svg>
{t('cloud.details')}
</h2>
<div className="bg-gray-50 border border-gray-200 rounded-lg p-4 overflow-auto">
<pre className="text-sm text-gray-800 whitespace-pre-wrap">{server.content}</pre>
</div>
</div>
)}
{/* Tools Card */}
<div className="bg-white rounded-xl shadow-sm p-6">
<h2 className="text-xl font-semibold text-gray-900 mb-4 flex items-center">
<svg className="h-5 w-5 text-gray-500 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
{t('cloud.tools')}
{tools.length > 0 && (
<span className="ml-2 bg-blue-100 text-blue-800 text-sm font-medium px-2.5 py-0.5 rounded-full">
{tools.length}
</span>
)}
</h2>
{/* Check for API key error */}
{toolsApiKeyError && (
<MCPRouterApiKeyError />
)}
{loadingTools ? (
<div className="flex items-center justify-center py-12">
<svg className="animate-spin h-8 w-8 text-blue-500 mr-3" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
<span className="text-gray-600">{t('cloud.loadingTools')}</span>
</div>
) : tools.length === 0 && !toolsApiKeyError ? (
<div className="text-center py-12">
<svg className="h-12 w-12 text-gray-400 mx-auto mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1} d="M20 13V6a2 2 0 00-2-2H6a2 2 0 00-2 2v7m16 0v5a2 2 0 01-2 2H6a2 2 0 01-2-2v-5m16 0h-2.586a1 1 0 00-.707.293l-2.414 2.414a1 1 0 01-.707.293h-3.172a1 1 0 01-.707-.293l-2.414-2.414A1 1 0 006.586 13H4" />
</svg>
<p className="text-gray-600">{t('cloud.noTools')}</p>
</div>
) : tools.length > 0 ? (
<div className="space-y-4">
{tools.map((tool, index) => (
<div key={index} className="border border-gray-200 rounded-lg p-6 hover:border-gray-300 transition-colors">
<div className="flex justify-between items-start mb-4">
<div className="flex-1">
<h3 className="text-lg font-medium text-gray-900 mb-2 flex items-center">
<span className="bg-blue-100 text-blue-800 text-xs font-medium px-2 py-1 rounded mr-3">
TOOL
</span>
{tool.name}
</h3>
<p className="text-gray-600 leading-relaxed whitespace-pre-wrap">{tool.description}</p>
</div>
{onCallTool && (
<button
onClick={() => handleCallTool(tool.name)}
disabled={toolCallLoading === tool.name}
className="ml-4 bg-blue-600 text-white px-4 py-2 rounded-lg hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors flex items-center min-w-[100px] justify-center"
>
{toolCallLoading === tool.name ? (
<>
<svg className="animate-spin h-4 w-4 mr-2" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
{t('cloud.calling')}
</>
) : (
<>
<svg className="h-4 w-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M14.828 14.828a4 4 0 01-5.656 0M9 10h6m2 8l4-4H7l4 4z" />
</svg>
{t('cloud.callTool')}
</>
)}
</button>
)}
</div>
{/* Tool inputs */}
{tool.inputSchema && tool.inputSchema.properties && Object.keys(tool.inputSchema.properties).length > 0 && (
<div className="border-t border-gray-100 pt-4">
<div className="flex items-center gap-3 mb-4">
<h4 className="text-sm font-medium text-gray-700">{t('cloud.parameters')}</h4>
<button
onClick={() => toggleSchema(tool.name)}
className="text-sm text-blue-600 hover:text-blue-800 focus:outline-none flex items-center gap-1 transition-colors"
>
{t('cloud.viewSchema')}
<svg
className={`h-3 w-3 transition-transform duration-200 ${expandedSchemas[tool.name] ? 'rotate-90' : 'rotate-0'}`}
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
>
<path fillRule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clipRule="evenodd" />
</svg>
</button>
</div>
{/* Schema content */}
{expandedSchemas[tool.name] && (
<div className="mb-4">
<div className="bg-gray-50 border border-gray-200 rounded-lg p-3 overflow-auto">
<pre className="text-sm text-gray-800">
{JSON.stringify(tool.inputSchema, null, 2)}
</pre>
</div>
</div>
)}
<div className="space-y-4">
{Object.entries(tool.inputSchema.properties).map(([propName, propSchema]: [string, any]) => (
<div key={propName} className="space-y-2">
<label className="block text-sm font-medium text-gray-700">
{propName}
{tool.inputSchema.required?.includes(propName) && (
<span className="text-red-500 ml-1">*</span>
)}
</label>
{propSchema.description && (
<p className="text-xs text-gray-500">{propSchema.description}</p>
)}
{renderToolInput(tool, propName, propSchema)}
</div>
))}
</div>
</div>
)}
{/* Tool call result */}
{toolCallResults[tool.name] && (
<div className="border-t border-gray-100 pt-4 mt-4">
{toolCallResults[tool.name].error ? (
<>
{isMCPRouterApiKeyError(toolCallResults[tool.name].error) ? (
<MCPRouterApiKeyError />
) : (
<>
<h4 className="text-sm font-medium text-red-600 mb-3 flex items-center">
<svg className="h-4 w-4 text-red-500 mr-2" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clipRule="evenodd" />
</svg>
{t('cloud.error')}
</h4>
<div className="bg-red-50 border border-red-200 rounded-lg p-4">
<pre className="text-sm text-red-800 whitespace-pre-wrap overflow-auto">
{toolCallResults[tool.name].error}
</pre>
</div>
</>
)}
</>
) : (
<>
<h4 className="text-sm font-medium text-gray-700 mb-3 flex items-center">
<svg className="h-4 w-4 text-green-500 mr-2" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clipRule="evenodd" />
</svg>
{t('cloud.result')}
</h4>
<div className="bg-gray-50 border border-gray-200 rounded-lg p-4">
<pre className="text-sm text-gray-800 whitespace-pre-wrap overflow-auto">
{JSON.stringify(toolCallResults[tool.name], null, 2)}
</pre>
</div>
</>
)}
</div>
)}
</div>
))}
</div>
) : null}
</div>
</div>
)}
{/* Install Modal */}
{modalVisible && server && (
<div className="fixed inset-0 bg-black/50 z-50 flex items-center justify-center p-4">
<ServerForm
onSubmit={handleInstallSubmit}
onCancel={handleModalClose}
modalTitle={t('cloud.installServer', { name: server.title || server.name })}
formError={installError}
initialData={{
name: server.name,
status: 'disconnected',
config: {
type: 'streamable-http',
url: server.server_url,
headers: {
'Authorization': `Bearer ${mcpRouterConfig.apiKey || '<MCPROUTER_API_KEY>'}`,
'HTTP-Referer': mcpRouterConfig.referer || '<YOUR_APP_URL>',
'X-Title': mcpRouterConfig.title || '<YOUR_APP_NAME>'
}
}
}}
/>
</div>
)}
</div>
);
};
export default CloudServerDetail;

View File

@@ -0,0 +1,394 @@
import React, { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { apiPost, apiGet, apiPut, fetchWithInterceptors } from '@/utils/fetchInterceptor';
import { getApiUrl } from '@/utils/runtime';
import ConfirmDialog from '@/components/ui/ConfirmDialog';
interface DxtUploadFormProps {
onSuccess: (serverConfig: any) => void;
onCancel: () => void;
}
interface DxtUploadResponse {
success: boolean;
data?: {
manifest: any;
extractDir: string;
};
message?: string;
}
const DxtUploadForm: React.FC<DxtUploadFormProps> = ({ onSuccess, onCancel }) => {
const { t } = useTranslation();
const [isDragging, setIsDragging] = useState(false);
const [isUploading, setIsUploading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [selectedFile, setSelectedFile] = useState<File | null>(null);
const [showServerForm, setShowServerForm] = useState(false);
const [manifestData, setManifestData] = useState<any>(null);
const [extractDir, setExtractDir] = useState<string>('');
const [showConfirmDialog, setShowConfirmDialog] = useState(false);
const [pendingServerName, setPendingServerName] = useState<string>('');
const handleDragOver = (e: React.DragEvent) => {
e.preventDefault();
setIsDragging(true);
};
const handleDragLeave = (e: React.DragEvent) => {
e.preventDefault();
setIsDragging(false);
};
const handleDrop = (e: React.DragEvent) => {
e.preventDefault();
setIsDragging(false);
const files = e.dataTransfer.files;
if (files.length > 0) {
const file = files[0];
if (file.name.endsWith('.dxt')) {
setSelectedFile(file);
setError(null);
} else {
setError(t('dxt.invalidFileType'));
}
}
};
const handleFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
const files = e.target.files;
if (files && files.length > 0) {
const file = files[0];
if (file.name.endsWith('.dxt')) {
setSelectedFile(file);
setError(null);
} else {
setError(t('dxt.invalidFileType'));
}
}
};
const handleUpload = async () => {
if (!selectedFile) {
setError(t('dxt.noFileSelected'));
return;
}
setIsUploading(true);
setError(null);
try {
const formData = new FormData();
formData.append('dxtFile', selectedFile);
const response = await fetchWithInterceptors(getApiUrl('/dxt/upload'), {
method: 'POST',
body: formData,
});
const result: DxtUploadResponse = await response.json();
if (!response.ok) {
throw new Error(result.message || `HTTP error! Status: ${response.status}`);
}
if (result.success && result.data) {
setManifestData(result.data.manifest);
setExtractDir(result.data.extractDir);
setShowServerForm(true);
} else {
throw new Error(result.message || t('dxt.uploadFailed'));
}
} catch (err) {
console.error('DXT upload error:', err);
setError(err instanceof Error ? err.message : t('dxt.uploadFailed'));
} finally {
setIsUploading(false);
}
};
const handleInstallServer = async (serverName: string, forceOverride: boolean = false) => {
setIsUploading(true);
setError(null);
try {
// Convert DXT manifest to MCPHub stdio server configuration
const serverConfig = convertDxtToMcpConfig(manifestData, extractDir, serverName);
// First, check if server exists
if (!forceOverride) {
const checkResult = await apiGet('/servers');
if (checkResult.success) {
const existingServer = checkResult.data?.find((server: any) => server.name === serverName);
if (existingServer) {
// Server exists, show confirmation dialog
setPendingServerName(serverName);
setShowConfirmDialog(true);
setIsUploading(false);
return;
}
}
}
// Install or override the server
let result;
if (forceOverride) {
result = await apiPut(`/servers/${encodeURIComponent(serverName)}`, {
name: serverName,
config: serverConfig,
});
} else {
result = await apiPost('/servers', {
name: serverName,
config: serverConfig,
});
}
if (result.success) {
onSuccess(serverConfig);
} else {
throw new Error(result.message || t('dxt.installFailed'));
}
} catch (err) {
console.error('DXT install error:', err);
setError(err instanceof Error ? err.message : t('dxt.installFailed'));
setIsUploading(false);
}
};
const handleConfirmOverride = () => {
setShowConfirmDialog(false);
if (pendingServerName) {
handleInstallServer(pendingServerName, true);
}
};
const handleCancelOverride = () => {
setShowConfirmDialog(false);
setPendingServerName('');
setIsUploading(false);
};
const convertDxtToMcpConfig = (manifest: any, extractPath: string, _serverName: string) => {
const mcpConfig = manifest.server?.mcp_config || {};
// Convert DXT manifest to MCPHub stdio configuration
const config: any = {
type: 'stdio',
command: mcpConfig.command || 'node',
args: (mcpConfig.args || []).map((arg: string) =>
arg.replace('${__dirname}', extractPath)
),
};
// Add environment variables if they exist
if (mcpConfig.env && Object.keys(mcpConfig.env).length > 0) {
config.env = { ...mcpConfig.env };
// Replace ${__dirname} in environment variables
Object.keys(config.env).forEach(key => {
if (typeof config.env[key] === 'string') {
config.env[key] = config.env[key].replace('${__dirname}', extractPath);
}
});
}
return config;
};
if (showServerForm && manifestData) {
return (
<>
<ConfirmDialog
isOpen={showConfirmDialog}
onClose={handleCancelOverride}
onConfirm={handleConfirmOverride}
title={t('dxt.serverExistsTitle')}
message={t('dxt.serverExistsConfirm', { serverName: pendingServerName })}
confirmText={t('dxt.override')}
cancelText={t('common.cancel')}
variant="warning"
/>
<div className={`fixed inset-0 bg-black/50 z-50 flex items-center justify-center p-4 ${showConfirmDialog ? 'opacity-50 pointer-events-none' : ''}`}>
<div className="bg-white shadow rounded-lg p-6 w-full max-w-2xl max-h-screen overflow-y-auto">
<div className="flex justify-between items-center mb-6">
<h2 className="text-xl font-semibold text-gray-900">{t('dxt.installServer')}</h2>
<button onClick={onCancel} className="text-gray-500 hover:text-gray-700">
</button>
</div>
{error && (
<div className="mb-4 bg-red-50 border-l-4 border-red-500 p-4 rounded">
<p className="text-red-700">{error}</p>
</div>
)}
<div className="space-y-6">
{/* Extension Info */}
<div className="bg-gray-50 p-4 rounded-lg">
<h3 className="font-medium text-gray-900 mb-2">{t('dxt.extensionInfo')}</h3>
<div className="space-y-2 text-sm">
<div><strong>{t('dxt.name')}:</strong> {manifestData.display_name || manifestData.name}</div>
<div><strong>{t('dxt.version')}:</strong> {manifestData.version}</div>
<div><strong>{t('dxt.description')}:</strong> {manifestData.description}</div>
{manifestData.author && (
<div><strong>{t('dxt.author')}:</strong> {manifestData.author.name}</div>
)}
{manifestData.tools && manifestData.tools.length > 0 && (
<div>
<strong>{t('dxt.tools')}:</strong>
<ul className="list-disc list-inside ml-4">
{manifestData.tools.map((tool: any, index: number) => (
<li key={index}>{tool.name} - {tool.description}</li>
))}
</ul>
</div>
)}
</div>
</div>
{/* Server Configuration */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
{t('dxt.serverName')}
</label>
<input
type="text"
id="serverName"
defaultValue={manifestData.name}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 form-input"
placeholder={t('dxt.serverNamePlaceholder')}
/>
</div>
{/* Action Buttons */}
<div className="flex justify-end space-x-4">
<button
onClick={onCancel}
disabled={isUploading}
className="px-4 py-2 text-gray-700 bg-gray-200 rounded hover:bg-gray-300 disabled:opacity-50 btn-secondary"
>
{t('common.cancel')}
</button>
<button
onClick={() => {
const nameInput = document.getElementById('serverName') as HTMLInputElement;
const serverName = nameInput?.value.trim() || manifestData.name;
handleInstallServer(serverName);
}}
disabled={isUploading}
className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 disabled:opacity-50 flex items-center btn-primary"
>
{isUploading ? (
<>
<svg className="animate-spin h-4 w-4 mr-2" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
{t('dxt.installing')}
</>
) : (
t('dxt.install')
)}
</button>
</div>
</div>
</div>
</div>
</>
);
}
return (
<div className="fixed inset-0 bg-black/50 z-50 flex items-center justify-center p-4">
<div className="bg-white shadow rounded-lg p-6 w-full max-w-lg">
<div className="flex justify-between items-center mb-6">
<h2 className="text-xl font-semibold text-gray-900">{t('dxt.uploadTitle')}</h2>
<button onClick={onCancel} className="text-gray-500 hover:text-gray-700">
</button>
</div>
{error && (
<div className="mb-4 bg-red-50 border-l-4 border-red-500 p-4 rounded">
<p className="text-red-700">{error}</p>
</div>
)}
{/* File Drop Zone */}
<div
className={`relative border-2 border-dashed rounded-lg p-8 text-center transition-colors ${isDragging
? 'border-blue-500 bg-blue-50'
: selectedFile
? 'border-gray-500 '
: 'border-gray-300 hover:border-gray-400'
}`}
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
onDrop={handleDrop}
>
{selectedFile ? (
<div className="space-y-2">
<svg className="mx-auto h-12 w-12 text-green-200" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<p className="text-sm text-gray-900 font-medium">{selectedFile.name}</p>
<p className="text-xs text-gray-500">{(selectedFile.size / 1024 / 1024).toFixed(2)} MB</p>
</div>
) : (
<div className="space-y-2">
<svg className="mx-auto h-12 w-12 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4" />
</svg>
<div>
<p className="text-sm text-gray-900">{t('dxt.dropFileHere')}</p>
<p className="text-xs text-gray-500">{t('dxt.orClickToSelect')}</p>
</div>
</div>
)}
<input
type="file"
accept=".dxt"
onChange={handleFileSelect}
className="absolute inset-0 w-full h-full opacity-0 cursor-pointer"
/>
</div>
<div className="mt-6 flex justify-end space-x-4">
<button
onClick={onCancel}
disabled={isUploading}
className="px-4 py-2 text-gray-700 bg-gray-200 rounded hover:bg-gray-300 disabled:opacity-50 btn-secondary"
>
{t('common.cancel')}
</button>
<button
onClick={handleUpload}
disabled={!selectedFile || isUploading}
className="px-4 py-2 text-white rounded hover:bg-blue-700 disabled:opacity-50 flex items-center btn-primary"
>
{isUploading ? (
<>
<svg className="animate-spin h-4 w-4 mr-2" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
{t('dxt.uploading')}
</>
) : (
t('dxt.upload')
)}
</button>
</div>
</div>
</div>
);
};
export default DxtUploadForm;

View File

@@ -1,9 +1,9 @@
import { useState, useEffect } from 'react'
import { useTranslation } from 'react-i18next'
import { Group, GroupFormData, Server } from '@/types'
import { Group, GroupFormData, Server, IGroupServerConfig } from '@/types'
import { useGroupData } from '@/hooks/useGroupData'
import { useServerData } from '@/hooks/useServerData'
import { ToggleGroup } from './ui/ToggleGroup'
import { ServerToolConfig } from './ServerToolConfig'
interface EditGroupFormProps {
group: Group
@@ -38,18 +38,6 @@ const EditGroupForm = ({ group, onEdit, onCancel }: EditGroupFormProps) => {
}))
}
const handleServerToggle = (serverName: string) => {
setFormData(prev => {
const isSelected = prev.servers.includes(serverName)
return {
...prev,
servers: isSelected
? prev.servers.filter(name => name !== serverName)
: [...prev.servers, serverName]
}
})
}
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
setIsSubmitting(true)
@@ -67,9 +55,9 @@ const EditGroupForm = ({ group, onEdit, onCancel }: EditGroupFormProps) => {
description: formData.description,
servers: formData.servers
})
if (!result) {
setError(t('groups.updateError'))
if (!result || !result.success) {
setError(result?.message || t('groups.updateError'))
setIsSubmitting(false)
return
}
@@ -83,64 +71,68 @@ const EditGroupForm = ({ group, onEdit, onCancel }: EditGroupFormProps) => {
return (
<div className="fixed inset-0 bg-black/50 z-50 flex items-center justify-center p-4">
<div className="bg-white rounded-lg shadow-lg max-w-md w-full">
<div className="p-6">
<div className="bg-white rounded-lg shadow-lg max-w-2xl w-full max-h-[90vh] flex flex-col">
<div className="p-6 flex-shrink-0">
<h2 className="text-xl font-semibold text-gray-800 mb-4">{t('groups.edit')}</h2>
{error && (
<div className="mb-4 p-3 bg-red-100 text-red-700 rounded">
<div className="mb-4 p-3 bg-red-100 text-red-700 rounded-md border border-gray-200">
{error}
</div>
)}
<form onSubmit={handleSubmit}>
<div className="mb-4">
<label className="block text-gray-700 text-sm font-bold mb-2" htmlFor="name">
{t('groups.name')} *
</label>
<input
type="text"
id="name"
name="name"
value={formData.name}
onChange={handleChange}
className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
placeholder={t('groups.namePlaceholder')}
required
/>
</div>
<ToggleGroup
className="mb-6"
label={t('groups.servers')}
noOptionsText={t('groups.noServerOptions')}
values={formData.servers}
options={availableServers.map(server => ({
value: server.name,
label: server.name
}))}
onChange={(servers) => setFormData(prev => ({ ...prev, servers }))}
/>
<div className="flex justify-end space-x-3">
<button
type="button"
onClick={onCancel}
className="px-4 py-2 text-gray-600 hover:text-gray-800"
disabled={isSubmitting}
>
{t('common.cancel')}
</button>
<button
type="submit"
className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 disabled:opacity-50"
disabled={isSubmitting}
>
{isSubmitting ? t('common.submitting') : t('common.save')}
</button>
</div>
</form>
</div>
<form onSubmit={handleSubmit} className="flex flex-col flex-1 min-h-0">
<div className="flex-1 overflow-y-auto px-6">
<div className="space-y-4">
<div>
<label className="block text-gray-700 text-sm font-bold mb-2" htmlFor="name">
{t('groups.name')} *
</label>
<input
type="text"
id="name"
name="name"
value={formData.name}
onChange={handleChange}
className="w-full border border-gray-300 rounded-md px-3 py-2 text-gray-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
placeholder={t('groups.namePlaceholder')}
required
/>
</div>
<div>
<label className="block text-gray-700 text-sm font-bold mb-2">
{t('groups.configureTools')}
</label>
<ServerToolConfig
servers={availableServers}
value={formData.servers as IGroupServerConfig[]}
onChange={(servers) => setFormData(prev => ({ ...prev, servers }))}
className="border border-gray-200 rounded-lg p-4 bg-gray-50"
/>
</div>
</div>
</div>
<div className="flex justify-end space-x-3 p-6 pt-4 border-t border-gray-200 flex-shrink-0">
<button
type="button"
onClick={onCancel}
className="px-4 py-2 text-gray-600 hover:text-gray-800 border border-gray-300 rounded-md hover:bg-gray-50 transition-colors"
disabled={isSubmitting}
>
{t('common.cancel')}
</button>
<button
type="submit"
className="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 disabled:opacity-50 transition-colors"
disabled={isSubmitting}
>
{isSubmitting ? t('common.submitting') : t('common.save')}
</button>
</div>
</form>
</div>
</div>
)

View File

@@ -1,65 +1,52 @@
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import { Server } from '@/types'
import { getApiUrl } from '../utils/runtime'
import ServerForm from './ServerForm'
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Server } from '@/types';
import { apiPut } from '../utils/fetchInterceptor';
import ServerForm from './ServerForm';
interface EditServerFormProps {
server: Server
onEdit: () => void
onCancel: () => void
server: Server;
onEdit: () => void;
onCancel: () => void;
}
const EditServerForm = ({ server, onEdit, onCancel }: EditServerFormProps) => {
const { t } = useTranslation()
const [error, setError] = useState<string | null>(null)
const { t } = useTranslation();
const [error, setError] = useState<string | null>(null);
const handleSubmit = async (payload: any) => {
try {
setError(null)
const token = localStorage.getItem('mcphub_token');
const response = await fetch(getApiUrl(`/servers/${server.name}`), {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'x-auth-token': token || ''
},
body: JSON.stringify(payload),
})
setError(null);
const encodedServerName = encodeURIComponent(server.name);
const result = await apiPut(`/servers/${encodedServerName}`, payload);
const result = await response.json()
if (!response.ok) {
if (!result.success) {
// Use specific error message from the response if available
if (result && result.message) {
setError(result.message)
} else if (response.status === 404) {
setError(t('server.notFound', { serverName: server.name }))
} else if (response.status === 400) {
setError(t('server.invalidData'))
setError(result.message);
} else {
setError(t('server.updateError', { serverName: server.name }))
setError(t('server.updateError', { serverName: server.name }));
}
return
return;
}
onEdit()
onEdit();
} catch (err) {
console.error('Error updating server:', err)
console.error('Error updating server:', err);
// Use friendly error messages based on error type
if (!navigator.onLine) {
setError(t('errors.network'))
} else if (err instanceof TypeError && (
err.message.includes('NetworkError') ||
err.message.includes('Failed to fetch')
)) {
setError(t('errors.serverConnection'))
setError(t('errors.network'));
} else if (
err instanceof TypeError &&
(err.message.includes('NetworkError') || err.message.includes('Failed to fetch'))
) {
setError(t('errors.serverConnection'));
} else {
setError(t('errors.serverUpdate', { serverName: server.name }))
setError(t('errors.serverUpdate', { serverName: server.name }));
}
}
}
};
return (
<div className="fixed inset-0 bg-black/50 z-50 flex items-center justify-center p-4">
@@ -71,7 +58,7 @@ const EditServerForm = ({ server, onEdit, onCancel }: EditServerFormProps) => {
formError={error}
/>
</div>
)
}
);
};
export default EditServerForm
export default EditServerForm;

View File

@@ -0,0 +1,161 @@
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useUserData } from '@/hooks/useUserData';
import { User, UserUpdateData } from '@/types';
interface EditUserFormProps {
user: User;
onEdit: () => void;
onCancel: () => void;
}
const EditUserForm = ({ user, onEdit, onCancel }: EditUserFormProps) => {
const { t } = useTranslation();
const { updateUser } = useUserData();
const [error, setError] = useState<string | null>(null);
const [isSubmitting, setIsSubmitting] = useState(false);
const [formData, setFormData] = useState({
isAdmin: user.isAdmin,
newPassword: '',
confirmPassword: '',
});
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setError(null);
// Validate passwords match if changing password
if (formData.newPassword && formData.newPassword !== formData.confirmPassword) {
setError(t('users.passwordMismatch'));
return;
}
if (formData.newPassword && formData.newPassword.length < 6) {
setError(t('users.passwordTooShort'));
return;
}
setIsSubmitting(true);
try {
const updateData: UserUpdateData = {
isAdmin: formData.isAdmin,
};
if (formData.newPassword) {
updateData.newPassword = formData.newPassword;
}
const result = await updateUser(user.username, updateData);
if (result?.success) {
onEdit();
} else {
setError(result?.message || t('users.updateError'));
}
} catch (err) {
setError(err instanceof Error ? err.message : t('users.updateError'));
} finally {
setIsSubmitting(false);
}
};
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value, type, checked } = e.target;
setFormData(prev => ({
...prev,
[name]: type === 'checkbox' ? checked : value
}));
};
return (
<div className="fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full flex items-center justify-center z-50">
<div className="bg-white p-8 rounded-lg shadow-xl max-w-md w-full mx-4">
<form onSubmit={handleSubmit}>
<h2 className="text-xl font-semibold text-gray-800 mb-4">
{t('users.edit')} - {user.username}
</h2>
{error && (
<div className="bg-red-100 border-l-4 border-red-500 text-red-700 p-3 mb-4">
<p className="text-sm">{error}</p>
</div>
)}
<div className="space-y-4">
<div className="flex items-center">
<input
type="checkbox"
id="isAdmin"
name="isAdmin"
checked={formData.isAdmin}
onChange={handleInputChange}
className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"
disabled={isSubmitting}
/>
<label htmlFor="isAdmin" className="ml-2 block text-sm text-gray-700">
{t('users.adminRole')}
</label>
</div>
<div>
<label htmlFor="newPassword" className="block text-sm font-medium text-gray-700 mb-1">
{t('users.newPassword')}
</label>
<input
type="password"
id="newPassword"
name="newPassword"
value={formData.newPassword}
onChange={handleInputChange}
placeholder={t('users.newPasswordPlaceholder')}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
disabled={isSubmitting}
minLength={6}
/>
</div>
{formData.newPassword && (
<div>
<label htmlFor="confirmPassword" className="block text-sm font-medium text-gray-700 mb-1">
{t('users.confirmPassword')}
</label>
<input
type="password"
id="confirmPassword"
name="confirmPassword"
value={formData.confirmPassword}
onChange={handleInputChange}
placeholder={t('users.confirmPasswordPlaceholder')}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
disabled={isSubmitting}
minLength={6}
/>
</div>
)}
</div>
<div className="flex justify-end space-x-3 mt-6">
<button
type="button"
onClick={onCancel}
className="px-4 py-2 text-gray-700 bg-gray-200 rounded hover:bg-gray-300 transition-colors duration-200"
disabled={isSubmitting}
>
{t('common.cancel')}
</button>
<button
type="submit"
className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 transition-colors duration-200 disabled:opacity-50 disabled:cursor-not-allowed"
disabled={isSubmitting}
>
{isSubmitting ? t('common.updating') : t('users.update')}
</button>
</div>
</form>
</div>
</div>
);
};
export default EditUserForm;

View File

@@ -1,9 +1,10 @@
import { useState } from 'react'
import { useState, useRef, useEffect } from 'react'
import { useTranslation } from 'react-i18next'
import { Group, Server } from '@/types'
import { Edit, Trash, Copy, Check } from '@/components/icons/LucideIcons'
import { Group, Server, IGroupServerConfig } from '@/types'
import { Edit, Trash, Copy, Check, Link, FileCode, DropdownIcon, Wrench } from '@/components/icons/LucideIcons'
import DeleteDialog from '@/components/ui/DeleteDialog'
import { useToast } from '@/contexts/ToastContext'
import { useSettingsData } from '@/hooks/useSettingsData'
interface GroupCardProps {
group: Group
@@ -20,8 +21,26 @@ const GroupCard = ({
}: GroupCardProps) => {
const { t } = useTranslation()
const { showToast } = useToast()
const { installConfig } = useSettingsData()
const [showDeleteDialog, setShowDeleteDialog] = useState(false)
const [copied, setCopied] = useState(false)
const [showCopyDropdown, setShowCopyDropdown] = useState(false)
const [expandedServer, setExpandedServer] = useState<string | null>(null)
const dropdownRef = useRef<HTMLDivElement>(null)
// Close dropdown when clicking outside
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
setShowCopyDropdown(false)
}
}
document.addEventListener('mousedown', handleClickOutside)
return () => {
document.removeEventListener('mousedown', handleClickOutside)
}
}, [])
const handleEdit = () => {
onEdit(group)
@@ -36,16 +55,18 @@ const GroupCard = ({
setShowDeleteDialog(false)
}
const copyToClipboard = () => {
const copyToClipboard = (text: string) => {
if (navigator.clipboard && window.isSecureContext) {
navigator.clipboard.writeText(group.id).then(() => {
navigator.clipboard.writeText(text).then(() => {
setCopied(true)
setShowCopyDropdown(false)
showToast(t('common.copySuccess'), 'success')
setTimeout(() => setCopied(false), 2000)
})
} else {
// Fallback for HTTP or unsupported clipboard API
const textArea = document.createElement('textarea')
textArea.value = group.id
textArea.value = text
// Avoid scrolling to bottom
textArea.style.position = 'fixed'
textArea.style.left = '-9999px'
@@ -55,6 +76,8 @@ const GroupCard = ({
try {
document.execCommand('copy')
setCopied(true)
setShowCopyDropdown(false)
showToast(t('common.copySuccess'), 'success')
setTimeout(() => setCopied(false), 2000)
} catch (err) {
showToast(t('common.copyFailed') || 'Copy failed', 'error')
@@ -64,24 +87,92 @@ const GroupCard = ({
}
}
const handleCopyId = () => {
copyToClipboard(group.id)
}
const handleCopyUrl = () => {
copyToClipboard(`${installConfig.baseUrl}/mcp/${group.id}`)
}
const handleCopyJson = () => {
const jsonConfig = {
mcpServers: {
mcphub: {
url: `${installConfig.baseUrl}/mcp/${group.id}`,
headers: {
Authorization: "Bearer <your-access-token>"
}
}
}
}
copyToClipboard(JSON.stringify(jsonConfig, null, 2))
}
// Helper function to normalize group servers to get server names
const getServerNames = (servers: string[] | IGroupServerConfig[]): string[] => {
return servers.map(server => typeof server === 'string' ? server : server.name);
};
// Helper function to get server configuration
const getServerConfig = (serverName: string): IGroupServerConfig | undefined => {
const server = group.servers.find(s =>
typeof s === 'string' ? s === serverName : s.name === serverName
);
if (typeof server === 'string') {
return { name: server, tools: 'all' };
}
return server;
};
// Get servers that belong to this group
const groupServers = servers.filter(server => group.servers.includes(server.name))
const serverNames = getServerNames(group.servers);
const groupServers = servers.filter(server => serverNames.includes(server.name));
return (
<div className="bg-white shadow rounded-lg p-6">
<div className="bg-white shadow rounded-lg p-6 ">
<div className="flex justify-between items-center mb-4">
<div>
<div className="flex items-center">
<h2 className="text-xl font-semibold text-gray-800">{group.name}</h2>
<div className="flex items-center ml-3">
<span className="text-xs text-gray-500 mr-1">{group.id}</span>
<button
onClick={copyToClipboard}
className="p-1 text-gray-400 hover:text-gray-600 transition-colors"
title={t('common.copy')}
>
{copied ? <Check size={14} className="text-green-500" /> : <Copy size={14} />}
</button>
<div className="relative" ref={dropdownRef}>
<button
onClick={() => setShowCopyDropdown(!showCopyDropdown)}
className="p-1 text-gray-400 hover:text-gray-600 transition-colors flex items-center"
title={t('common.copy')}
>
{copied ? <Check size={14} className="text-green-500" /> : <Copy size={14} />}
<DropdownIcon size={12} className="ml-1" />
</button>
{showCopyDropdown && (
<div className="absolute top-full left-0 mt-1 bg-white shadow-lg rounded-md border border-gray-200 py-1 z-10 min-w-[140px]">
<button
onClick={handleCopyId}
className="w-full px-3 py-2 text-left text-sm text-gray-700 hover:bg-gray-100 flex items-center"
>
<Copy size={12} className="mr-2" />
{t('common.copyId')}
</button>
<button
onClick={handleCopyUrl}
className="w-full px-3 py-2 text-left text-sm text-gray-700 hover:bg-gray-100 flex items-center"
>
<Link size={12} className="mr-2" />
{t('common.copyUrl')}
</button>
<button
onClick={handleCopyJson}
className="w-full px-3 py-2 text-left text-sm text-gray-700 hover:bg-gray-100 flex items-center"
>
<FileCode size={12} className="mr-2" />
{t('common.copyJson')}
</button>
</div>
)}
</div>
</div>
</div>
{group.description && (
@@ -89,7 +180,7 @@ const GroupCard = ({
)}
</div>
<div className="flex items-center space-x-3">
<div className="bg-blue-50 text-blue-700 px-3 py-1 rounded-full text-sm">
<div className="bg-blue-50 text-blue-700 px-3 py-1 rounded-full text-sm btn-secondary">
{t('groups.serverCount', { count: group.servers.length })}
</div>
<button
@@ -113,18 +204,68 @@ const GroupCard = ({
{groupServers.length === 0 ? (
<p className="text-gray-500 italic">{t('groups.noServers')}</p>
) : (
<div className="flex flex-wrap gap-2 mt-2">
{groupServers.map(server => (
<div
key={server.name}
className="inline-flex items-center px-3 py-1 bg-gray-50 rounded"
>
<span className="font-medium text-gray-700 text-sm">{server.name}</span>
<span className={`ml-2 inline-block h-2 w-2 rounded-full ${server.status === 'connected' ? 'bg-green-500' :
server.status === 'connecting' ? 'bg-yellow-500' : 'bg-red-500'
}`}></span>
</div>
))}
<div className="flex flex-wrap gap-2">
{groupServers.map(server => {
const serverConfig = getServerConfig(server.name);
const hasToolRestrictions = serverConfig && serverConfig.tools !== 'all' && Array.isArray(serverConfig.tools);
const toolCount = hasToolRestrictions && Array.isArray(serverConfig?.tools)
? serverConfig.tools.length
: (server.tools?.length || 0); // Show total tool count when all tools are selected
const isExpanded = expandedServer === server.name;
// Get tools list for display
const getToolsList = () => {
if (hasToolRestrictions && Array.isArray(serverConfig?.tools)) {
return serverConfig.tools;
} else if (server.tools && server.tools.length > 0) {
return server.tools.map(tool => tool.name);
}
return [];
};
const handleServerClick = () => {
setExpandedServer(isExpanded ? null : server.name);
};
return (
<div key={server.name} className="relative">
<div
className="flex items-center space-x-2 bg-gray-50 rounded-lg px-3 py-2 cursor-pointer hover:bg-gray-100 transition-colors"
onClick={handleServerClick}
>
<span className="font-medium text-gray-700 text-sm">{server.name}</span>
<span className={`inline-block h-2 w-2 rounded-full ${server.status === 'connected' ? 'bg-green-500' :
server.status === 'connecting' ? 'bg-yellow-500' : 'bg-red-500'
}`}></span>
{toolCount > 0 && (
<span className="text-xs text-blue-600 bg-blue-100 px-2 py-0.5 rounded flex items-center gap-1">
<Wrench size={12} />
{toolCount}
</span>
)}
</div>
{isExpanded && (
<div className="absolute top-full left-0 mt-1 bg-white shadow-lg rounded-md border border-gray-200 p-3 z-10 min-w-[300px] max-w-[400px]">
<div className="text-gray-600 text-xs mb-2">
{hasToolRestrictions ? t('groups.selectedTools') : t('groups.allTools')}:
</div>
<div className="flex flex-wrap gap-1">
{getToolsList().map((toolName, index) => (
<span
key={index}
className="inline-block bg-gray-100 text-gray-700 px-2 py-1 rounded text-xs"
>
{toolName}
</span>
))}
</div>
</div>
)}
</div>
);
})}
</div>
)}
</div>

View File

@@ -0,0 +1,311 @@
import React, { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { apiPost } from '@/utils/fetchInterceptor';
interface JSONImportFormProps {
onSuccess: () => void;
onCancel: () => void;
}
interface McpServerConfig {
command?: string;
args?: string[];
env?: Record<string, string>;
type?: string;
url?: string;
headers?: Record<string, string>;
}
interface ImportJsonFormat {
mcpServers: Record<string, McpServerConfig>;
}
const JSONImportForm: React.FC<JSONImportFormProps> = ({ onSuccess, onCancel }) => {
const { t } = useTranslation();
const [jsonInput, setJsonInput] = useState('');
const [error, setError] = useState<string | null>(null);
const [isImporting, setIsImporting] = useState(false);
const [previewServers, setPreviewServers] = useState<Array<{ name: string; config: any }> | null>(
null,
);
const examplePlaceholder = `STDIO example:
{
"mcpServers": {
"stdio-server-example": {
"command": "npx",
"args": ["-y", "mcp-server-example"]
}
}
}
SSE example:
{
"mcpServers": {
"sse-server-example": {
"type": "sse",
"url": "http://localhost:3000"
}
}
}
HTTP example:
{
"mcpServers": {
"http-server-example": {
"type": "streamable-http",
"url": "http://localhost:3001",
"headers": {
"Content-Type": "application/json",
"Authorization": "Bearer your-token"
}
}
}
}`;
const parseAndValidateJson = (input: string): ImportJsonFormat | null => {
try {
const parsed = JSON.parse(input.trim());
// Validate structure
if (!parsed.mcpServers || typeof parsed.mcpServers !== 'object') {
setError(t('jsonImport.invalidFormat'));
return null;
}
return parsed as ImportJsonFormat;
} catch (e) {
setError(t('jsonImport.parseError'));
return null;
}
};
const handlePreview = () => {
setError(null);
const parsed = parseAndValidateJson(jsonInput);
if (!parsed) return;
const servers = Object.entries(parsed.mcpServers).map(([name, config]) => {
// Normalize config to MCPHub format
const normalizedConfig: any = {};
if (config.type === 'sse' || config.type === 'streamable-http') {
normalizedConfig.type = config.type;
normalizedConfig.url = config.url;
if (config.headers) {
normalizedConfig.headers = config.headers;
}
} else {
// Default to stdio
normalizedConfig.type = 'stdio';
normalizedConfig.command = config.command;
normalizedConfig.args = config.args || [];
if (config.env) {
normalizedConfig.env = config.env;
}
}
return { name, config: normalizedConfig };
});
setPreviewServers(servers);
};
const handleImport = async () => {
if (!previewServers) return;
setIsImporting(true);
setError(null);
try {
let successCount = 0;
const errors: string[] = [];
for (const server of previewServers) {
try {
const result = await apiPost('/servers', {
name: server.name,
config: server.config,
});
if (result.success) {
successCount++;
} else {
errors.push(`${server.name}: ${result.message || t('jsonImport.addFailed')}`);
}
} catch (err) {
errors.push(
`${server.name}: ${err instanceof Error ? err.message : t('jsonImport.addFailed')}`,
);
}
}
if (errors.length > 0) {
setError(
t('jsonImport.partialSuccess', { count: successCount, total: previewServers.length }) +
'\n' +
errors.join('\n'),
);
}
if (successCount > 0) {
onSuccess();
}
} catch (err) {
console.error('Import error:', err);
setError(t('jsonImport.importFailed'));
} finally {
setIsImporting(false);
}
};
return (
<div className="fixed inset-0 bg-black/50 z-50 flex items-center justify-center p-4">
<div className="bg-white shadow rounded-lg p-6 w-full max-w-4xl max-h-[90vh] overflow-y-auto">
<div className="flex justify-between items-center mb-6">
<h2 className="text-xl font-semibold text-gray-900">{t('jsonImport.title')}</h2>
<button onClick={onCancel} className="text-gray-500 hover:text-gray-700">
</button>
</div>
{error && (
<div className="mb-4 bg-red-50 border-l-4 border-red-500 p-4 rounded">
<p className="text-red-700 whitespace-pre-wrap">{error}</p>
</div>
)}
{!previewServers ? (
<div>
<div className="mb-4">
<label className="block text-sm font-medium text-gray-700 mb-2">
{t('jsonImport.inputLabel')}
</label>
<textarea
value={jsonInput}
onChange={(e) => setJsonInput(e.target.value)}
className="w-full h-96 px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 font-mono text-sm"
placeholder={examplePlaceholder}
/>
<p className="text-xs text-gray-500 mt-2">{t('jsonImport.inputHelp')}</p>
</div>
<div className="flex justify-end space-x-4">
<button
onClick={onCancel}
className="px-4 py-2 text-gray-700 bg-gray-200 rounded hover:bg-gray-300 btn-secondary"
>
{t('common.cancel')}
</button>
<button
onClick={handlePreview}
disabled={!jsonInput.trim()}
className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 disabled:opacity-50 btn-primary"
>
{t('jsonImport.preview')}
</button>
</div>
</div>
) : (
<div>
<div className="mb-4">
<h3 className="text-lg font-medium text-gray-900 mb-3">
{t('jsonImport.previewTitle')}
</h3>
<div className="space-y-3">
{previewServers.map((server, index) => (
<div key={index} className="bg-gray-50 p-4 rounded-lg border border-gray-200">
<div className="flex items-start justify-between">
<div className="flex-1">
<h4 className="font-medium text-gray-900">{server.name}</h4>
<div className="mt-2 space-y-1 text-sm text-gray-600">
<div>
<strong>{t('server.type')}:</strong> {server.config.type || 'stdio'}
</div>
{server.config.command && (
<div>
<strong>{t('server.command')}:</strong> {server.config.command}
</div>
)}
{server.config.args && server.config.args.length > 0 && (
<div>
<strong>{t('server.arguments')}:</strong>{' '}
{server.config.args.join(' ')}
</div>
)}
{server.config.url && (
<div>
<strong>{t('server.url')}:</strong> {server.config.url}
</div>
)}
{server.config.env && Object.keys(server.config.env).length > 0 && (
<div>
<strong>{t('server.envVars')}:</strong>{' '}
{Object.keys(server.config.env).join(', ')}
</div>
)}
{server.config.headers &&
Object.keys(server.config.headers).length > 0 && (
<div>
<strong>{t('server.headers')}:</strong>{' '}
{Object.keys(server.config.headers).join(', ')}
</div>
)}
</div>
</div>
</div>
</div>
))}
</div>
</div>
<div className="flex justify-end space-x-4">
<button
onClick={() => setPreviewServers(null)}
disabled={isImporting}
className="px-4 py-2 text-gray-700 bg-gray-200 rounded hover:bg-gray-300 disabled:opacity-50 btn-secondary"
>
{t('common.back')}
</button>
<button
onClick={handleImport}
disabled={isImporting}
className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 disabled:opacity-50 flex items-center btn-primary"
>
{isImporting ? (
<>
<svg
className="animate-spin h-4 w-4 mr-2"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<circle
className="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"
></circle>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path>
</svg>
{t('jsonImport.importing')}
</>
) : (
t('jsonImport.import')
)}
</button>
</div>
</div>
)}
</div>
</div>
);
};
export default JSONImportForm;

View File

@@ -48,25 +48,26 @@ const LogViewer: React.FC<LogViewerProps> = ({ logs, isLoading = false, error =
// Get badge color based on log type
const getLogTypeColor = (type: string) => {
switch (type) {
case 'error': return 'bg-red-400';
case 'warn': return 'bg-yellow-400';
case 'debug': return 'bg-purple-400';
default: return 'bg-blue-400';
case 'error': return 'bg-red-400/80 text-white';
case 'warn': return 'bg-yellow-400/80 text-gray-900';
case 'debug': return 'bg-purple-400/80 text-white';
case 'info': return 'bg-blue-400/80 text-white';
default: return 'bg-blue-400/80 text-white';
}
};
// Get badge color based on log source
const getSourceColor = (source: string) => {
switch (source) {
case 'main': return 'bg-green-400';
case 'child': return 'bg-orange-400';
default: return 'bg-gray-400';
case 'main': return 'bg-green-400/80 text-white';
case 'child': return 'bg-orange-400/80 text-white';
default: return 'bg-gray-400/80 text-white';
}
};
return (
<div className="flex flex-col h-full">
<div className="bg-card p-3 rounded-t-md border-b flex flex-wrap items-center justify-between gap-2">
<div className="bg-card p-3 rounded-t-md flex flex-wrap items-center justify-between gap-2">
<div className="flex flex-wrap items-center gap-2">
<span className="font-semibold text-sm">{t('logs.filters')}:</span>
@@ -74,14 +75,14 @@ const LogViewer: React.FC<LogViewerProps> = ({ logs, isLoading = false, error =
<input
type="text"
placeholder={t('logs.search')}
className="px-2 py-1 text-sm border rounded"
className="shadow appearance-none border border-gray-200 rounded py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline form-input"
value={filter}
onChange={(e) => setFilter(e.target.value)}
/>
{/* Log type filters */}
<div className="flex gap-1 items-center">
{(['info', 'error', 'warn', 'debug'] as const).map(type => (
{(['debug', 'info', 'error', 'warn'] as const).map(type => (
<Badge
key={type}
variant={typeFilter.includes(type) ? 'default' : 'outline'}
@@ -134,6 +135,7 @@ const LogViewer: React.FC<LogViewerProps> = ({ logs, isLoading = false, error =
variant="outline"
size="sm"
onClick={onClear}
className='btn-secondary'
disabled={isLoading || logs.length === 0}
>
{t('logs.clearLogs')}
@@ -164,7 +166,7 @@ const LogViewer: React.FC<LogViewerProps> = ({ logs, isLoading = false, error =
filteredLogs.map((log, index) => (
<div
key={`${log.timestamp}-${index}`}
className={`py-1 border-b border-gray-100 dark:border-gray-800 ${log.type === 'error' ? 'text-red-500' :
className={`py-1 ${log.type === 'error' ? 'text-red-500' :
log.type === 'warn' ? 'text-yellow-500' : ''
}`}
>

View File

@@ -0,0 +1,92 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import { useNavigate } from 'react-router-dom';
const MCPRouterApiKeyError: React.FC = () => {
const { t } = useTranslation();
const navigate = useNavigate();
const handleConfigureSettings = () => {
navigate('/settings');
};
const handleGetApiKey = () => {
window.open('https://mcprouter.co', '_blank', 'noopener,noreferrer');
};
return (
<div className="bg-amber-50 border border-amber-200 rounded-lg p-6 mb-6">
<div className="flex items-start">
<div className="flex-shrink-0">
<svg
className="h-5 w-5 text-amber-400"
viewBox="0 0 20 20"
fill="currentColor"
>
<path
fillRule="evenodd"
d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z"
clipRule="evenodd"
/>
</svg>
</div>
<div className="ml-3 flex-1">
<h3 className="text-sm font-medium text-amber-800">
{t('cloud.apiKeyNotConfigured')}
</h3>
<div className="mt-2 text-sm text-amber-700">
<p>{t('cloud.apiKeyNotConfiguredDescription')}</p>
</div>
<div className="mt-4 flex flex-wrap gap-2">
<button
onClick={handleGetApiKey}
className="inline-flex items-center px-3 py-2 text-sm font-medium text-white bg-blue-600 rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-colors duration-200"
>
<svg
className="w-4 h-4 mr-1.5"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"
/>
</svg>
{t('cloud.getApiKey')}
</button>
<button
onClick={handleConfigureSettings}
className="inline-flex items-center px-3 py-2 text-sm font-medium text-amber-800 bg-amber-100 border border-amber-300 rounded-md hover:bg-amber-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-amber-500 transition-colors duration-200"
>
<svg
className="w-4 h-4 mr-1.5"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"
/>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
/>
</svg>
{t('cloud.configureInSettings')}
</button>
</div>
</div>
</div>
</div>
);
};
export default MCPRouterApiKeyError;

View File

@@ -10,36 +10,46 @@ interface MarketServerCardProps {
const MarketServerCard: React.FC<MarketServerCardProps> = ({ server, onClick }) => {
const { t } = useTranslation();
// Get initials for avatar
const getAuthorInitials = (name: string) => {
return name
.split(' ')
.map(word => word.charAt(0))
.join('')
.toUpperCase()
.slice(0, 2);
};
// Intelligently calculate how many tags to display to ensure they fit in a single line
const getTagsToDisplay = () => {
if (!server.tags || server.tags.length === 0) {
return { tagsToShow: [], hasMore: false, moreCount: 0 };
}
// Estimate available width in the card (in characters)
const estimatedAvailableWidth = 28; // Estimated number of characters that can fit in one line
// Calculate the character space needed for tags and plus sign (including # and spacing)
const calculateTagWidth = (tag: string) => tag.length + 3; // +3 for # and spacing
// Loop to determine the maximum number of tags that can be displayed
let totalWidth = 0;
let i = 0;
// First, sort tags by length to prioritize displaying shorter tags
const sortedTags = [...server.tags].sort((a, b) => a.length - b.length);
// Calculate how many tags can fit
for (i = 0; i < sortedTags.length; i++) {
const tagWidth = calculateTagWidth(sortedTags[i]);
// If this tag would make the total width exceed available width, stop adding
if (totalWidth + tagWidth > estimatedAvailableWidth) {
break;
}
totalWidth += tagWidth;
// If this is the last tag but there's still space, no need to show "more"
if (i === sortedTags.length - 1) {
return {
@@ -49,16 +59,16 @@ const MarketServerCard: React.FC<MarketServerCardProps> = ({ server, onClick })
};
}
}
// If there's not enough space to display any tags, show at least one
if (i === 0 && sortedTags.length > 0) {
i = 1;
}
// Calculate space needed for the "more" tag
const moreCount = sortedTags.length - i;
const moreTagWidth = 3 + String(moreCount).length + t('market.moreTags').length;
// If there's enough remaining space to display the "more" tag
if (totalWidth + moreTagWidth <= estimatedAvailableWidth || i < 1) {
return {
@@ -67,7 +77,7 @@ const MarketServerCard: React.FC<MarketServerCardProps> = ({ server, onClick })
moreCount
};
}
// If there's not enough space for even the "more" tag, reduce one tag to make room
return {
tagsToShow: sortedTags.slice(0, Math.max(1, i - 1)),
@@ -79,71 +89,90 @@ const MarketServerCard: React.FC<MarketServerCardProps> = ({ server, onClick })
const { tagsToShow, hasMore, moreCount } = getTagsToDisplay();
return (
<div
className="bg-white rounded-lg shadow-md p-5 hover:shadow-lg transition-shadow cursor-pointer flex flex-col h-full"
<div
className="bg-white border border-gray-200 rounded-xl p-4 hover:shadow-lg hover:border-blue-400 hover:-translate-y-1 transition-all duration-300 cursor-pointer group relative overflow-hidden h-full flex flex-col"
onClick={() => onClick(server)}
>
<div className="flex justify-between items-start mb-3">
<h3 className="text-lg font-semibold text-gray-900 line-clamp-1 mr-2">{server.display_name}</h3>
{server.is_official && (
<span className="bg-blue-100 text-blue-800 text-xs font-medium px-2.5 py-0.5 rounded flex-shrink-0">
{t('market.official')}
</span>
)}
</div>
<p className="text-gray-600 text-sm mb-4 line-clamp-2 min-h-[40px]">{server.description}</p>
{/* Categories */}
<div className="flex flex-wrap gap-1 mb-2 min-h-[28px]">
{server.categories?.length > 0 ? (
server.categories.map((category, index) => (
<span
key={index}
className="bg-gray-100 text-gray-800 text-xs px-2 py-1 rounded whitespace-nowrap"
>
{category}
</span>
))
) : (
<span className="text-xs text-gray-400 py-1">-</span>
)}
</div>
{/* Tags */}
<div className="relative mb-3 min-h-[28px] overflow-x-auto">
{server.tags?.length > 0 ? (
<div className="flex gap-1 items-center whitespace-nowrap">
{tagsToShow.map((tag, index) => (
<span
key={index}
className="bg-green-50 text-green-700 text-xs px-2 py-1 rounded flex-shrink-0"
>
#{tag}
</span>
))}
{hasMore && (
<span className="bg-gray-100 text-gray-600 text-xs px-1.5 py-1 rounded flex-shrink-0">
+{moreCount} {t('market.moreTags')}
{/* Background gradient overlay on hover */}
<div className="absolute inset-0 bg-gradient-to-br from-blue-50/0 to-purple-50/0 group-hover:from-blue-50/30 group-hover:to-purple-50/30 transition-all duration-300 pointer-events-none" />
{/* Server Header */}
<div className="relative z-10 flex-1 flex flex-col">
<div className="flex items-start justify-between mb-2">
<div className="flex-1">
<h3 className="text-lg font-bold text-gray-900 group-hover:text-blue-600 transition-colors duration-200 mb-1 line-clamp-1 mr-2">
{server.display_name}
</h3>
{/* Author Section */}
<div className="flex items-center space-x-2 mb-1">
<div className="w-6 h-6 bg-gradient-to-br from-blue-500 to-purple-600 rounded-full flex items-center justify-center text-white text-xs font-semibold">
{getAuthorInitials(server.author?.name || t('market.unknown'))}
</div>
<div>
<p className="text-xs font-medium text-gray-700">{server.author?.name || t('market.unknown')}</p>
</div>
</div>
</div>
{/* Server Type Badge */}
<div className="flex flex-col items-end space-y-2">
{server.is_official && (
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800">
{t('market.official')}
</span>
)}
</div>
) : (
<span className="text-xs text-gray-400 py-1">-</span>
)}
</div>
<div className="flex justify-between items-center mt-auto pt-2 text-xs text-gray-500 border-t border-gray-100">
<div className="overflow-hidden">
<span className="whitespace-nowrap">{t('market.by')} </span>
<span className="font-medium whitespace-nowrap overflow-hidden text-ellipsis max-w-[120px] inline-block align-bottom">
{server.author?.name || t('market.unknown')}
</span>
</div>
<div className="flex items-center flex-shrink-0">
<svg className="h-4 w-4 mr-1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
<path fillRule="evenodd" d="M11.3 1.046A1 1 0 0112 2v5h4a1 1 0 01.82 1.573l-7 10A1 1 0 018 18v-5H4a1 1 0 01-.82-1.573l7-10a1 1 0 011.12-.38z" clipRule="evenodd" />
</svg>
<span>{server.tools?.length || 0} {t('market.tools')}</span>
{/* Description */}
<div className="mb-2 flex-1">
<p className="text-gray-600 text-sm leading-relaxed line-clamp-2 min-h-[36px]">
{server.description}
</p>
</div>
{/* Categories */}
<div className="mb-2">
<div className="flex flex-wrap gap-1 min-h-[24px]">
{server.categories?.length > 0 ? (
server.categories.map((category, index) => (
<span
key={index}
className="bg-gray-100 text-gray-800 text-xs px-2 py-1 rounded whitespace-nowrap"
>
{category}
</span>
))
) : (
<span className="text-xs text-gray-400 py-1">-</span>
)}
</div>
</div>
{/* Tags */}
<div className="mb-2">
<div className="relative min-h-[24px] overflow-x-auto">
{server.tags?.length > 0 ? (
<div className="flex gap-1 items-center whitespace-nowrap">
{tagsToShow.map((tag, index) => (
<span
key={index}
className="bg-green-50 text-green-700 text-xs px-2 py-1 rounded flex-shrink-0"
>
#{tag}
</span>
))}
{hasMore && (
<span className="bg-gray-100 text-gray-600 text-xs px-1.5 py-1 rounded flex-shrink-0">
+{moreCount} {t('market.moreTags')}
</span>
)}
</div>
) : (
<span className="text-xs text-gray-400 py-1">-</span>
)}
</div>
</div>
</div>
</div>

View File

@@ -2,11 +2,14 @@ import React, { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { MarketServer, MarketServerInstallation } from '@/types';
import ServerForm from './ServerForm';
import { detectVariables } from '../utils/variableDetection';
import { ServerConfig } from '@/types';
interface MarketServerDetailProps {
server: MarketServer;
onBack: () => void;
onInstall: (server: MarketServer) => void;
onInstall: (server: MarketServer, config: ServerConfig) => void;
installing?: boolean;
isInstalled?: boolean;
}
@@ -21,6 +24,9 @@ const MarketServerDetail: React.FC<MarketServerDetailProps> = ({
const { t } = useTranslation();
const [modalVisible, setModalVisible] = useState(false);
const [error, setError] = useState<string | null>(null);
const [confirmationVisible, setConfirmationVisible] = useState(false);
const [pendingPayload, setPendingPayload] = useState<any>(null);
const [detectedVariables, setDetectedVariables] = useState<string[]>([]);
// Helper function to determine button state
const getButtonProps = () => {
@@ -38,7 +44,7 @@ const MarketServerDetail: React.FC<MarketServerDetailProps> = ({
};
} else {
return {
className: "bg-blue-600 hover:bg-blue-700 px-4 py-2 rounded text-sm font-medium text-white",
className: "bg-blue-600 hover:bg-blue-700 px-4 py-2 rounded text-sm font-medium text-white btn-primary",
disabled: false,
text: t('market.install')
};
@@ -48,6 +54,27 @@ const MarketServerDetail: React.FC<MarketServerDetailProps> = ({
const toggleModal = () => {
setModalVisible(!modalVisible);
setError(null); // Clear any previous errors when toggling modal
setConfirmationVisible(false);
setPendingPayload(null);
};
const handleConfirmInstall = async () => {
if (pendingPayload) {
await proceedWithInstall(pendingPayload);
setConfirmationVisible(false);
setPendingPayload(null);
}
};
const proceedWithInstall = async (payload: any) => {
try {
setError(null);
onInstall(server, payload.config);
setModalVisible(false);
} catch (err) {
console.error('Error installing server:', err);
setError(t('errors.serverInstall'));
}
};
const handleInstall = () => {
@@ -70,24 +97,32 @@ const MarketServerDetail: React.FC<MarketServerDetailProps> = ({
} else if (server.installations.default) {
return server.installations.default;
}
// If none of the preferred types are available, get the first available installation type
const installTypes = Object.keys(server.installations);
if (installTypes.length > 0) {
return server.installations[installTypes[0]];
}
return undefined;
};
const handleSubmit = async (payload: any) => {
try {
setError(null);
// Pass the server object to the parent component for installation
onInstall(server);
setModalVisible(false);
// Check for variables in the payload
const variables = detectVariables(payload);
if (variables.length > 0) {
// Show confirmation dialog
setDetectedVariables(variables);
setPendingPayload(payload);
setConfirmationVisible(true);
} else {
// Install directly if no variables found
await proceedWithInstall(payload);
}
} catch (err) {
console.error('Error installing server:', err);
console.error('Error processing server installation:', err);
setError(t('errors.serverInstall'));
}
};
@@ -112,15 +147,15 @@ const MarketServerDetail: React.FC<MarketServerDetailProps> = ({
<div className="flex justify-between items-start mb-4">
<div>
<h2 className="text-2xl font-bold text-gray-900 flex items-center flex-wrap">
{server.display_name}
{server.display_name}
<span className="text-sm font-normal text-gray-500 ml-2">({server.name})</span>
<span className="text-sm font-normal text-gray-600 ml-4">
{t('market.author')}: {server.author.name} {t('market.license')}: {server.license}
{t('market.author')}: {server.author.name} {t('market.license')}: {server.license}
<a
href={server.repository.url}
target="_blank"
rel="noopener noreferrer"
className="text-blue-600 hover:underline ml-1"
className="text-blue-500 hover:underline ml-1"
>
{t('market.repository')}
</a>
@@ -130,7 +165,7 @@ const MarketServerDetail: React.FC<MarketServerDetailProps> = ({
<div className="flex items-center">
{server.is_official && (
<span className="bg-blue-100 text-blue-800 text-sm font-medium px-4 py-2 rounded mr-2 flex items-center">
<span className="bg-blue-100 text-blue-800 text-sm font-normal px-4 py-2 rounded mr-2 flex items-center label-primary">
{t('market.official')}
</span>
)}
@@ -167,7 +202,7 @@ const MarketServerDetail: React.FC<MarketServerDetailProps> = ({
<h3 className="text-lg font-semibold mb-3">{t('market.arguments')}</h3>
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50">
<thead className="bg-gray-50 border-b border-gray-200">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider whitespace-nowrap">
{t('market.argumentName')}
@@ -196,7 +231,7 @@ const MarketServerDetail: React.FC<MarketServerDetailProps> = ({
{arg.required ? (
<span className="text-green-600"></span>
) : (
<span className="text-red-600"></span>
<span className="text-gray-600"></span>
)}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
@@ -226,7 +261,7 @@ const MarketServerDetail: React.FC<MarketServerDetailProps> = ({
element.classList.toggle('hidden');
}
}}
className="text-sm text-blue-600 hover:underline focus:outline-none ml-2"
className="text-sm text-blue-500 font-normal hover:underline focus:outline-none ml-2"
>
{t('market.viewSchema')}
</button>
@@ -279,19 +314,73 @@ const MarketServerDetail: React.FC<MarketServerDetailProps> = ({
initialData={{
name: server.name,
status: 'disconnected',
config: preferredInstallation
config: preferredInstallation
? {
command: preferredInstallation.command || '',
args: preferredInstallation.args || [],
env: preferredInstallation.env || {}
}
command: preferredInstallation.command || '',
args: preferredInstallation.args || [],
env: preferredInstallation.env || {}
}
: undefined
}}
/>
</div>
)}
{confirmationVisible && (
<div className="fixed inset-0 bg-black/50 z-[60] flex items-center justify-center p-4">
<div className="bg-white rounded-lg p-6 w-full max-w-md">
<h3 className="text-lg font-semibold text-gray-900 mb-4">
{t('server.confirmVariables')}
</h3>
<p className="text-gray-600 mb-4">
{t('server.variablesDetected')}
</p>
<div className="bg-yellow-50 border border-yellow-200 rounded p-3 mb-4">
<div className="flex items-start">
<div className="flex-shrink-0">
<svg className="h-5 w-5 text-yellow-400" viewBox="0 0 20 20" fill="currentColor">
<path fillRule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clipRule="evenodd" />
</svg>
</div>
<div className="ml-3">
<h4 className="text-sm font-medium text-yellow-800">
{t('server.detectedVariables')}:
</h4>
<ul className="mt-1 text-sm text-yellow-700">
{detectedVariables.map((variable, index) => (
<li key={index} className="font-mono">
${`{${variable}}`}
</li>
))}
</ul>
</div>
</div>
</div>
<p className="text-gray-600 text-sm mb-6">
{t('market.confirmVariablesMessage')}
</p>
<div className="flex justify-end space-x-3">
<button
onClick={() => {
setConfirmationVisible(false)
setPendingPayload(null)
}}
className="px-4 py-2 text-gray-600 border border-gray-300 rounded hover:bg-gray-50 btn-secondary"
>
{t('common.cancel')}
</button>
<button
onClick={handleConfirmInstall}
className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 btn-primary"
>
{t('market.confirmAndInstall')}
</button>
</div>
</div>
</div>
)}
</div>
);
};
export default MarketServerDetail;
export default MarketServerDetail;

View File

@@ -0,0 +1,95 @@
import React from 'react';
import { useAuth } from '../contexts/AuthContext';
interface PermissionCheckerProps {
permissions: string | string[];
fallback?: React.ReactNode;
children: React.ReactNode;
}
/**
* Permission checker component for conditional rendering
* @param permissions Required permissions, supports single permission string or permission array
* @param fallback Content to show when permission is denied, defaults to null
* @param children Content to show when permission is granted
*/
export const PermissionChecker: React.FC<PermissionCheckerProps> = ({
permissions,
fallback = null,
children,
}) => {
const hasPermission = usePermissionCheck(permissions);
return hasPermission ? <>{children}</> : <>{fallback}</>;
};
/**
* Permission check hook
* @param requiredPermissions Permissions to check
* @returns Whether user has permission
*/
export const usePermissionCheck = (requiredPermissions: string | string[]): boolean => {
const { auth } = useAuth();
if (!auth.isAuthenticated || !auth.user) {
return false;
}
const userPermissions = auth.user.permissions || [];
if (requiredPermissions === 'x' && !userPermissions.includes('x')) {
return false;
}
// If user has '*' permission, they have all permissions
if (userPermissions.includes('*')) {
return true;
}
// If user is admin, they have all permissions by default
if (auth.user.isAdmin) {
return true;
}
// Normalize required permissions to array
const permissionsToCheck = Array.isArray(requiredPermissions)
? requiredPermissions
: [requiredPermissions];
// Check if user has any of the required permissions
return permissionsToCheck.some(permission =>
userPermissions.includes(permission)
);
};
/**
* Permission check hook - requires all permissions
* @param requiredPermissions Array of permissions to check
* @returns Whether user has all permissions
*/
export const usePermissionCheckAll = (requiredPermissions: string[]): boolean => {
const { auth } = useAuth();
if (!auth.isAuthenticated || !auth.user) {
return false;
}
const userPermissions = auth.user.permissions || [];
// If user has '*' permission, they have all permissions
if (userPermissions.includes('*')) {
return true;
}
// If user is admin, they have all permissions by default
if (auth.user.isAdmin) {
return true;
}
// Check if user has all required permissions
return requiredPermissions.every(permission =>
userPermissions.includes(permission)
);
};
export default PermissionChecker;

View File

@@ -7,20 +7,20 @@ interface ProtectedRouteProps {
redirectPath?: string;
}
const ProtectedRoute: React.FC<ProtectedRouteProps> = ({
redirectPath = '/login'
const ProtectedRoute: React.FC<ProtectedRouteProps> = ({
redirectPath = '/login'
}) => {
const { t } = useTranslation();
const { auth } = useAuth();
if (auth.loading) {
return <div className="flex items-center justify-center h-screen">{t('app.loading')}</div>;
}
if (!auth.isAuthenticated) {
return <Navigate to={redirectPath} replace />;
}
return <Outlet />;
};

View File

@@ -0,0 +1,205 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import { RegistryServerEntry } from '@/types';
interface RegistryServerCardProps {
serverEntry: RegistryServerEntry;
onClick: (serverEntry: RegistryServerEntry) => void;
}
const RegistryServerCard: React.FC<RegistryServerCardProps> = ({ serverEntry, onClick }) => {
const { t } = useTranslation();
const { server, _meta } = serverEntry;
const handleClick = () => {
onClick(serverEntry);
};
// Get display description
const getDisplayDescription = () => {
if (server.description && server.description.length <= 150) {
return server.description;
}
return server.description
? server.description.slice(0, 150) + '...'
: t('registry.noDescription');
};
// Format date for display
const formatDate = (dateString?: string) => {
if (!dateString) return '';
try {
const date = new Date(dateString);
const year = date.getFullYear();
const month = (date.getMonth() + 1).toString().padStart(2, '0');
const day = date.getDate().toString().padStart(2, '0');
return `${year}/${month}/${day}`;
} catch {
return '';
}
};
// Get icon to display
const getIcon = () => {
if (server.icons && server.icons.length > 0) {
// Prefer light theme icon
const lightIcon = server.icons.find((icon) => !icon.theme || icon.theme === 'light');
return lightIcon || server.icons[0];
}
return null;
};
const icon = getIcon();
const officialMeta = _meta?.['io.modelcontextprotocol.registry/official'];
const isLatest = officialMeta?.isLatest;
const publishedAt = officialMeta?.publishedAt;
const updatedAt = officialMeta?.updatedAt;
// Count packages and remotes
const packageCount = server.packages?.length || 0;
const remoteCount = server.remotes?.length || 0;
const totalOptions = packageCount + remoteCount;
return (
<div
className="bg-white border border-gray-200 rounded-xl p-4 hover:shadow-lg hover:border-blue-400 hover:-translate-y-1 transition-all duration-300 cursor-pointer group relative overflow-hidden h-full flex flex-col"
onClick={handleClick}
>
{/* Background gradient overlay on hover */}
<div className="absolute inset-0 bg-gradient-to-br from-blue-50/0 to-purple-50/0 group-hover:from-blue-50/30 group-hover:to-purple-50/30 transition-all duration-300 pointer-events-none" />
{/* Server Header */}
<div className="relative z-10 flex-1 flex flex-col">
<div className="flex items-start justify-between mb-3">
<div className="flex items-start space-x-3 flex-1">
{/* Icon */}
{icon ? (
<img
src={icon.src}
alt={server.title}
className="w-12 h-12 rounded-lg object-cover flex-shrink-0"
onError={(e) => {
e.currentTarget.style.display = 'none';
}}
/>
) : (
<div className="w-12 h-12 bg-gradient-to-br from-blue-500 to-purple-600 rounded-lg flex items-center justify-center text-white text-xl font-semibold flex-shrink-0">
M
</div>
)}
{/* Title and badges */}
<div className="flex-1 min-w-0">
<h3 className="text-lg font-bold text-gray-900 group-hover:text-blue-600 transition-colors duration-200 mb-1 line-clamp-2">
{server.name}
</h3>
<div className="flex flex-wrap gap-1 mb-1">
{isLatest && (
<span className="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800">
{t('registry.latest')}
</span>
)}
<span className="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800">
v{server.version}
</span>
</div>
</div>
</div>
</div>
{/* Server Name */}
{/* <div className="mb-2">
<p className="text-xs text-gray-500 font-mono">{server.name}</p>
</div> */}
{/* Description */}
<div className="mb-3 flex-1">
<p className="text-gray-600 text-sm leading-relaxed line-clamp-3">
{getDisplayDescription()}
</p>
</div>
{/* Installation Options Info */}
{totalOptions > 0 && (
<div className="mb-3">
<div className="flex items-center space-x-4">
{packageCount > 0 && (
<div className="flex items-center space-x-1">
<svg
className="w-4 h-4 text-gray-500"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4"
/>
</svg>
<span className="text-sm text-gray-600">
{packageCount}{' '}
{packageCount === 1 ? t('registry.package') : t('registry.packages')}
</span>
</div>
)}
{remoteCount > 0 && (
<div className="flex items-center space-x-1">
<svg
className="w-4 h-4 text-gray-500"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9a9 9 0 01-9-9m9 9c1.657 0 3-4.03 3-9s-1.343-9-3-9m0 18c-1.657 0-3-4.03-3-9s1.343-9 3-9m-9 9a9 9 0 019-9"
/>
</svg>
<span className="text-sm text-gray-600">
{remoteCount} {remoteCount === 1 ? t('registry.remote') : t('registry.remotes')}
</span>
</div>
)}
</div>
</div>
)}
{/* Footer - fixed at bottom */}
<div className="flex items-center justify-between pt-3 border-t border-gray-100 mt-auto">
<div className="flex items-center space-x-2 text-xs text-gray-500">
{(publishedAt || updatedAt) && (
<>
<svg className="w-3 h-3" fill="currentColor" viewBox="0 0 20 20">
<path
fillRule="evenodd"
d="M6 2a1 1 0 00-1 1v1H4a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V6a2 2 0 00-2-2h-1V3a1 1 0 10-2 0v1H7V3a1 1 0 00-1-1zm0 5a1 1 0 000 2h8a1 1 0 100-2H6z"
clipRule="evenodd"
/>
</svg>
<span>{formatDate(updatedAt || publishedAt)}</span>
</>
)}
</div>
<div className="flex items-center text-blue-600 text-sm font-medium group-hover:text-blue-700 transition-colors">
<span>{t('registry.viewDetails')}</span>
<svg
className="w-4 h-4 ml-1 transform group-hover:translate-x-1 transition-transform"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
</svg>
</div>
</div>
</div>
</div>
);
};
export default RegistryServerCard;

View File

@@ -0,0 +1,698 @@
import { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import {
RegistryServerEntry,
RegistryPackage,
RegistryRemote,
RegistryServerData,
ServerConfig,
} from '@/types';
import ServerForm from './ServerForm';
interface RegistryServerDetailProps {
serverEntry: RegistryServerEntry;
onBack: () => void;
onInstall?: (server: RegistryServerData, config: ServerConfig) => void;
installing?: boolean;
isInstalled?: boolean;
fetchVersions?: (serverName: string) => Promise<RegistryServerEntry[]>;
}
const RegistryServerDetail: React.FC<RegistryServerDetailProps> = ({
serverEntry,
onBack,
onInstall,
installing = false,
isInstalled = false,
fetchVersions,
}) => {
const { t } = useTranslation();
const { server, _meta } = serverEntry;
const [_selectedVersion, _setSelectedVersion] = useState<string>(server.version);
const [_availableVersions, setAvailableVersions] = useState<RegistryServerEntry[]>([]);
const [_loadingVersions, setLoadingVersions] = useState(false);
const [modalVisible, setModalVisible] = useState(false);
const [selectedInstallType, setSelectedInstallType] = useState<'package' | 'remote' | null>(null);
const [selectedOption, setSelectedOption] = useState<RegistryPackage | RegistryRemote | null>(
null,
);
const [installError, setInstallError] = useState<string | null>(null);
const [expandedSections, setExpandedSections] = useState<Record<string, boolean>>({
packages: true,
remotes: true,
repository: true,
});
const officialMeta = _meta?.['io.modelcontextprotocol.registry/official'];
// Load available versions
useEffect(() => {
const loadVersions = async () => {
if (fetchVersions) {
setLoadingVersions(true);
try {
const versions = await fetchVersions(server.name);
setAvailableVersions(versions);
} catch (error) {
console.error('Failed to load versions:', error);
} finally {
setLoadingVersions(false);
}
}
};
loadVersions();
}, [server.name, fetchVersions]);
// Get icon to display
const getIcon = () => {
if (server.icons && server.icons.length > 0) {
const lightIcon = server.icons.find((icon) => !icon.theme || icon.theme === 'light');
return lightIcon || server.icons[0];
}
return null;
};
const icon = getIcon();
// Format date
const formatDate = (dateString?: string) => {
if (!dateString) return '';
try {
const date = new Date(dateString);
return date.toLocaleDateString();
} catch {
return '';
}
};
// Toggle section expansion
const toggleSection = (section: string) => {
setExpandedSections((prev) => ({ ...prev, [section]: !prev[section] }));
};
// Handle install button click
const handleInstallClick = (
type: 'package' | 'remote',
option: RegistryPackage | RegistryRemote,
) => {
setSelectedInstallType(type);
setSelectedOption(option);
setInstallError(null);
setModalVisible(true);
};
// Handle modal close
const handleModalClose = () => {
setModalVisible(false);
setInstallError(null);
};
// Handle install submission
const handleInstallSubmit = async (payload: any) => {
try {
if (!onInstall || !selectedOption || !selectedInstallType) return;
setInstallError(null);
// Extract the ServerConfig from the payload
const config: ServerConfig = payload.config;
// Call onInstall with server data and config
onInstall(server, config);
setModalVisible(false);
} catch (err) {
console.error('Error installing server:', err);
setInstallError(t('errors.serverInstall'));
}
};
// Build initial data for ServerForm
const getInitialFormData = () => {
if (!selectedOption || !selectedInstallType) return null;
console.log('Building initial form data for:', selectedOption);
if (selectedInstallType === 'package' && 'identifier' in selectedOption) {
const pkg = selectedOption as RegistryPackage;
// Build environment variables from package definition
const env: Record<string, string> = {};
if (pkg.environmentVariables) {
pkg.environmentVariables.forEach((envVar) => {
env[envVar.name] = envVar.default || '';
});
}
const command = getCommand(pkg.registryType);
return {
name: server.name,
status: 'disconnected' as const,
config: {
type: 'stdio' as const,
command: command,
args: getArgs(command, pkg),
env: Object.keys(env).length > 0 ? env : undefined,
},
};
} else if (selectedInstallType === 'remote' && 'url' in selectedOption) {
const remote = selectedOption as RegistryRemote;
// Build headers from remote definition
const headers: Record<string, string> = {};
if (remote.headers) {
remote.headers.forEach((header) => {
headers[header.name] = header.default || header.value || '';
});
}
// Determine transport type - default to streamable-http for remotes
const transportType = remote.type === 'sse' ? ('sse' as const) : ('streamable-http' as const);
return {
name: server.name,
status: 'disconnected' as const,
config: {
type: transportType,
url: remote.url,
headers: Object.keys(headers).length > 0 ? headers : undefined,
},
};
}
return null;
};
// Render package option
const renderPackage = (pkg: RegistryPackage, index: number) => {
return (
<div
key={index}
className="border border-gray-200 rounded-lg p-4 mb-3 hover:border-blue-400 transition-colors"
>
<div className="flex items-start justify-between mb-3">
<div className="flex-1">
<h4 className="font-medium text-gray-900">{pkg.identifier}</h4>
{pkg.version && <p className="text-sm text-gray-500">Version: {pkg.version}</p>}
{pkg.runtimeHint && <p className="text-sm text-gray-600 mt-1">{pkg.runtimeHint}</p>}
</div>
<button
onClick={() => handleInstallClick('package', pkg)}
disabled={isInstalled || installing}
className={`px-4 py-2 rounded text-sm font-medium transition-colors ${
isInstalled
? 'bg-green-600 text-white cursor-default'
: installing
? 'bg-gray-400 text-white cursor-not-allowed'
: 'bg-blue-600 text-white hover:bg-blue-700'
}`}
>
{isInstalled
? t('registry.installed')
: installing
? t('registry.installing')
: t('registry.install')}
</button>
</div>
{/* Package details */}
{pkg.registryType && (
<div className="text-sm text-gray-600 mb-2">
<span className="font-medium">Registry:</span> {pkg.registryType}
</div>
)}
{/* Transport type */}
{pkg.transport && (
<div className="text-sm text-gray-600 mb-2">
<span className="font-medium">Transport:</span> {pkg.transport.type}
{pkg.transport.url && <span className="ml-2 text-gray-500">({pkg.transport.url})</span>}
</div>
)}
{/* Environment Variables */}
{pkg.environmentVariables && pkg.environmentVariables.length > 0 && (
<div className="mt-3 border-t border-gray-200 pt-3">
<h5 className="text-sm font-medium text-gray-700 mb-2">
{t('registry.environmentVariables')}:
</h5>
<div className="space-y-2">
{pkg.environmentVariables.map((envVar, envIndex) => (
<div key={envIndex} className="text-sm">
<div className="flex items-start">
<span className="font-mono text-gray-900 font-medium">{envVar.name}</span>
{envVar.isRequired && (
<span className="ml-2 inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-red-100 text-red-800">
{t('common.required')}
</span>
)}
{envVar.isSecret && (
<span className="ml-2 inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-yellow-100 text-yellow-800">
{t('common.secret')}
</span>
)}
</div>
{envVar.description && <p className="text-gray-600 mt-1">{envVar.description}</p>}
{envVar.default && (
<p className="text-gray-500 mt-1">
<span className="font-medium">{t('common.default')}:</span>{' '}
<span className="font-mono">{envVar.default}</span>
</p>
)}
</div>
))}
</div>
</div>
)}
{/* Package Arguments */}
{pkg.packageArguments && pkg.packageArguments.length > 0 && (
<div className="mt-3 border-t border-gray-200 pt-3">
<h5 className="text-sm font-medium text-gray-700 mb-2">
{t('registry.packageArguments')}:
</h5>
<div className="space-y-2">
{pkg.packageArguments.map((arg, argIndex) => (
<div key={argIndex} className="text-sm">
<div className="flex items-start">
<span className="font-mono text-gray-900 font-medium">{arg.name}</span>
{arg.isRequired && (
<span className="ml-2 inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-red-100 text-red-800">
{t('common.required')}
</span>
)}
{arg.isSecret && (
<span className="ml-2 inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-yellow-100 text-yellow-800">
{t('common.secret')}
</span>
)}
{arg.isRepeated && (
<span className="ml-2 inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-purple-100 text-purple-800">
{t('common.repeated')}
</span>
)}
</div>
{arg.description && <p className="text-gray-600 mt-1">{arg.description}</p>}
{arg.type && (
<p className="text-gray-500 mt-1">
<span className="font-medium">{t('common.type')}:</span>{' '}
<span className="font-mono">{arg.type}</span>
</p>
)}
{arg.default && (
<p className="text-gray-500 mt-1">
<span className="font-medium">{t('common.default')}:</span>{' '}
<span className="font-mono">{arg.default}</span>
</p>
)}
{arg.value && (
<p className="text-gray-500 mt-1">
<span className="font-medium">{t('common.value')}:</span>{' '}
<span className="font-mono">{arg.value}</span>
</p>
)}
{arg.valueHint && (
<p className="text-gray-500 mt-1">
<span className="font-medium">{t('common.valueHint')}:</span>{' '}
<span className="font-mono">{arg.valueHint}</span>
</p>
)}
{arg.choices && arg.choices.length > 0 && (
<p className="text-gray-500 mt-1">
<span className="font-medium">{t('common.choices')}:</span>{' '}
<span className="font-mono">{arg.choices.join(', ')}</span>
</p>
)}
</div>
))}
</div>
</div>
)}
</div>
);
};
// Render remote option
const renderRemote = (remote: RegistryRemote, index: number) => {
return (
<div
key={index}
className="border border-gray-200 rounded-lg p-4 mb-3 hover:border-blue-400 transition-colors"
>
<div className="flex items-start justify-between mb-3">
<div className="flex-1">
<h4 className="font-medium text-gray-900">{remote.type}</h4>
<p className="text-sm text-gray-600 mt-1 break-all">{remote.url}</p>
</div>
<button
onClick={() => handleInstallClick('remote', remote)}
disabled={isInstalled || installing}
className={`px-4 py-2 rounded text-sm font-medium transition-colors ${
isInstalled
? 'bg-green-600 text-white cursor-default'
: installing
? 'bg-gray-400 text-white cursor-not-allowed'
: 'bg-blue-600 text-white hover:bg-blue-700'
}`}
>
{isInstalled
? t('registry.installed')
: installing
? t('registry.installing')
: t('registry.install')}
</button>
</div>
{/* Headers */}
{remote.headers && remote.headers.length > 0 && (
<div className="mt-3 border-t border-gray-200 pt-3">
<h5 className="text-sm font-medium text-gray-700 mb-2">{t('registry.headers')}:</h5>
<div className="space-y-2">
{remote.headers.map((header, headerIndex) => (
<div key={headerIndex} className="text-sm">
<div className="flex items-start">
<span className="font-mono text-gray-900 font-medium">{header.name}</span>
{header.isRequired && (
<span className="ml-2 inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-red-100 text-red-800">
{t('common.required')}
</span>
)}
{header.isSecret && (
<span className="ml-2 inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-yellow-100 text-yellow-800">
{t('common.secret')}
</span>
)}
</div>
{header.description && <p className="text-gray-600 mt-1">{header.description}</p>}
{header.value && (
<p className="text-gray-500 mt-1">
<span className="font-medium">{t('common.value')}:</span>{' '}
<span className="font-mono">{header.value}</span>
</p>
)}
{header.default && (
<p className="text-gray-500 mt-1">
<span className="font-medium">{t('common.default')}:</span>{' '}
<span className="font-mono">{header.default}</span>
</p>
)}
</div>
))}
</div>
</div>
)}
</div>
);
};
return (
<div className="bg-white shadow rounded-lg p-6">
{/* Header */}
<div className="mb-6">
<button
onClick={onBack}
className="flex items-center text-blue-600 hover:text-blue-800 mb-4 transition-colors"
>
<svg className="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M15 19l-7-7 7-7"
/>
</svg>
{t('registry.backToList')}
</button>
<div className="flex items-start space-x-4">
{/* Icon */}
{icon ? (
<img
src={icon.src}
alt={server.title}
className="w-20 h-20 rounded-lg object-cover flex-shrink-0"
onError={(e) => {
e.currentTarget.style.display = 'none';
}}
/>
) : (
<div className="w-20 h-20 bg-gradient-to-br from-blue-500 to-purple-600 rounded-lg flex items-center justify-center text-white text-3xl font-semibold flex-shrink-0">
M
</div>
)}
{/* Title and metadata */}
<div className="flex-1">
<h1 className="text-3xl font-bold text-gray-900 mb-2">{server.name}</h1>
<div className="flex flex-wrap gap-2 mb-3">
{officialMeta?.isLatest && (
<span className="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-green-100 text-green-800">
{t('registry.latest')}
</span>
)}
<span className="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-blue-100 text-blue-800">
v{server.version}
</span>
{officialMeta?.status && (
<span className="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-gray-100 text-gray-800">
{officialMeta.status}
</span>
)}
{/* Dates */}
<span className="flex flex-wrap items-center gap-4 text-sm text-gray-600">
{officialMeta?.publishedAt && (
<div>
<span className="font-medium">{t('registry.published')}:</span>{' '}
{formatDate(officialMeta.publishedAt)}
</div>
)}
{officialMeta?.updatedAt && (
<div>
<span className="font-medium">{t('registry.updated')}:</span>{' '}
{formatDate(officialMeta.updatedAt)}
</div>
)}
</span>
</div>
</div>
</div>
</div>
{/* Description */}
<div className="mb-6">
<h2 className="text-xl font-semibold text-gray-900 mb-3">{t('registry.description')}</h2>
<p className="text-gray-700 leading-relaxed whitespace-pre-wrap">{server.description}</p>
</div>
{/* Website */}
{server.websiteUrl && (
<div className="mb-6">
<h2 className="text-xl font-semibold text-gray-900 mb-3">{t('registry.website')}</h2>
<a
href={server.websiteUrl}
target="_blank"
rel="noopener noreferrer"
className="text-blue-600 hover:text-blue-800 hover:underline"
>
{server.websiteUrl}
</a>
</div>
)}
{/* Packages */}
{server.packages && server.packages.length > 0 && (
<div className="mb-6">
<button
onClick={() => toggleSection('packages')}
className="flex items-center justify-between w-full text-xl font-semibold text-gray-900 mb-3 hover:text-blue-600 transition-colors"
>
<span>
{t('registry.packages')} ({server.packages.length})
</span>
<svg
className={`w-5 h-5 transform transition-transform ${expandedSections.packages ? 'rotate-180' : ''}`}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M19 9l-7 7-7-7"
/>
</svg>
</button>
{expandedSections.packages && (
<div className="space-y-3">{server.packages.map(renderPackage)}</div>
)}
</div>
)}
{/* Remotes */}
{server.remotes && server.remotes.length > 0 && (
<div className="mb-6">
<button
onClick={() => toggleSection('remotes')}
className="flex items-center justify-between w-full text-xl font-semibold text-gray-900 mb-3 hover:text-blue-600 transition-colors"
>
<span>
{t('registry.remotes')} ({server.remotes.length})
</span>
<svg
className={`w-5 h-5 transform transition-transform ${expandedSections.remotes ? 'rotate-180' : ''}`}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M19 9l-7 7-7-7"
/>
</svg>
</button>
{expandedSections.remotes && (
<div className="space-y-3">{server.remotes.map(renderRemote)}</div>
)}
</div>
)}
{/* Repository */}
{server.repository && (
<div className="mb-6">
<button
onClick={() => toggleSection('repository')}
className="flex items-center justify-between w-full text-xl font-semibold text-gray-900 mb-3 hover:text-blue-600 transition-colors"
>
<span>{t('registry.repository')}</span>
<svg
className={`w-5 h-5 transform transition-transform ${expandedSections.repository ? 'rotate-180' : ''}`}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M19 9l-7 7-7-7"
/>
</svg>
</button>
{expandedSections.repository && (
<div className="border border-gray-200 rounded-lg p-4">
{server.repository.url && (
<div className="mb-2">
<span className="font-medium text-gray-700">URL:</span>{' '}
<a
href={server.repository.url}
target="_blank"
rel="noopener noreferrer"
className="text-blue-600 hover:text-blue-800 hover:underline break-all"
>
{server.repository.url}
</a>
</div>
)}
{server.repository.source && (
<div className="mb-2">
<span className="font-medium text-gray-700">Source:</span>{' '}
{server.repository.source}
</div>
)}
{server.repository.subfolder && (
<div className="mb-2">
<span className="font-medium text-gray-700">Subfolder:</span>{' '}
{server.repository.subfolder}
</div>
)}
{server.repository.id && (
<div>
<span className="font-medium text-gray-700">ID:</span> {server.repository.id}
</div>
)}
</div>
)}
</div>
)}
{/* Install Modal */}
{modalVisible && selectedOption && selectedInstallType && (
<div className="fixed inset-0 bg-black/50 z-50 flex items-center justify-center p-4">
<ServerForm
onSubmit={handleInstallSubmit}
onCancel={handleModalClose}
modalTitle={t('registry.installServer', { name: server.title || server.name })}
formError={installError}
initialData={getInitialFormData()}
/>
</div>
)}
</div>
);
};
export default RegistryServerDetail;
// Helper function to determine command based on registry type
function getCommand(registryType: string): string {
// Map registry types to appropriate commands
switch (registryType.toLowerCase()) {
case 'pypi':
case 'python':
return 'uvx';
case 'npm':
case 'node':
return 'npx';
case 'oci':
case 'docker':
return 'docker';
default:
return '';
}
}
// Helper function to get appropriate args based on command type and package identifier
function getArgs(command: string, pkg: RegistryPackage): string[] {
const identifier = [pkg.identifier + (pkg.version ? `@${pkg.version}` : '')];
// Build package arguments if available
const packageArgs: string[] = [];
if (pkg.packageArguments && pkg.packageArguments.length > 0) {
pkg.packageArguments.forEach((arg) => {
// Add required arguments or arguments with default values
if (arg.isRequired || arg.default || arg.value) {
const argName = `--${arg.name}`;
// Priority: value > default > placeholder
const argValue = arg.value || arg.default || `\${${arg.name.toUpperCase()}}`;
packageArgs.push(argName, argValue);
}
});
}
// Map commands to appropriate argument patterns
switch (command.toLowerCase()) {
case 'uvx':
// For Python packages: uvx package-name --arg1 value1 --arg2 value2
return [...identifier, ...packageArgs];
case 'npx':
// For Node.js packages: npx package-name --arg1 value1 --arg2 value2
return [...identifier, ...packageArgs];
case 'docker': {
// add envs from environment variables if available
const envs: string[] = [];
if (pkg.environmentVariables) {
pkg.environmentVariables.forEach((env) => {
envs.push('-e', `${env.name}`);
});
}
// For Docker images: docker run -i package-name --arg1 value1 --arg2 value2
return ['run', '-i', '--rm', ...envs, ...identifier, ...packageArgs];
}
default:
// If no specific pattern is defined, return identifier with package args
return [...identifier, ...packageArgs];
}
}

View File

@@ -1,124 +1,248 @@
import { useState, useRef, useEffect } from 'react'
import { useTranslation } from 'react-i18next'
import { Server } from '@/types'
import { ChevronDown, ChevronRight, AlertCircle, Copy, Check } from 'lucide-react'
import { StatusBadge } from '@/components/ui/Badge'
import ToolCard from '@/components/ui/ToolCard'
import DeleteDialog from '@/components/ui/DeleteDialog'
import { useToast } from '@/contexts/ToastContext'
import { useState, useRef, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { Server } from '@/types';
import { ChevronDown, ChevronRight, AlertCircle, Copy, Check } from 'lucide-react';
import { StatusBadge } from '@/components/ui/Badge';
import ToolCard from '@/components/ui/ToolCard';
import PromptCard from '@/components/ui/PromptCard';
import DeleteDialog from '@/components/ui/DeleteDialog';
import { useToast } from '@/contexts/ToastContext';
import { useSettingsData } from '@/hooks/useSettingsData';
interface ServerCardProps {
server: Server
onRemove: (serverName: string) => void
onEdit: (server: Server) => void
onToggle?: (server: Server, enabled: boolean) => void
server: Server;
onRemove: (serverName: string) => void;
onEdit: (server: Server) => void;
onToggle?: (server: Server, enabled: boolean) => Promise<boolean>;
onRefresh?: () => void;
}
const ServerCard = ({ server, onRemove, onEdit, onToggle }: ServerCardProps) => {
const { t } = useTranslation()
const { showToast } = useToast()
const [isExpanded, setIsExpanded] = useState(false)
const [showDeleteDialog, setShowDeleteDialog] = useState(false)
const [isToggling, setIsToggling] = useState(false)
const [showErrorPopover, setShowErrorPopover] = useState(false)
const [copied, setCopied] = useState(false)
const errorPopoverRef = useRef<HTMLDivElement>(null)
const ServerCard = ({ server, onRemove, onEdit, onToggle, onRefresh }: ServerCardProps) => {
const { t } = useTranslation();
const { showToast } = useToast();
const [isExpanded, setIsExpanded] = useState(false);
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
const [isToggling, setIsToggling] = useState(false);
const [showErrorPopover, setShowErrorPopover] = useState(false);
const [copied, setCopied] = useState(false);
const errorPopoverRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (errorPopoverRef.current && !errorPopoverRef.current.contains(event.target as Node)) {
setShowErrorPopover(false)
setShowErrorPopover(false);
}
}
};
document.addEventListener('mousedown', handleClickOutside)
document.addEventListener('mousedown', handleClickOutside);
return () => {
document.removeEventListener('mousedown', handleClickOutside)
}
}, [])
document.removeEventListener('mousedown', handleClickOutside);
};
}, []);
const { exportMCPSettings } = useSettingsData();
const handleRemove = (e: React.MouseEvent) => {
e.stopPropagation()
setShowDeleteDialog(true)
}
e.stopPropagation();
setShowDeleteDialog(true);
};
const handleEdit = (e: React.MouseEvent) => {
e.stopPropagation()
onEdit(server)
}
e.stopPropagation();
onEdit(server);
};
const handleToggle = async (e: React.MouseEvent) => {
e.stopPropagation()
if (isToggling || !onToggle) return
e.stopPropagation();
if (isToggling || !onToggle) return;
setIsToggling(true)
setIsToggling(true);
try {
await onToggle(server, !(server.enabled !== false))
await onToggle(server, !(server.enabled !== false));
} finally {
setIsToggling(false)
setIsToggling(false);
}
}
};
const handleErrorIconClick = (e: React.MouseEvent) => {
e.stopPropagation()
setShowErrorPopover(!showErrorPopover)
}
e.stopPropagation();
setShowErrorPopover(!showErrorPopover);
};
const copyToClipboard = (e: React.MouseEvent) => {
e.stopPropagation()
if (!server.error) return
e.stopPropagation();
if (!server.error) return;
if (navigator.clipboard && window.isSecureContext) {
navigator.clipboard.writeText(server.error).then(() => {
setCopied(true)
showToast(t('common.copySuccess') || 'Copied to clipboard', 'success')
setTimeout(() => setCopied(false), 2000)
})
setCopied(true);
showToast(t('common.copySuccess') || 'Copied to clipboard', 'success');
setTimeout(() => setCopied(false), 2000);
});
} else {
// Fallback for HTTP or unsupported clipboard API
const textArea = document.createElement('textarea')
textArea.value = server.error
const textArea = document.createElement('textarea');
textArea.value = server.error;
// Avoid scrolling to bottom
textArea.style.position = 'fixed'
textArea.style.left = '-9999px'
document.body.appendChild(textArea)
textArea.focus()
textArea.select()
textArea.style.position = 'fixed';
textArea.style.left = '-9999px';
document.body.appendChild(textArea);
textArea.focus();
textArea.select();
try {
document.execCommand('copy')
setCopied(true)
showToast(t('common.copySuccess') || 'Copied to clipboard', 'success')
setTimeout(() => setCopied(false), 2000)
document.execCommand('copy');
setCopied(true);
showToast(t('common.copySuccess') || 'Copied to clipboard', 'success');
setTimeout(() => setCopied(false), 2000);
} catch (err) {
showToast(t('common.copyFailed') || 'Copy failed', 'error')
console.error('Copy to clipboard failed:', err)
showToast(t('common.copyFailed') || 'Copy failed', 'error');
console.error('Copy to clipboard failed:', err);
}
document.body.removeChild(textArea)
document.body.removeChild(textArea);
}
}
};
const handleCopyServerConfig = async (e: React.MouseEvent) => {
e.stopPropagation();
try {
const result = await exportMCPSettings(server.name);
const configJson = JSON.stringify(result.data, null, 2);
if (navigator.clipboard && window.isSecureContext) {
await navigator.clipboard.writeText(configJson);
showToast(t('common.copySuccess') || 'Copied to clipboard', 'success');
} else {
// Fallback for HTTP or unsupported clipboard API
const textArea = document.createElement('textarea');
textArea.value = configJson;
textArea.style.position = 'fixed';
textArea.style.left = '-9999px';
document.body.appendChild(textArea);
textArea.focus();
textArea.select();
try {
document.execCommand('copy');
showToast(t('common.copySuccess') || 'Copied to clipboard', 'success');
} catch (err) {
showToast(t('common.copyFailed') || 'Copy failed', 'error');
console.error('Copy to clipboard failed:', err);
}
document.body.removeChild(textArea);
}
} catch (error) {
console.error('Error copying server configuration:', error);
showToast(t('common.copyFailed') || 'Copy failed', 'error');
}
};
const handleConfirmDelete = () => {
onRemove(server.name)
setShowDeleteDialog(false)
}
onRemove(server.name);
setShowDeleteDialog(false);
};
const handleToolToggle = async (toolName: string, enabled: boolean) => {
try {
const { toggleTool } = await import('@/services/toolService');
const result = await toggleTool(server.name, toolName, enabled);
if (result.success) {
showToast(
t(enabled ? 'tool.enableSuccess' : 'tool.disableSuccess', { name: toolName }),
'success',
);
// Trigger refresh to update the tool's state in the UI
if (onRefresh) {
onRefresh();
}
} else {
showToast(result.error || t('tool.toggleFailed'), 'error');
}
} catch (error) {
console.error('Error toggling tool:', error);
showToast(t('tool.toggleFailed'), 'error');
}
};
const handlePromptToggle = async (promptName: string, enabled: boolean) => {
try {
const { togglePrompt } = await import('@/services/promptService');
const result = await togglePrompt(server.name, promptName, enabled);
if (result.success) {
showToast(
t(enabled ? 'tool.enableSuccess' : 'tool.disableSuccess', { name: promptName }),
'success',
);
// Trigger refresh to update the prompt's state in the UI
if (onRefresh) {
onRefresh();
}
} else {
showToast(result.error || t('tool.toggleFailed'), 'error');
}
} catch (error) {
console.error('Error toggling prompt:', error);
showToast(t('tool.toggleFailed'), 'error');
}
};
const handleOAuthAuthorization = (e: React.MouseEvent) => {
e.stopPropagation();
// Open the OAuth authorization URL in a new window
if (server.oauth?.authorizationUrl) {
const width = 600;
const height = 700;
const left = window.screen.width / 2 - width / 2;
const top = window.screen.height / 2 - height / 2;
window.open(
server.oauth.authorizationUrl,
'OAuth Authorization',
`width=${width},height=${height},left=${left},top=${top}`,
);
showToast(t('status.oauthWindowOpened'), 'info');
}
};
return (
<>
<div className={`bg-white shadow rounded-lg p-6 mb-6 ${server.enabled === false ? 'opacity-60' : ''}`}>
<div
className={`bg-white shadow rounded-lg p-6 mb-6 page-card transition-all duration-200 ${server.enabled === false ? 'opacity-60' : ''}`}
>
<div
className="flex justify-between items-center cursor-pointer"
onClick={() => setIsExpanded(!isExpanded)}
>
<div className="flex items-center space-x-3">
<h2 className={`text-xl font-semibold ${server.enabled === false ? 'text-gray-600' : 'text-gray-900'}`}>{server.name}</h2>
<StatusBadge status={server.status} />
<h2
className={`text-xl font-semibold ${server.enabled === false ? 'text-gray-600' : 'text-gray-900'}`}
>
{server.name}
</h2>
<StatusBadge status={server.status} onAuthClick={handleOAuthAuthorization} />
{/* Tool count display */}
<div className="flex items-center px-2 py-1 bg-blue-50 text-blue-700 rounded-full text-sm">
<div className="flex items-center px-2 py-1 bg-blue-50 text-blue-700 rounded-full text-sm btn-primary">
<svg className="w-4 h-4 mr-1" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M11.3 1.046A1 1 0 0112 2v5h4a1 1 0 01.82 1.573l-7 10A1 1 0 018 18v-5H4a1 1 0 01-.82-1.573l7-10a1 1 0 011.12-.38z" clipRule="evenodd" />
<path
fillRule="evenodd"
d="M11.3 1.046A1 1 0 0112 2v5h4a1 1 0 01.82 1.573l-7 10A1 1 0 018 18v-5H4a1 1 0 01-.82-1.573l7-10a1 1 0 011.12-.38z"
clipRule="evenodd"
/>
</svg>
<span>{server.tools?.length || 0} {t('server.tools')}</span>
<span>
{server.tools?.length || 0} {t('server.tools')}
</span>
</div>
{/* Prompt count display */}
<div className="flex items-center px-2 py-1 bg-purple-50 text-purple-700 rounded-full text-sm btn-primary">
<svg className="w-4 h-4 mr-1" fill="currentColor" viewBox="0 0 20 20">
<path d="M2 5a2 2 0 012-2h7a2 2 0 012 2v4a2 2 0 01-2 2H9l-3 3v-3H4a2 2 0 01-2-2V5z" />
<path d="M15 7v2a4 4 0 01-4 4H9.828l-1.766 1.767c.28.149.599.233.938.233h2l3 3v-3h2a2 2 0 002-2V9a2 2 0 00-2-2h-1z" />
</svg>
<span>
{server.prompts?.length || 0} {t('server.prompts')}
</span>
</div>
{server.error && (
@@ -141,25 +265,31 @@ const ServerCard = ({ server, onRemove, onEdit, onToggle }: ServerCardProps) =>
maxHeight: '300px',
overflowY: 'auto',
width: '480px',
transform: 'translateX(50%)'
transform: 'translateX(50%)',
}}
onClick={(e) => e.stopPropagation()}
>
<div className="flex justify-between items-center sticky top-0 bg-white py-2 px-4 border-b border-gray-200 z-20 shadow-sm">
<div className="flex items-center space-x-2">
<h4 className="text-sm font-medium text-red-600">{t('server.errorDetails')}</h4>
<h4 className="text-sm font-medium text-red-600">
{t('server.errorDetails')}
</h4>
<button
onClick={copyToClipboard}
className="p-1 text-gray-400 hover:text-gray-600 transition-colors"
className="p-1 text-gray-400 hover:text-gray-600 transition-colors btn-secondary"
title={t('common.copy')}
>
{copied ? <Check size={14} className="text-green-500" /> : <Copy size={14} />}
{copied ? (
<Check size={14} className="text-green-500" />
) : (
<Copy size={14} />
)}
</button>
</div>
<button
onClick={(e) => {
e.stopPropagation()
setShowErrorPopover(false)
e.stopPropagation();
setShowErrorPopover(false);
}}
className="text-gray-400 hover:text-gray-600"
>
@@ -167,7 +297,9 @@ const ServerCard = ({ server, onRemove, onEdit, onToggle }: ServerCardProps) =>
</button>
</div>
<div className="p-4 pt-2">
<pre className="text-sm text-gray-700 break-words whitespace-pre-wrap">{server.error}</pre>
<pre className="text-sm text-gray-700 break-words whitespace-pre-wrap">
{server.error}
</pre>
</div>
</div>
)}
@@ -175,52 +307,88 @@ const ServerCard = ({ server, onRemove, onEdit, onToggle }: ServerCardProps) =>
)}
</div>
<div className="flex space-x-2">
<button onClick={handleCopyServerConfig} className={`px-3 py-1 btn-secondary`}>
{t('server.copy')}
</button>
<button
onClick={handleEdit}
className="px-3 py-1 bg-blue-100 text-blue-800 rounded hover:bg-blue-200 text-sm"
className="px-3 py-1 bg-blue-100 text-blue-800 rounded hover:bg-blue-200 text-sm btn-primary"
>
{t('server.edit')}
</button>
<div className="flex items-center">
<button
onClick={handleToggle}
className={`px-3 py-1 text-sm rounded transition-colors ${isToggling
? 'bg-gray-200 text-gray-500'
: server.enabled !== false
? 'bg-green-100 text-green-800 hover:bg-green-200'
: 'bg-gray-100 text-gray-800 hover:bg-gray-200'
}`}
className={`px-3 py-1 text-sm rounded transition-colors ${
isToggling
? 'bg-gray-200 text-gray-500'
: server.enabled !== false
? 'bg-green-100 text-green-800 hover:bg-green-200 btn-secondary'
: 'bg-gray-100 text-gray-800 hover:bg-gray-200 btn-primary'
}`}
disabled={isToggling}
>
{isToggling
? t('common.processing')
: server.enabled !== false
? t('server.disable')
: t('server.enable')
}
: t('server.enable')}
</button>
</div>
<button
onClick={handleRemove}
className="px-3 py-1 bg-red-100 text-red-800 rounded hover:bg-red-200 text-sm"
className="px-3 py-1 bg-red-100 text-red-800 rounded hover:bg-red-200 text-sm btn-danger"
>
{t('server.delete')}
</button>
<button className="text-gray-400 hover:text-gray-600">
<button className="text-gray-400 hover:text-gray-600 btn-secondary">
{isExpanded ? <ChevronDown size={18} /> : <ChevronRight size={18} />}
</button>
</div>
</div>
{isExpanded && server.tools && (
<div className="mt-6">
<h6 className={`font-medium ${server.enabled === false ? 'text-gray-600' : 'text-gray-900'} mb-4`}>{t('server.tools')}</h6>
<div className="space-y-4">
{server.tools.map((tool, index) => (
<ToolCard key={index} server={server.name} tool={tool} />
))}
</div>
</div>
{isExpanded && (
<>
{server.tools && (
<div className="mt-6">
<h6
className={`font-medium ${server.enabled === false ? 'text-gray-600' : 'text-gray-900'} mb-4`}
>
{t('server.tools')}
</h6>
<div className="space-y-4">
{server.tools.map((tool, index) => (
<ToolCard
key={index}
server={server.name}
tool={tool}
onToggle={handleToolToggle}
/>
))}
</div>
</div>
)}
{server.prompts && (
<div className="mt-6">
<h6
className={`font-medium ${server.enabled === false ? 'text-gray-600' : 'text-gray-900'} mb-4`}
>
{t('server.prompts')}
</h6>
<div className="space-y-4">
{server.prompts.map((prompt, index) => (
<PromptCard
key={index}
server={server.name}
prompt={prompt}
onToggle={handlePromptToggle}
/>
))}
</div>
</div>
)}
</>
)}
</div>
@@ -231,7 +399,7 @@ const ServerCard = ({ server, onRemove, onEdit, onToggle }: ServerCardProps) =>
serverName={server.name}
/>
</>
)
}
);
};
export default ServerCard
export default ServerCard;

Some files were not shown because too many files have changed in this diff Show More