Compare commits

...

7 Commits

41 changed files with 4765 additions and 2277 deletions

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

@@ -27,27 +27,16 @@
"pages": [
"features/server-management",
"features/group-management",
"features/smart-routing",
"features/authentication",
"features/monitoring"
"features/smart-routing"
]
},
{
"group": "Configuration",
"pages": [
"configuration/mcp-settings",
"configuration/environment-variables",
"configuration/docker-setup",
"configuration/nginx"
]
},
{
"group": "Development",
"pages": [
"development/getting-started",
"development/architecture",
"development/contributing"
]
}
]
},
@@ -67,51 +56,16 @@
"pages": [
"zh/features/server-management",
"zh/features/group-management",
"zh/features/smart-routing",
"zh/features/authentication",
"zh/features/monitoring"
"zh/features/smart-routing"
]
},
{
"group": "配置指南",
"pages": [
"zh/configuration/mcp-settings",
"zh/configuration/environment-variables",
"zh/configuration/docker-setup",
"zh/configuration/nginx"
]
},
{
"group": "开发指南",
"pages": [
"zh/development/getting-started",
"zh/development/architecture",
"zh/development/contributing"
]
}
]
},
{
"tab": "API Reference",
"groups": [
{
"group": "MCP Endpoints",
"pages": [
"api-reference/introduction",
"api-reference/mcp-http",
"api-reference/mcp-sse",
"api-reference/smart-routing"
]
},
{
"group": "Management API",
"pages": [
"api-reference/servers",
"api-reference/groups",
"api-reference/auth",
"api-reference/logs",
"api-reference/config"
]
}
]
}
@@ -144,13 +98,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": {

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

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
@@ -287,7 +287,7 @@ Access Smart Routing through the special `$smart` endpoint:
</Tab>
</Tabs>
### Basic Usage
{/* ### Basic Usage
Connect your AI client to the Smart Routing endpoint and make natural language requests:
@@ -330,9 +330,9 @@ Response:
]
}
}
```
``` */}
### Advanced Queries
{/* ### Advanced Queries
Smart Routing supports various query types:
@@ -405,9 +405,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 +426,9 @@ curl -X POST http://localhost:3000/mcp/$smart \
}
}
}'
```
``` */}
## Performance Optimization
{/* ## Performance Optimization
### Embedding Cache
@@ -585,7 +585,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

@@ -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

File diff suppressed because it is too large Load Diff

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

@@ -1,5 +1,5 @@
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';
@@ -15,6 +15,12 @@ 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 (
@@ -35,6 +41,12 @@ function App() {
<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>

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">True</option>
<option value="false">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,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,6 +10,16 @@ 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) {
@@ -80,70 +90,89 @@ const MarketServerCard: React.FC<MarketServerCardProps> = ({ server, onClick })
return (
<div
className="bg-white rounded-lg shadow-md p-5 hover:shadow-lg transition-all duration-200 cursor-pointer flex flex-col h-full page-card"
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="text-xs font-medium px-2.5 py-0.5 rounded flex-shrink-0 label-primary">
{t('market.official')}
</span>
)}
</div>
<p className="text-gray-600 text-sm mb-4 line-clamp-2 min-h-[40px]">{server.description}</p>
{/* 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" />
{/* 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.5 rounded whitespace-nowrap"
>
{category}
</span>
))
) : (
<span className="text-xs text-gray-400 py-1">-</span>
)}
</div>
{/* 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>
{/* 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 label-secondary"
>
#{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')}
{/* 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">
<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

@@ -297,7 +297,7 @@ const DynamicForm: React.FC<DynamicFormProps> = ({ schema, onSubmit, onCancel, l
<input
type="number"
step={schema.type === 'integer' ? '1' : 'any'}
value={value || ''}
value={value ?? ''}
onChange={(e) => {
const val = e.target.value === '' ? '' : schema.type === 'integer' ? parseInt(e.target.value) : parseFloat(e.target.value);
onChange(val);
@@ -542,7 +542,7 @@ const DynamicForm: React.FC<DynamicFormProps> = ({ schema, onSubmit, onCancel, l
<input
type="number"
step={propSchema.type === 'integer' ? '1' : 'any'}
value={value || ''}
value={value !== undefined && value !== null ? value : ''}
onChange={(e) => {
const val = e.target.value === '' ? '' : propSchema.type === 'integer' ? parseInt(e.target.value) : parseFloat(e.target.value);
handleInputChange(fullPath, val);

View File

@@ -0,0 +1,350 @@
import { useState, useEffect, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { CloudServer, ApiResponse, CloudServerTool } from '@/types';
import { apiGet, apiPost } from '../utils/fetchInterceptor';
export const useCloudData = () => {
const { t } = useTranslation();
const [servers, setServers] = useState<CloudServer[]>([]);
const [allServers, setAllServers] = useState<CloudServer[]>([]);
const [categories, setCategories] = useState<string[]>([]);
const [tags, setTags] = useState<string[]>([]);
const [selectedCategory, setSelectedCategory] = useState<string>('');
const [selectedTag, setSelectedTag] = useState<string>('');
const [searchQuery, setSearchQuery] = useState<string>('');
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [currentServer, setCurrentServer] = useState<CloudServer | null>(null);
// Pagination states
const [currentPage, setCurrentPage] = useState(1);
const [serversPerPage, setServersPerPage] = useState(9);
const [totalPages, setTotalPages] = useState(1);
// Fetch all cloud market servers
const fetchCloudServers = useCallback(async () => {
try {
setLoading(true);
const data: ApiResponse<CloudServer[]> = await apiGet('/cloud/servers');
if (data && data.success && Array.isArray(data.data)) {
setAllServers(data.data);
// Apply pagination to the fetched data
applyPagination(data.data, currentPage);
} else {
console.error('Invalid cloud market servers data format:', data);
setError(t('cloud.fetchError'));
}
} catch (err) {
console.error('Error fetching cloud market servers:', err);
const errorMessage = err instanceof Error ? err.message : String(err);
// Keep the original error message for API key errors
if (
errorMessage === 'MCPROUTER_API_KEY_NOT_CONFIGURED' ||
errorMessage.toLowerCase().includes('mcprouter api key not configured')
) {
setError(errorMessage);
} else {
setError(errorMessage);
}
} finally {
setLoading(false);
}
}, [t]);
// Apply pagination to data
const applyPagination = useCallback(
(data: CloudServer[], page: number, itemsPerPage = serversPerPage) => {
const totalItems = data.length;
const calculatedTotalPages = Math.ceil(totalItems / itemsPerPage);
setTotalPages(calculatedTotalPages);
// Ensure current page is valid
const validPage = Math.max(1, Math.min(page, calculatedTotalPages));
if (validPage !== page) {
setCurrentPage(validPage);
}
const startIndex = (validPage - 1) * itemsPerPage;
const paginatedServers = data.slice(startIndex, startIndex + itemsPerPage);
setServers(paginatedServers);
},
[serversPerPage],
);
// Change page
const changePage = useCallback(
(page: number) => {
setCurrentPage(page);
applyPagination(allServers, page, serversPerPage);
},
[allServers, applyPagination, serversPerPage],
);
// Fetch all categories
const fetchCategories = useCallback(async () => {
try {
const data: ApiResponse<string[]> = await apiGet('/cloud/categories');
if (data && data.success && Array.isArray(data.data)) {
setCategories(data.data);
} else {
console.error('Invalid cloud market categories data format:', data);
}
} catch (err) {
console.error('Error fetching cloud market categories:', err);
}
}, []);
// Fetch all tags
const fetchTags = useCallback(async () => {
try {
const data: ApiResponse<string[]> = await apiGet('/cloud/tags');
if (data && data.success && Array.isArray(data.data)) {
setTags(data.data);
} else {
console.error('Invalid cloud market tags data format:', data);
}
} catch (err) {
console.error('Error fetching cloud market tags:', err);
}
}, []);
// Fetch server by name
const fetchServerByName = useCallback(
async (name: string) => {
try {
setLoading(true);
const data: ApiResponse<CloudServer> = await apiGet(`/cloud/servers/${name}`);
if (data && data.success && data.data) {
setCurrentServer(data.data);
return data.data;
} else {
console.error('Invalid cloud server data format:', data);
setError(t('cloud.serverNotFound'));
return null;
}
} catch (err) {
console.error(`Error fetching cloud server ${name}:`, err);
const errorMessage = err instanceof Error ? err.message : String(err);
// Keep the original error message for API key errors
if (
errorMessage === 'MCPROUTER_API_KEY_NOT_CONFIGURED' ||
errorMessage.toLowerCase().includes('mcprouter api key not configured')
) {
setError(errorMessage);
} else {
setError(errorMessage);
}
return null;
} finally {
setLoading(false);
}
},
[t],
);
// Search servers by query
const searchServers = useCallback(
async (query: string) => {
try {
setLoading(true);
setSearchQuery(query);
if (!query.trim()) {
// Fetch fresh data from server instead of just applying pagination
fetchCloudServers();
return;
}
const data: ApiResponse<CloudServer[]> = await apiGet(
`/cloud/servers/search?query=${encodeURIComponent(query)}`,
);
if (data && data.success && Array.isArray(data.data)) {
setAllServers(data.data);
setCurrentPage(1);
applyPagination(data.data, 1);
} else {
console.error('Invalid cloud search results format:', data);
setError(t('cloud.searchError'));
}
} catch (err) {
console.error('Error searching cloud servers:', err);
setError(err instanceof Error ? err.message : String(err));
} finally {
setLoading(false);
}
},
[t, allServers, applyPagination, fetchCloudServers],
);
// Filter servers by category
const filterByCategory = useCallback(
async (category: string) => {
try {
setLoading(true);
setSelectedCategory(category);
setSelectedTag(''); // Reset tag filter when filtering by category
if (!category) {
fetchCloudServers();
return;
}
const data: ApiResponse<CloudServer[]> = await apiGet(
`/cloud/categories/${encodeURIComponent(category)}`,
);
if (data && data.success && Array.isArray(data.data)) {
setAllServers(data.data);
setCurrentPage(1);
applyPagination(data.data, 1);
} else {
console.error('Invalid cloud category filter results format:', data);
setError(t('cloud.filterError'));
}
} catch (err) {
console.error('Error filtering cloud servers by category:', err);
setError(err instanceof Error ? err.message : String(err));
} finally {
setLoading(false);
}
},
[t, fetchCloudServers, applyPagination],
);
// Filter servers by tag
const filterByTag = useCallback(
async (tag: string) => {
try {
setLoading(true);
setSelectedTag(tag);
setSelectedCategory(''); // Reset category filter when filtering by tag
if (!tag) {
fetchCloudServers();
return;
}
const data: ApiResponse<CloudServer[]> = await apiGet(
`/cloud/tags/${encodeURIComponent(tag)}`,
);
if (data && data.success && Array.isArray(data.data)) {
setAllServers(data.data);
setCurrentPage(1);
applyPagination(data.data, 1);
} else {
console.error('Invalid cloud tag filter results format:', data);
setError(t('cloud.tagFilterError'));
}
} catch (err) {
console.error('Error filtering cloud servers by tag:', err);
setError(err instanceof Error ? err.message : String(err));
} finally {
setLoading(false);
}
},
[t, fetchCloudServers, applyPagination],
);
// Fetch tools for a specific server
const fetchServerTools = useCallback(async (serverName: string) => {
try {
const data: ApiResponse<CloudServerTool[]> = await apiGet(
`/cloud/servers/${serverName}/tools`,
);
if (!data.success) {
console.error('Failed to fetch cloud server tools:', data);
throw new Error(data.message || 'Failed to fetch cloud server tools');
}
if (data && data.success && Array.isArray(data.data)) {
return data.data;
} else {
console.error('Invalid cloud server tools data format:', data);
return [];
}
} catch (err) {
console.error(`Error fetching tools for cloud server ${serverName}:`, err);
const errorMessage = err instanceof Error ? err.message : String(err);
// Re-throw API key errors so they can be handled by the component
if (
errorMessage === 'MCPROUTER_API_KEY_NOT_CONFIGURED' ||
errorMessage.toLowerCase().includes('mcprouter api key not configured')
) {
throw err;
}
return [];
}
}, []);
// Call a tool on a cloud server
const callServerTool = useCallback(
async (serverName: string, toolName: string, args: Record<string, any>) => {
try {
const data = await apiPost(`/cloud/servers/${serverName}/tools/${toolName}/call`, {
arguments: args,
});
if (data && data.success) {
return data.data;
} else {
throw new Error(data.message || 'Failed to call tool');
}
} catch (err) {
console.error(`Error calling tool ${toolName} on cloud server ${serverName}:`, err);
throw err;
}
},
[],
);
// Change servers per page
const changeServersPerPage = useCallback(
(perPage: number) => {
setServersPerPage(perPage);
setCurrentPage(1);
applyPagination(allServers, 1, perPage);
},
[allServers, applyPagination],
);
// Load initial data
useEffect(() => {
fetchCloudServers();
fetchCategories();
fetchTags();
}, [fetchCloudServers, fetchCategories, fetchTags]);
return {
servers,
allServers,
categories,
tags,
selectedCategory,
selectedTag,
searchQuery,
loading,
error,
setError,
currentServer,
fetchCloudServers: fetchCloudServers,
fetchServerByName,
searchServers,
filterByCategory,
filterByTag,
fetchServerTools,
callServerTool,
// Pagination properties and methods
currentPage,
totalPages,
serversPerPage,
changePage,
changeServersPerPage,
};
};

View File

@@ -27,11 +27,19 @@ interface SmartRoutingConfig {
openaiApiEmbeddingModel: string;
}
interface MCPRouterConfig {
apiKey: string;
referer: string;
title: string;
baseUrl: string;
}
interface SystemSettings {
systemConfig?: {
routing?: RoutingConfig;
install?: InstallConfig;
smartRouting?: SmartRoutingConfig;
mcpRouter?: MCPRouterConfig;
};
}
@@ -69,6 +77,13 @@ export const useSettingsData = () => {
openaiApiEmbeddingModel: '',
});
const [mcpRouterConfig, setMCPRouterConfig] = useState<MCPRouterConfig>({
apiKey: '',
referer: 'https://mcphub.app',
title: 'MCPHub',
baseUrl: 'https://api.mcprouter.to/v1',
});
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [refreshKey, setRefreshKey] = useState(0);
@@ -112,6 +127,14 @@ export const useSettingsData = () => {
data.data.systemConfig.smartRouting.openaiApiEmbeddingModel || '',
});
}
if (data.success && data.data?.systemConfig?.mcpRouter) {
setMCPRouterConfig({
apiKey: data.data.systemConfig.mcpRouter.apiKey || '',
referer: data.data.systemConfig.mcpRouter.referer || 'https://mcphub.app',
title: data.data.systemConfig.mcpRouter.title || 'MCPHub',
baseUrl: data.data.systemConfig.mcpRouter.baseUrl || 'https://api.mcprouter.to/v1',
});
}
} catch (error) {
console.error('Failed to fetch settings:', error);
setError(error instanceof Error ? error.message : 'Failed to fetch settings');
@@ -290,6 +313,77 @@ export const useSettingsData = () => {
}
};
// Update MCPRouter configuration
const updateMCPRouterConfig = async <T extends keyof MCPRouterConfig>(
key: T,
value: MCPRouterConfig[T],
) => {
setLoading(true);
setError(null);
try {
const data = await apiPut('/system-config', {
mcpRouter: {
[key]: value,
},
});
if (data.success) {
setMCPRouterConfig({
...mcpRouterConfig,
[key]: value,
});
showToast(t('settings.systemConfigUpdated'));
return true;
} else {
showToast(data.message || t('errors.failedToUpdateSystemConfig'));
return false;
}
} catch (error) {
console.error('Failed to update MCPRouter config:', error);
const errorMessage =
error instanceof Error ? error.message : 'Failed to update MCPRouter config';
setError(errorMessage);
showToast(errorMessage);
return false;
} finally {
setLoading(false);
}
};
// Update multiple MCPRouter configuration fields at once
const updateMCPRouterConfigBatch = async (updates: Partial<MCPRouterConfig>) => {
setLoading(true);
setError(null);
try {
const data = await apiPut('/system-config', {
mcpRouter: updates,
});
if (data.success) {
setMCPRouterConfig({
...mcpRouterConfig,
...updates,
});
showToast(t('settings.systemConfigUpdated'));
return true;
} else {
showToast(data.message || t('errors.failedToUpdateSystemConfig'));
return false;
}
} catch (error) {
console.error('Failed to update MCPRouter config:', error);
const errorMessage =
error instanceof Error ? error.message : 'Failed to update MCPRouter config';
setError(errorMessage);
showToast(errorMessage);
return false;
} finally {
setLoading(false);
}
};
// Fetch settings when the component mounts or refreshKey changes
useEffect(() => {
fetchSettings();
@@ -309,6 +403,7 @@ export const useSettingsData = () => {
setTempRoutingConfig,
installConfig,
smartRoutingConfig,
mcpRouterConfig,
loading,
error,
setError,
@@ -319,5 +414,7 @@ export const useSettingsData = () => {
updateSmartRoutingConfig,
updateSmartRoutingConfigBatch,
updateRoutingConfigBatch,
updateMCPRouterConfig,
updateMCPRouterConfigBatch,
};
};

View File

@@ -442,6 +442,30 @@ tbody tr:hover {
color: rgba(239, 154, 154, 0.9) !important;
}
/* External link styles */
.external-link {
color: #2563eb !important; /* Blue-600 for light mode */
text-decoration: none;
border-bottom: 1px solid transparent;
transition: all 0.2s ease-in-out;
cursor: pointer;
}
.external-link:hover {
color: #1d4ed8 !important; /* Blue-700 for light mode */
border-bottom-color: #1d4ed8;
text-decoration: none;
}
.dark .external-link {
color: #60a5fa !important; /* Blue-400 for dark mode */
}
.dark .external-link:hover {
color: #93c5fd !important; /* Blue-300 for dark mode */
border-bottom-color: #93c5fd;
}
.border-red {
border-color: #937d7d; /* Tailwind red-800 for light mode */
}

View File

@@ -1,11 +1,16 @@
import React, { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { useNavigate, useParams } from 'react-router-dom';
import { MarketServer, ServerConfig } from '@/types';
import { useNavigate, useParams, useSearchParams } from 'react-router-dom';
import { MarketServer, CloudServer, ServerConfig } from '@/types';
import { useMarketData } from '@/hooks/useMarketData';
import { useCloudData } from '@/hooks/useCloudData';
import { useToast } from '@/contexts/ToastContext';
import { apiPost } from '@/utils/fetchInterceptor';
import MarketServerCard from '@/components/MarketServerCard';
import MarketServerDetail from '@/components/MarketServerDetail';
import CloudServerCard from '@/components/CloudServerCard';
import CloudServerDetail from '@/components/CloudServerDetail';
import MCPRouterApiKeyError from '@/components/MCPRouterApiKeyError';
import Pagination from '@/components/ui/Pagination';
const MarketPage: React.FC = () => {
@@ -14,82 +19,140 @@ const MarketPage: React.FC = () => {
const { serverName } = useParams<{ serverName?: string }>();
const { showToast } = useToast();
// Get tab from URL search params, default to cloud market
const [searchParams, setSearchParams] = useSearchParams();
const currentTab = searchParams.get('tab') || 'cloud';
// Local market data
const {
servers,
allServers,
categories,
loading,
error,
setError,
searchServers,
filterByCategory,
filterByTag,
selectedCategory,
selectedTag,
installServer,
fetchServerByName,
servers: localServers,
allServers: allLocalServers,
categories: localCategories,
loading: localLoading,
error: localError,
setError: setLocalError,
searchServers: searchLocalServers,
filterByCategory: filterLocalByCategory,
filterByTag: filterLocalByTag,
selectedCategory: selectedLocalCategory,
selectedTag: selectedLocalTag,
installServer: installLocalServer,
fetchServerByName: fetchLocalServerByName,
isServerInstalled,
// Pagination
currentPage,
totalPages,
changePage,
serversPerPage,
changeServersPerPage
currentPage: localCurrentPage,
totalPages: localTotalPages,
changePage: changeLocalPage,
serversPerPage: localServersPerPage,
changeServersPerPage: changeLocalServersPerPage
} = useMarketData();
// Cloud market data
const {
servers: cloudServers,
allServers: allCloudServers,
loading: cloudLoading,
error: cloudError,
setError: setCloudError,
fetchServerTools,
callServerTool,
// Pagination
currentPage: cloudCurrentPage,
totalPages: cloudTotalPages,
changePage: changeCloudPage,
serversPerPage: cloudServersPerPage,
changeServersPerPage: changeCloudServersPerPage
} = useCloudData();
const [selectedServer, setSelectedServer] = useState<MarketServer | null>(null);
const [selectedCloudServer, setSelectedCloudServer] = useState<CloudServer | null>(null);
const [searchQuery, setSearchQuery] = useState('');
const [installing, setInstalling] = useState(false);
const [installedCloudServers, setInstalledCloudServers] = useState<Set<string>>(new Set());
// Load server details if a server name is in the URL
useEffect(() => {
const loadServerDetails = async () => {
if (serverName) {
const server = await fetchServerByName(serverName);
if (server) {
setSelectedServer(server);
// Determine if it's a cloud or local server based on the current tab
if (currentTab === 'cloud') {
// Try to find the server in cloud servers
const server = cloudServers.find(s => s.name === serverName);
if (server) {
setSelectedCloudServer(server);
} else {
// If server not found, navigate back to market page
navigate('/market?tab=cloud');
}
} else {
// If server not found, navigate back to market page
navigate('/market');
// Local market
const server = await fetchLocalServerByName(serverName);
if (server) {
setSelectedServer(server);
} else {
// If server not found, navigate back to market page
navigate('/market?tab=local');
}
}
} else {
setSelectedServer(null);
setSelectedCloudServer(null);
}
};
loadServerDetails();
}, [serverName, fetchServerByName, navigate]);
}, [serverName, currentTab, cloudServers, fetchLocalServerByName, navigate]);
// Tab switching handler
const switchTab = (tab: 'local' | 'cloud') => {
const newSearchParams = new URLSearchParams(searchParams);
newSearchParams.set('tab', tab);
setSearchParams(newSearchParams);
// Clear any selected server when switching tabs
if (serverName) {
navigate('/market?' + newSearchParams.toString());
}
};
const handleSearch = (e: React.FormEvent) => {
e.preventDefault();
searchServers(searchQuery);
if (currentTab === 'local') {
searchLocalServers(searchQuery);
}
// Cloud search is not implemented in the original cloud page
};
const handleCategoryClick = (category: string) => {
filterByCategory(category);
if (currentTab === 'local') {
filterLocalByCategory(category);
}
};
const handleClearFilters = () => {
setSearchQuery('');
filterByCategory('');
filterByTag('');
if (currentTab === 'local') {
filterLocalByCategory('');
filterLocalByTag('');
}
};
const handleServerClick = (server: MarketServer) => {
navigate(`/market/${server.name}`);
const handleServerClick = (server: MarketServer | CloudServer) => {
if (currentTab === 'cloud') {
navigate(`/market/${server.name}?tab=cloud`);
} else {
navigate(`/market/${server.name}?tab=local`);
}
};
const handleBackToList = () => {
navigate('/market');
navigate(`/market?tab=${currentTab}`);
};
const handleInstall = async (server: MarketServer, config: ServerConfig) => {
const handleLocalInstall = async (server: MarketServer, config: ServerConfig) => {
try {
setInstalling(true);
// Pass the server object and the config to the installServer function
const success = await installServer(server, config);
const success = await installLocalServer(server, config);
if (success) {
// Show success message using toast instead of alert
showToast(t('market.installSuccess', { serverName: server.display_name }), 'success');
}
} finally {
@@ -97,15 +160,75 @@ const MarketPage: React.FC = () => {
}
};
// Handle cloud server installation
const handleCloudInstall = async (server: CloudServer, config: ServerConfig) => {
try {
setInstalling(true);
const payload = {
name: server.name,
config: config
};
const result = await apiPost('/servers', payload);
if (!result.success) {
const errorMessage = result?.message || t('server.addError');
showToast(errorMessage, 'error');
return;
}
// Update installed servers set
setInstalledCloudServers(prev => new Set(prev).add(server.name));
showToast(t('cloud.installSuccess', { name: server.title || server.name }), 'success');
} catch (error) {
console.error('Error installing cloud server:', error);
const errorMessage = error instanceof Error ? error.message : String(error);
showToast(t('cloud.installError', { error: errorMessage }), 'error');
} finally {
setInstalling(false);
}
};
const handleCallTool = async (serverName: string, toolName: string, args: Record<string, any>) => {
try {
const result = await callServerTool(serverName, toolName, args);
showToast(t('cloud.toolCallSuccess', { toolName }), 'success');
return result;
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
// Don't show toast for API key errors, let the component handle it
if (!isMCPRouterApiKeyError(errorMessage)) {
showToast(t('cloud.toolCallError', { toolName, error: errorMessage }), 'error');
}
throw error;
}
};
// Helper function to check if error is MCPRouter API key not configured
const isMCPRouterApiKeyError = (errorMessage: string) => {
return errorMessage === 'MCPROUTER_API_KEY_NOT_CONFIGURED' ||
errorMessage.toLowerCase().includes('mcprouter api key not configured');
};
const handlePageChange = (page: number) => {
changePage(page);
if (currentTab === 'local') {
changeLocalPage(page);
} else {
changeCloudPage(page);
}
// Scroll to top of page when changing pages
window.scrollTo({ top: 0, behavior: 'smooth' });
};
const handleChangeItemsPerPage = (e: React.ChangeEvent<HTMLSelectElement>) => {
const newValue = parseInt(e.target.value, 10);
changeServersPerPage(newValue);
if (currentTab === 'local') {
changeLocalServersPerPage(newValue);
} else {
changeCloudServersPerPage(newValue);
}
};
// Render detailed view if a server is selected
@@ -114,164 +237,201 @@ const MarketPage: React.FC = () => {
<MarketServerDetail
server={selectedServer}
onBack={handleBackToList}
onInstall={handleInstall}
onInstall={handleLocalInstall}
installing={installing}
isInstalled={isServerInstalled(selectedServer.name)}
/>
);
}
// Render cloud server detail if selected
if (selectedCloudServer) {
return (
<CloudServerDetail
serverName={selectedCloudServer.name}
onBack={handleBackToList}
onCallTool={handleCallTool}
fetchServerTools={fetchServerTools}
onInstall={handleCloudInstall}
installing={installing}
isInstalled={installedCloudServers.has(selectedCloudServer.name)}
/>
);
}
// Get current data based on active tab
const isLocalTab = currentTab === 'local';
const servers = isLocalTab ? localServers : cloudServers;
const allServers = isLocalTab ? allLocalServers : allCloudServers;
const categories = isLocalTab ? localCategories : [];
const loading = isLocalTab ? localLoading : cloudLoading;
const error = isLocalTab ? localError : cloudError;
const setError = isLocalTab ? setLocalError : setCloudError;
const selectedCategory = isLocalTab ? selectedLocalCategory : '';
const selectedTag = isLocalTab ? selectedLocalTag : '';
const currentPage = isLocalTab ? localCurrentPage : cloudCurrentPage;
const totalPages = isLocalTab ? localTotalPages : cloudTotalPages;
const serversPerPage = isLocalTab ? localServersPerPage : cloudServersPerPage;
return (
<div>
<div className="flex justify-between items-center mb-8">
<div>
<h1 className="text-2xl font-bold text-gray-900 flex items-center">
{t('market.title')}
<span className="text-sm text-gray-500 font-normal ml-2">{t('pages.market.title').split(' - ')[1]}</span>
</h1>
{/* Tab Navigation */}
<div className="mb-6">
<div className="border-b border-gray-200">
<nav className="-mb-px flex space-x-3">
<button
onClick={() => switchTab('cloud')}
className={`py-2 px-1 border-b-2 font-medium text-lg hover:cursor-pointer transition-colors duration-200 ${!isLocalTab
? 'border-blue-500 text-blue-600'
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
}`}
>
{t('cloud.title')}
<span className="text-xs text-gray-400 font-normal ml-1">(
<a
href="https://mcprouter.co"
target="_blank"
rel="noopener noreferrer"
className="external-link"
>
MCPRouter
</a>
)
</span>
</button>
<button
onClick={() => switchTab('local')}
className={`py-2 px-1 border-b-2 font-medium text-lg hover:cursor-pointer transition-colors duration-200 ${isLocalTab
? 'border-blue-500 text-blue-600'
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
}`}
>
{t('market.title')}
<span className="text-xs text-gray-400 font-normal ml-1">(
<a
href="https://mcpm.sh"
target="_blank"
rel="noopener noreferrer"
className="external-link"
>
MCPM
</a>
)
</span>
</button>
</nav>
</div>
</div>
{error && (
<div className="bg-red-100 border-l-4 border-red-500 text-red-700 p-4 mb-6 error-box rounded-lg">
<div className="flex items-center justify-between">
<p>{error}</p>
<>
{!isLocalTab && isMCPRouterApiKeyError(error) ? (
<MCPRouterApiKeyError />
) : (
<div className="bg-red-100 border-l-4 border-red-500 text-red-700 p-4 mb-6 error-box rounded-lg">
<div className="flex items-center justify-between">
<p>{error}</p>
<button
onClick={() => setError(null)}
className="text-red-700 hover:text-red-900 transition-colors duration-200"
>
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path fillRule="evenodd" d="M4.293 4.293a1 1 011.414 0L10 8.586l4.293-4.293a1 1 01.414 1.414L11.414 10l4.293 4.293a1 1 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 01-1.414-1.414L8.586 10 4.293 5.707a1 1 010-1.414z" clipRule="evenodd" />
</svg>
</button>
</div>
</div>
)}
</>
)}
{/* Search bar for local market only */}
{isLocalTab && (
<div className="bg-white shadow rounded-lg p-6 mb-6 page-card">
<form onSubmit={handleSearch} className="flex space-x-4 mb-0">
<div className="flex-grow">
<input
type="text"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
placeholder={t('market.searchPlaceholder')}
className="shadow appearance-none border border-gray-200 rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline form-input"
/>
</div>
<button
onClick={() => setError(null)}
className="text-red-700 hover:text-red-900 transition-colors duration-200"
type="submit"
className="px-4 py-2 bg-blue-100 text-blue-800 rounded hover:bg-blue-200 flex items-center btn-primary transition-all duration-200"
>
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path fillRule="evenodd" d="M4.293 4.293a1 1 011.414 0L10 8.586l4.293-4.293a1 1 01.414 1.414L11.414 10l4.293 4.293a1 1 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 01-1.414-1.414L8.586 10 4.293 5.707a1 1 010-1.414z" clipRule="evenodd" />
</svg>
{t('market.search')}
</button>
</div>
{(searchQuery || selectedCategory || selectedTag) && (
<button
type="button"
onClick={handleClearFilters}
className="border border-gray-300 text-gray-700 font-medium py-2 px-4 rounded hover:bg-gray-50 btn-secondary transition-all duration-200"
>
{t('market.clearFilters')}
</button>
)}
</form>
</div>
)}
{/* Search bar at the top */}
<div className="bg-white shadow rounded-lg p-6 mb-6 page-card">
<form onSubmit={handleSearch} className="flex space-x-4 mb-0">
<div className="flex-grow">
<input
type="text"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
placeholder={t('market.searchPlaceholder')}
className="shadow appearance-none border border-gray-200 rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline form-input"
/>
</div>
<button
type="submit"
className="px-4 py-2 bg-blue-100 text-blue-800 rounded hover:bg-blue-200 flex items-center btn-primary transition-all duration-200"
>
{t('market.search')}
</button>
{(searchQuery || selectedCategory || selectedTag) && (
<button
type="button"
onClick={handleClearFilters}
className="border border-gray-300 text-gray-700 font-medium py-2 px-4 rounded hover:bg-gray-50 btn-secondary transition-all duration-200"
>
{t('market.clearFilters')}
</button>
)}
</form>
</div>
<div className="flex flex-col md:flex-row gap-6">
{/* Left sidebar for filters (without search) */}
<div className="md:w-48 flex-shrink-0">
<div className="bg-white shadow rounded-lg p-4 mb-6 sticky top-4 page-card">
{/* Categories */}
{categories.length > 0 ? (
<div className="mb-6">
<div className="flex justify-between items-center mb-3">
<h3 className="font-medium text-gray-900">{t('market.categories')}</h3>
{selectedCategory && (
<span className="text-xs text-blue-600 cursor-pointer hover:underline transition-colors duration-200" onClick={() => filterByCategory('')}>
{t('market.clearCategoryFilter')}
</span>
)}
</div>
<div className="flex flex-col gap-2">
{categories.map((category) => (
<button
key={category}
onClick={() => handleCategoryClick(category)}
className={`px-3 py-2 rounded text-sm text-left transition-all duration-200 ${selectedCategory === category
? 'bg-blue-100 text-blue-800 font-medium btn-primary'
: 'bg-gray-100 text-gray-800 hover:bg-gray-200 btn-secondary'
}`}
>
{category}
</button>
))}
</div>
</div>
) : loading ? (
<div className="mb-6">
<div className="mb-3">
<h3 className="font-medium text-gray-900">{t('market.categories')}</h3>
</div>
<div className="flex flex-col gap-2 items-center py-4 loading-container">
<svg className="animate-spin h-6 w-6 text-blue-500 mb-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>
<p className="text-sm text-gray-600">{t('app.loading')}</p>
</div>
</div>
) : (
<div className="mb-6">
<div className="mb-3">
<h3 className="font-medium text-gray-900">{t('market.categories')}</h3>
</div>
<p className="text-sm text-gray-600 py-2">{t('market.noCategories')}</p>
</div>
)}
{/* Tags */}
{/* {tags.length > 0 && (
<div className="mb-4">
<div className="flex justify-between items-center mb-3">
<div className="flex items-center">
<h3 className="font-medium text-gray-900">{t('market.tags')}</h3>
<button
onClick={toggleTagsVisibility}
className="ml-2 p-1 text-gray-600 hover:text-blue-600 hover:bg-gray-100 rounded-full"
aria-label={showTags ? t('market.hideTags') : t('market.showTags')}
>
<svg xmlns="http://www.w3.org/2000/svg" className={`h-5 w-5 transition-transform ${showTags ? 'rotate-180' : ''}`} viewBox="0 0 20 20" fill="currentColor">
<path fillRule="evenodd" d="M5.293 7.293a1 1 011.414 0L10 10.586l3.293-3.293a1 1 011.414 1.414l-4 4a1 1 01-1.414 0l-4-4a1 1 010-1.414z" clipRule="evenodd" />
</svg>
</button>
{/* Left sidebar for filters (local market only) */}
{isLocalTab && (
<div className="md:w-48 flex-shrink-0">
<div className="bg-white shadow rounded-lg p-4 mb-6 sticky top-4 page-card">
{/* Categories */}
{categories.length > 0 ? (
<div className="mb-6">
<div className="flex justify-between items-center mb-3">
<h3 className="font-medium text-gray-900">{t('market.categories')}</h3>
{selectedCategory && (
<span className="text-xs text-blue-600 cursor-pointer hover:underline transition-colors duration-200" onClick={() => filterLocalByCategory('')}>
{t('market.clearCategoryFilter')}
</span>
)}
</div>
{selectedTag && (
<span className="text-xs text-blue-600 cursor-pointer hover:underline" onClick={() => filterByTag('')}>
{t('market.clearTagFilter')}
</span>
)}
</div>
{showTags && (
<div className="flex flex-wrap gap-2 max-h-48 overflow-y-auto pr-2">
{tags.map((tag) => (
<div className="flex flex-col gap-2">
{categories.map((category) => (
<button
key={tag}
onClick={() => handleTagClick(tag)}
className={`px-2 py-1 rounded text-xs ${selectedTag === tag
? 'bg-green-100 text-green-800 font-medium'
: 'bg-gray-100 text-gray-700 hover:bg-gray-200'
key={category}
onClick={() => handleCategoryClick(category)}
className={`px-3 py-2 rounded text-sm text-left transition-all duration-200 ${selectedCategory === category
? 'bg-blue-100 text-blue-800 font-medium btn-primary'
: 'bg-gray-100 text-gray-800 hover:bg-gray-200 btn-secondary'
}`}
>
#{tag}
{category}
</button>
))}
</div>
)}
</div>
)} */}
</div>
) : loading ? (
<div className="mb-6">
<div className="mb-3">
<h3 className="font-medium text-gray-900">{t('market.categories')}</h3>
</div>
<div className="flex flex-col gap-2 items-center py-4 loading-container">
<svg className="animate-spin h-6 w-6 text-blue-500 mb-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>
<p className="text-sm text-gray-600">{t('app.loading')}</p>
</div>
</div>
) : (
<div className="mb-6">
<div className="mb-3">
<h3 className="font-medium text-gray-900">{t('market.categories')}</h3>
</div>
<p className="text-sm text-gray-600 py-2">{t('market.noCategories')}</p>
</div>
)}
</div>
</div>
</div>
)}
{/* Main content area */}
<div className="flex-grow">
@@ -287,27 +447,43 @@ const MarketPage: React.FC = () => {
</div>
) : servers.length === 0 ? (
<div className="bg-white shadow rounded-lg p-6">
<p className="text-gray-600">{t('market.noServers')}</p>
<p className="text-gray-600">{isLocalTab ? t('market.noServers') : t('cloud.noServers')}</p>
</div>
) : (
<>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-2 xl:grid-cols-3 gap-6">
{servers.map((server, index) => (
<MarketServerCard
key={index}
server={server}
onClick={handleServerClick}
/>
isLocalTab ? (
<MarketServerCard
key={index}
server={server as MarketServer}
onClick={handleServerClick}
/>
) : (
<CloudServerCard
key={index}
server={server as CloudServer}
onClick={handleServerClick}
/>
)
))}
</div>
<div className="flex justify-between items-center mb-4">
<div className="text-sm text-gray-500">
{t('market.showing', {
from: (currentPage - 1) * serversPerPage + 1,
to: Math.min(currentPage * serversPerPage, allServers.length),
total: allServers.length
})}
{isLocalTab ? (
t('market.showing', {
from: (currentPage - 1) * serversPerPage + 1,
to: Math.min(currentPage * serversPerPage, allServers.length),
total: allServers.length
})
) : (
t('cloud.showing', {
from: (currentPage - 1) * serversPerPage + 1,
to: Math.min(currentPage * serversPerPage, allServers.length),
total: allServers.length
})
)}
</div>
<Pagination
currentPage={currentPage}
@@ -316,7 +492,7 @@ const MarketPage: React.FC = () => {
/>
<div className="flex items-center space-x-2">
<label htmlFor="perPage" className="text-sm text-gray-600">
{t('market.perPage')}:
{isLocalTab ? t('market.perPage') : t('cloud.perPage')}:
</label>
<select
id="perPage"
@@ -333,7 +509,6 @@ const MarketPage: React.FC = () => {
</div>
<div className="mt-6">
</div>
</>
)}

View File

@@ -36,18 +36,32 @@ const SettingsPage: React.FC = () => {
openaiApiEmbeddingModel: '',
});
const [tempMCPRouterConfig, setTempMCPRouterConfig] = useState<{
apiKey: string;
referer: string;
title: string;
baseUrl: string;
}>({
apiKey: '',
referer: 'https://mcphub.app',
title: 'MCPHub',
baseUrl: 'https://api.mcprouter.to/v1',
});
const {
routingConfig,
tempRoutingConfig,
setTempRoutingConfig,
installConfig: savedInstallConfig,
smartRoutingConfig,
mcpRouterConfig,
loading,
updateRoutingConfig,
updateRoutingConfigBatch,
updateInstallConfig,
updateSmartRoutingConfig,
updateSmartRoutingConfigBatch
updateSmartRoutingConfigBatch,
updateMCPRouterConfig
} = useSettingsData();
// Update local installConfig when savedInstallConfig changes
@@ -69,14 +83,27 @@ const SettingsPage: React.FC = () => {
}
}, [smartRoutingConfig]);
// Update local tempMCPRouterConfig when mcpRouterConfig changes
useEffect(() => {
if (mcpRouterConfig) {
setTempMCPRouterConfig({
apiKey: mcpRouterConfig.apiKey || '',
referer: mcpRouterConfig.referer || 'https://mcphub.app',
title: mcpRouterConfig.title || 'MCPHub',
baseUrl: mcpRouterConfig.baseUrl || 'https://api.mcprouter.to/v1',
});
}
}, [mcpRouterConfig]);
const [sectionsVisible, setSectionsVisible] = useState({
routingConfig: false,
installConfig: false,
smartRoutingConfig: false,
mcpRouterConfig: false,
password: false
});
const toggleSection = (section: 'routingConfig' | 'installConfig' | 'smartRoutingConfig' | 'password') => {
const toggleSection = (section: 'routingConfig' | 'installConfig' | 'smartRoutingConfig' | 'mcpRouterConfig' | 'password') => {
setSectionsVisible(prev => ({
...prev,
[section]: !prev[section]
@@ -143,6 +170,17 @@ const SettingsPage: React.FC = () => {
await updateSmartRoutingConfig(key, tempSmartRoutingConfig[key]);
};
const handleMCPRouterConfigChange = (key: 'apiKey' | 'referer' | 'title' | 'baseUrl', value: string) => {
setTempMCPRouterConfig({
...tempMCPRouterConfig,
[key]: value
});
};
const saveMCPRouterConfig = async (key: 'apiKey' | 'referer' | 'title' | 'baseUrl') => {
await updateMCPRouterConfig(key, tempMCPRouterConfig[key]);
};
const handleSmartRoutingEnabledChange = async (value: boolean) => {
// If enabling Smart Routing, validate required fields and save any unsaved changes
if (value) {
@@ -197,7 +235,7 @@ const SettingsPage: React.FC = () => {
{/* Smart Routing Configuration Settings */}
<PermissionChecker permissions={PERMISSIONS.SETTINGS_SMART_ROUTING}>
<div className="bg-white shadow rounded-lg py-4 px-6 mb-6 page-card">
<div className="bg-white shadow rounded-lg py-4 px-6 mb-6 page-card dashboard-card">
<div
className="flex justify-between items-center cursor-pointer transition-colors duration-200 hover:text-blue-600"
onClick={() => toggleSection('smartRoutingConfig')}
@@ -322,8 +360,123 @@ const SettingsPage: React.FC = () => {
</div>
</PermissionChecker>
{/* MCPRouter Configuration Settings */}
<PermissionChecker permissions={PERMISSIONS.SETTINGS_INSTALL_CONFIG}>
<div className="bg-white shadow rounded-lg py-4 px-6 mb-6 page-card dashboard-card">
<div
className="flex justify-between items-center cursor-pointer transition-colors duration-200 hover:text-blue-600"
onClick={() => toggleSection('mcpRouterConfig')}
>
<h2 className="font-semibold text-gray-800">{t('settings.mcpRouterConfig')}</h2>
<span className="text-gray-500 transition-transform duration-200">
{sectionsVisible.mcpRouterConfig ? '▼' : '►'}
</span>
</div>
{sectionsVisible.mcpRouterConfig && (
<div className="space-y-4 mt-4">
<div className="p-3 bg-gray-50 rounded-md">
<div className="mb-2">
<h3 className="font-medium text-gray-700">{t('settings.mcpRouterApiKey')}</h3>
<p className="text-sm text-gray-500">{t('settings.mcpRouterApiKeyDescription')}</p>
</div>
<div className="flex items-center gap-3">
<input
type="password"
value={tempMCPRouterConfig.apiKey}
onChange={(e) => handleMCPRouterConfigChange('apiKey', e.target.value)}
placeholder={t('settings.mcpRouterApiKeyPlaceholder')}
className="flex-1 mt-1 block w-full py-2 px-3 border rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm border-gray-300 form-input"
disabled={loading}
/>
<button
onClick={() => saveMCPRouterConfig('apiKey')}
disabled={loading}
className="mt-1 px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-md text-sm font-medium disabled:opacity-50 btn-primary"
>
{t('common.save')}
</button>
</div>
</div>
<div className="p-3 bg-gray-50 rounded-md">
<div className="mb-2">
<h3 className="font-medium text-gray-700">{t('settings.mcpRouterReferer')}</h3>
<p className="text-sm text-gray-500">{t('settings.mcpRouterRefererDescription')}</p>
</div>
<div className="flex items-center gap-3">
<input
type="text"
value={tempMCPRouterConfig.referer}
onChange={(e) => handleMCPRouterConfigChange('referer', e.target.value)}
placeholder={t('settings.mcpRouterRefererPlaceholder')}
className="flex-1 mt-1 block w-full py-2 px-3 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm form-input"
disabled={loading}
/>
<button
onClick={() => saveMCPRouterConfig('referer')}
disabled={loading}
className="mt-1 px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-md text-sm font-medium disabled:opacity-50 btn-primary"
>
{t('common.save')}
</button>
</div>
</div>
<div className="p-3 bg-gray-50 rounded-md">
<div className="mb-2">
<h3 className="font-medium text-gray-700">{t('settings.mcpRouterTitle')}</h3>
<p className="text-sm text-gray-500">{t('settings.mcpRouterTitleDescription')}</p>
</div>
<div className="flex items-center gap-3">
<input
type="text"
value={tempMCPRouterConfig.title}
onChange={(e) => handleMCPRouterConfigChange('title', e.target.value)}
placeholder={t('settings.mcpRouterTitlePlaceholder')}
className="flex-1 mt-1 block w-full py-2 px-3 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm form-input"
disabled={loading}
/>
<button
onClick={() => saveMCPRouterConfig('title')}
disabled={loading}
className="mt-1 px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-md text-sm font-medium disabled:opacity-50 btn-primary"
>
{t('common.save')}
</button>
</div>
</div>
<div className="p-3 bg-gray-50 rounded-md">
<div className="mb-2">
<h3 className="font-medium text-gray-700">{t('settings.mcpRouterBaseUrl')}</h3>
<p className="text-sm text-gray-500">{t('settings.mcpRouterBaseUrlDescription')}</p>
</div>
<div className="flex items-center gap-3">
<input
type="text"
value={tempMCPRouterConfig.baseUrl}
onChange={(e) => handleMCPRouterConfigChange('baseUrl', e.target.value)}
placeholder={t('settings.mcpRouterBaseUrlPlaceholder')}
className="flex-1 mt-1 block w-full py-2 px-3 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm form-input"
disabled={loading}
/>
<button
onClick={() => saveMCPRouterConfig('baseUrl')}
disabled={loading}
className="mt-1 px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-md text-sm font-medium disabled:opacity-50 btn-primary"
>
{t('common.save')}
</button>
</div>
</div>
</div>
)}
</div>
</PermissionChecker>
{/* Route Configuration Settings */}
<div className="bg-white shadow rounded-lg py-4 px-6 mb-6">
<div className="bg-white shadow rounded-lg py-4 px-6 mb-6 dashboard-card">
<div
className="flex justify-between items-center cursor-pointer"
onClick={() => toggleSection('routingConfig')}
@@ -418,7 +571,7 @@ const SettingsPage: React.FC = () => {
{/* Installation Configuration Settings */}
<PermissionChecker permissions={PERMISSIONS.SETTINGS_INSTALL_CONFIG}>
<div className="bg-white shadow rounded-lg py-4 px-6 mb-6">
<div className="bg-white shadow rounded-lg py-4 px-6 mb-6 dashboard-card">
<div
className="flex justify-between items-center cursor-pointer"
onClick={() => toggleSection('installConfig')}
@@ -508,7 +661,7 @@ const SettingsPage: React.FC = () => {
</PermissionChecker>
{/* Change Password */}
<div className="bg-white shadow rounded-lg py-4 px-6 mb-6">
<div className="bg-white shadow rounded-lg py-4 px-6 mb-6 dashboard-card">
<div
className="flex justify-between items-center cursor-pointer"
onClick={() => toggleSection('password')}

View File

@@ -55,6 +55,27 @@ export interface MarketServer {
is_official?: boolean;
}
// Cloud Server types (for MCPRouter API)
export interface CloudServer {
created_at: string;
updated_at: string;
name: string;
author_name: string;
title: string;
description: string;
content: string;
server_key: string;
config_name: string;
server_url: string;
tools?: CloudServerTool[];
}
export interface CloudServerTool {
name: string;
description: string;
inputSchema: Record<string, any>;
}
// Tool input schema types
export interface ToolInputSchema {
type: string;

View File

@@ -1,12 +1,9 @@
/** @type {import('tailwindcss').Config} */
export default {
content: [
"./index.html",
"./src/**/*.{js,ts,jsx,tsx}",
],
content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],
darkMode: 'class', // Use class strategy for dark mode
theme: {
extend: {},
},
plugins: [],
}
plugins: [require('@tailwindcss/line-clamp')],
};

View File

@@ -197,6 +197,7 @@
"settings": "Settings",
"changePassword": "Change Password",
"market": "Market",
"cloud": "Cloud Market",
"logs": "Logs"
},
"pages": {
@@ -228,7 +229,7 @@
"smartRouting": "Smart Routing"
},
"market": {
"title": "Server Market - (Data from mcpm.sh)"
"title": "Market Hub - Local and Cloud Markets"
},
"logs": {
"title": "System Logs"
@@ -281,7 +282,7 @@
"configureTools": "Configure Tools"
},
"market": {
"title": "Server Market",
"title": "Local Installation",
"official": "Official",
"by": "By",
"unknown": "Unknown",
@@ -324,6 +325,58 @@
"confirmVariablesMessage": "Please ensure these variables are properly defined in your runtime environment. Continue installing server?",
"confirmAndInstall": "Confirm and Install"
},
"cloud": {
"title": "Cloud Support",
"subtitle": "Powered by MCPRouter",
"by": "By",
"server": "Server",
"config": "Config",
"created": "Created",
"updated": "Updated",
"available": "Available",
"description": "Description",
"details": "Details",
"tools": "Tools",
"tool": "tool",
"toolsAvailable": "{{count}} tool available||{{count}} tools available",
"loadingTools": "Loading tools...",
"noTools": "No tools available for this server",
"noDescription": "No description available",
"viewDetails": "View Details",
"parameters": "Parameters",
"result": "Result",
"error": "Error",
"callTool": "Call",
"calling": "Calling...",
"toolCallSuccess": "Tool {{toolName}} executed successfully",
"toolCallError": "Failed to call tool {{toolName}}: {{error}}",
"viewSchema": "View Schema",
"backToList": "Back to Cloud Market",
"search": "Search",
"searchPlaceholder": "Search cloud servers by name, title, or author",
"clearFilters": "Clear Filters",
"clearCategoryFilter": "Clear",
"clearTagFilter": "Clear",
"categories": "Categories",
"tags": "Tags",
"noCategories": "No categories found",
"noTags": "No tags found",
"noServers": "No cloud servers found",
"fetchError": "Error fetching cloud servers",
"serverNotFound": "Cloud server not found",
"searchError": "Error searching cloud servers",
"filterError": "Error filtering cloud servers by category",
"tagFilterError": "Error filtering cloud servers by tag",
"showing": "Showing {{from}}-{{to}} of {{total}} cloud servers",
"perPage": "Per page",
"apiKeyNotConfigured": "MCPRouter API key not configured",
"apiKeyNotConfiguredDescription": "To use cloud servers, you need to configure your MCPRouter API key.",
"getApiKey": "Get API Key",
"configureInSettings": "Configure in Settings",
"installServer": "Install {{name}}",
"installSuccess": "Server {{name}} installed successfully",
"installError": "Failed to install server: {{error}}"
},
"tool": {
"run": "Run",
"running": "Running...",
@@ -394,7 +447,20 @@
"openaiApiEmbeddingModelPlaceholder": "text-embedding-3-small",
"smartRoutingConfigUpdated": "Smart routing configuration updated successfully",
"smartRoutingRequiredFields": "Database URL and OpenAI API Key are required to enable smart routing",
"smartRoutingValidationError": "Please fill in the required fields before enabling Smart Routing: {{fields}}"
"smartRoutingValidationError": "Please fill in the required fields before enabling Smart Routing: {{fields}}",
"mcpRouterConfig": "Cloud Market",
"mcpRouterApiKey": "MCPRouter API Key",
"mcpRouterApiKeyDescription": "API key for accessing MCPRouter cloud market services",
"mcpRouterApiKeyPlaceholder": "Enter MCPRouter API key",
"mcpRouterReferer": "Referer",
"mcpRouterRefererDescription": "Referer header for MCPRouter API requests",
"mcpRouterRefererPlaceholder": "https://mcphub.app",
"mcpRouterTitle": "Title",
"mcpRouterTitleDescription": "Title header for MCPRouter API requests",
"mcpRouterTitlePlaceholder": "MCPHub",
"mcpRouterBaseUrl": "Base URL",
"mcpRouterBaseUrlDescription": "Base URL for MCPRouter API",
"mcpRouterBaseUrlPlaceholder": "https://api.mcprouter.to/v1"
},
"dxt": {
"upload": "Upload",

View File

@@ -198,6 +198,7 @@
"groups": "分组",
"users": "用户",
"market": "市场",
"cloud": "云端市场",
"logs": "日志"
},
"pages": {
@@ -229,7 +230,7 @@
"title": "用户管理"
},
"market": {
"title": "服务器市场 - (数据来源于 mcpm.sh"
"title": "市场中心 - 本地市场和云端市场"
},
"logs": {
"title": "系统日志"
@@ -282,7 +283,7 @@
"configureTools": "配置工具"
},
"market": {
"title": "服务器市场",
"title": "本地安装",
"official": "官方",
"by": "作者",
"unknown": "未知",
@@ -314,7 +315,7 @@
"required": "必填",
"example": "示例",
"viewSchema": "查看结构",
"fetchError": "获取服务器市场数据失败",
"fetchError": "获取本地市场服务器数据失败",
"serverNotFound": "未找到服务器",
"searchError": "搜索服务器失败",
"filterError": "按分类筛选服务器失败",
@@ -325,6 +326,58 @@
"confirmVariablesMessage": "请确保这些变量在运行环境中已正确定义。是否继续安装服务器?",
"confirmAndInstall": "确认并安装"
},
"cloud": {
"title": "云端支持",
"subtitle": "由 MCPRouter 提供支持",
"by": "作者",
"server": "服务器",
"config": "配置",
"created": "创建时间",
"updated": "更新时间",
"available": "可用",
"description": "描述",
"details": "详细信息",
"tools": "工具",
"tool": "个工具",
"toolsAvailable": "{{count}} 个工具可用",
"loadingTools": "加载工具中...",
"noTools": "该服务器没有可用工具",
"noDescription": "无描述信息",
"viewDetails": "查看详情",
"parameters": "参数",
"result": "结果",
"error": "错误",
"callTool": "调用",
"calling": "调用中...",
"toolCallSuccess": "工具 {{toolName}} 执行成功",
"toolCallError": "调用工具 {{toolName}} 失败:{{error}}",
"viewSchema": "查看结构",
"backToList": "返回云端市场",
"search": "搜索",
"searchPlaceholder": "搜索云端服务器名称、标题或作者",
"clearFilters": "清除筛选",
"clearCategoryFilter": "清除",
"clearTagFilter": "清除",
"categories": "分类",
"tags": "标签",
"noCategories": "未找到分类",
"noTags": "未找到标签",
"noServers": "未找到云端服务器",
"fetchError": "获取云端服务器失败",
"serverNotFound": "未找到云端服务器",
"searchError": "搜索云端服务器失败",
"filterError": "按分类筛选云端服务器失败",
"tagFilterError": "按标签筛选云端服务器失败",
"showing": "显示 {{from}}-{{to}}/{{total}} 个云端服务器",
"perPage": "每页显示",
"apiKeyNotConfigured": "MCPRouter API 密钥未配置",
"apiKeyNotConfiguredDescription": "要使用云端服务器,您需要配置 MCPRouter API 密钥。",
"getApiKey": "获取 API 密钥",
"configureInSettings": "在设置中配置",
"installServer": "安装 {{name}}",
"installSuccess": "服务器 {{name}} 安装成功",
"installError": "安装服务器失败:{{error}}"
},
"tool": {
"run": "运行",
"running": "运行中...",
@@ -396,7 +449,20 @@
"openaiApiEmbeddingModelPlaceholder": "text-embedding-3-small",
"smartRoutingConfigUpdated": "智能路由配置更新成功",
"smartRoutingRequiredFields": "启用智能路由需要填写数据库连接地址和 OpenAI API 密钥",
"smartRoutingValidationError": "启用智能路由前请先填写必要字段:{{fields}}"
"smartRoutingValidationError": "启用智能路由前请先填写必要字段:{{fields}}",
"mcpRouterConfig": "云端市场",
"mcpRouterApiKey": "MCPRouter API 密钥",
"mcpRouterApiKeyDescription": "用于访问 MCPRouter 云端市场服务的 API 密钥",
"mcpRouterApiKeyPlaceholder": "请输入 MCPRouter API 密钥",
"mcpRouterReferer": "引用地址",
"mcpRouterRefererDescription": "MCPRouter API 请求的引用地址头",
"mcpRouterRefererPlaceholder": "https://mcphub.app",
"mcpRouterTitle": "标题",
"mcpRouterTitleDescription": "MCPRouter API 请求的标题头",
"mcpRouterTitlePlaceholder": "MCPHub",
"mcpRouterBaseUrl": "基础地址",
"mcpRouterBaseUrlDescription": "MCPRouter API 的基础地址",
"mcpRouterBaseUrlPlaceholder": "https://api.mcprouter.to/v1"
},
"dxt": {
"upload": "上传",

View File

@@ -75,6 +75,7 @@
"@shadcn/ui": "^0.0.4",
"@swc/core": "^1.13.0",
"@swc/jest": "^0.2.39",
"@tailwindcss/line-clamp": "^0.4.4",
"@tailwindcss/postcss": "^4.1.3",
"@tailwindcss/vite": "^4.1.7",
"@types/bcryptjs": "^3.0.0",

12
pnpm-lock.yaml generated
View File

@@ -93,6 +93,9 @@ importers:
'@swc/jest':
specifier: ^0.2.39
version: 0.2.39(@swc/core@1.13.0)
'@tailwindcss/line-clamp':
specifier: ^0.4.4
version: 0.4.4(tailwindcss@4.1.11)
'@tailwindcss/postcss':
specifier: ^4.1.3
version: 4.1.11
@@ -1445,6 +1448,11 @@ packages:
'@swc/types@0.1.23':
resolution: {integrity: sha512-u1iIVZV9Q0jxY+yM2vw/hZGDNudsN85bBpTqzAQ9rzkxW9D+e3aEM4Han+ow518gSewkXgjmEK0BD79ZcNVgPw==}
'@tailwindcss/line-clamp@0.4.4':
resolution: {integrity: sha512-5U6SY5z8N42VtrCrKlsTAA35gy2VSyYtHWCsg1H87NU1SXnEfekTVlrga9fzUDrrHcGi2Lb5KenUWb4lRQT5/g==}
peerDependencies:
tailwindcss: '>=2.0.0 || >=3.0.0 || >=3.0.0-alpha.1'
'@tailwindcss/node@4.1.11':
resolution: {integrity: sha512-yzhzuGRmv5QyU9qLNg4GTlYI6STedBWRE7NjxP45CsFYYq9taI0zJXZBMqIC/c8fViNLhmrbpSFS57EoxUmD6Q==}
@@ -5486,6 +5494,10 @@ snapshots:
dependencies:
'@swc/counter': 0.1.3
'@tailwindcss/line-clamp@0.4.4(tailwindcss@4.1.11)':
dependencies:
tailwindcss: 4.1.11
'@tailwindcss/node@4.1.11':
dependencies:
'@ampproject/remapping': 2.3.0

View File

@@ -0,0 +1,273 @@
import { Request, Response } from 'express';
import { ApiResponse, CloudServer, CloudTool } from '../types/index.js';
import {
getCloudServers,
getCloudServerByName,
getCloudServerTools,
callCloudServerTool,
getCloudCategories,
getCloudTags,
searchCloudServers,
filterCloudServersByCategory,
filterCloudServersByTag,
} from '../services/cloudService.js';
// Get all cloud market servers
export const getAllCloudServers = async (_: Request, res: Response): Promise<void> => {
try {
const cloudServers = await getCloudServers();
const response: ApiResponse<CloudServer[]> = {
success: true,
data: cloudServers,
};
res.json(response);
} catch (error) {
console.error('Error getting cloud market servers:', error);
const errorMessage =
error instanceof Error ? error.message : 'Failed to get cloud market servers';
res.status(500).json({
success: false,
message: errorMessage,
});
}
};
// Get a specific cloud market server by name
export const getCloudServer = async (req: Request, res: Response): Promise<void> => {
try {
const { name } = req.params;
if (!name) {
res.status(400).json({
success: false,
message: 'Server name is required',
});
return;
}
const server = await getCloudServerByName(name);
if (!server) {
res.status(404).json({
success: false,
message: 'Cloud server not found',
});
return;
}
// Fetch tools for this server
try {
const tools = await getCloudServerTools(server.server_key);
server.tools = tools;
} catch (toolError) {
console.warn(`Failed to fetch tools for server ${server.name}:`, toolError);
// Continue without tools
}
const response: ApiResponse<CloudServer> = {
success: true,
data: server,
};
res.json(response);
} catch (error) {
console.error('Error getting cloud market server:', error);
const errorMessage =
error instanceof Error ? error.message : 'Failed to get cloud market server';
res.status(500).json({
success: false,
message: errorMessage,
});
}
};
// Get all cloud market categories
export const getAllCloudCategories = async (_: Request, res: Response): Promise<void> => {
try {
const categories = await getCloudCategories();
const response: ApiResponse<string[]> = {
success: true,
data: categories,
};
res.json(response);
} catch (error) {
console.error('Error getting cloud market categories:', error);
const errorMessage =
error instanceof Error ? error.message : 'Failed to get cloud market categories';
res.status(500).json({
success: false,
message: errorMessage,
});
}
};
// Get all cloud market tags
export const getAllCloudTags = async (_: Request, res: Response): Promise<void> => {
try {
const tags = await getCloudTags();
const response: ApiResponse<string[]> = {
success: true,
data: tags,
};
res.json(response);
} catch (error) {
console.error('Error getting cloud market tags:', error);
const errorMessage = error instanceof Error ? error.message : 'Failed to get cloud market tags';
res.status(500).json({
success: false,
message: errorMessage,
});
}
};
// Search cloud market servers
export const searchCloudServersByQuery = async (req: Request, res: Response): Promise<void> => {
try {
const query = req.query.query as string;
if (!query || query.trim() === '') {
res.status(400).json({
success: false,
message: 'Search query is required',
});
return;
}
const servers = await searchCloudServers(query);
const response: ApiResponse<CloudServer[]> = {
success: true,
data: servers,
};
res.json(response);
} catch (error) {
console.error('Error searching cloud market servers:', error);
const errorMessage =
error instanceof Error ? error.message : 'Failed to search cloud market servers';
res.status(500).json({
success: false,
message: errorMessage,
});
}
};
// Get cloud market servers by category
export const getCloudServersByCategory = async (req: Request, res: Response): Promise<void> => {
try {
const { category } = req.params;
if (!category) {
res.status(400).json({
success: false,
message: 'Category is required',
});
return;
}
const servers = await filterCloudServersByCategory(category);
const response: ApiResponse<CloudServer[]> = {
success: true,
data: servers,
};
res.json(response);
} catch (error) {
console.error('Error getting cloud market servers by category:', error);
const errorMessage =
error instanceof Error ? error.message : 'Failed to get cloud market servers by category';
res.status(500).json({
success: false,
message: errorMessage,
});
}
};
// Get cloud market servers by tag
export const getCloudServersByTag = async (req: Request, res: Response): Promise<void> => {
try {
const { tag } = req.params;
if (!tag) {
res.status(400).json({
success: false,
message: 'Tag is required',
});
return;
}
const servers = await filterCloudServersByTag(tag);
const response: ApiResponse<CloudServer[]> = {
success: true,
data: servers,
};
res.json(response);
} catch (error) {
console.error('Error getting cloud market servers by tag:', error);
const errorMessage =
error instanceof Error ? error.message : 'Failed to get cloud market servers by tag';
res.status(500).json({
success: false,
message: errorMessage,
});
}
};
// Get tools for a specific cloud server
export const getCloudServerToolsList = async (req: Request, res: Response): Promise<void> => {
try {
const { serverName } = req.params;
if (!serverName) {
res.status(400).json({
success: false,
message: 'Server name is required',
});
return;
}
const tools = await getCloudServerTools(serverName);
const response: ApiResponse<CloudTool[]> = {
success: true,
data: tools,
};
res.json(response);
} catch (error) {
console.error('Error getting cloud server tools:', error);
const errorMessage =
error instanceof Error ? error.message : 'Failed to get cloud server tools';
res.status(500).json({
success: false,
message: errorMessage,
});
}
};
// Call a tool on a cloud server
export const callCloudTool = async (req: Request, res: Response): Promise<void> => {
try {
const { serverName, toolName } = req.params;
const { arguments: args } = req.body;
if (!serverName) {
res.status(400).json({
success: false,
message: 'Server name is required',
});
return;
}
if (!toolName) {
res.status(400).json({
success: false,
message: 'Tool name is required',
});
return;
}
const result = await callCloudServerTool(serverName, toolName, args || {});
const response: ApiResponse = {
success: true,
data: result,
};
res.json(response);
} catch (error) {
console.error('Error calling cloud server tool:', error);
const errorMessage =
error instanceof Error ? error.message : 'Failed to call cloud server tool';
res.status(500).json({
success: false,
message: errorMessage,
});
}
};

View File

@@ -0,0 +1,33 @@
import { Request, Response } from 'express';
import { connected } from '../services/mcpService.js';
/**
* Health check endpoint
* Returns 200 OK when all MCPs are loaded and connected
* Returns 503 Service Unavailable when MCPs are not ready
*/
export const healthCheck = (_req: Request, res: Response): void => {
try {
const allConnected = connected();
if (allConnected) {
res.status(200).json({
status: 'healthy',
message: 'All enabled MCP servers are ready',
timestamp: new Date().toISOString(),
});
} else {
res.status(503).json({
status: 'unhealthy',
message: 'Not all enabled MCP servers are ready',
timestamp: new Date().toISOString(),
});
}
} catch (error) {
console.error('Health check error:', error);
res.status(500).json({
status: 'error',
message: 'Internal server error during health check',
timestamp: new Date().toISOString(),
});
}
};

View File

@@ -280,7 +280,7 @@ export const updateServer = async (req: Request, res: Response): Promise<void> =
const result = await addOrUpdateServer(name, config, true); // Allow override for updates
if (result.success) {
notifyToolChanged();
notifyToolChanged(name);
res.json({
success: true,
message: 'Server updated successfully',
@@ -505,7 +505,7 @@ export const updateToolDescription = async (req: Request, res: Response): Promis
export const updateSystemConfig = (req: Request, res: Response): void => {
try {
const { routing, install, smartRouting } = req.body;
const { routing, install, smartRouting, mcpRouter } = req.body;
const currentUser = (req as any).user;
if (
@@ -524,7 +524,12 @@ export const updateSystemConfig = (req: Request, res: Response): void => {
typeof smartRouting.dbUrl !== 'string' &&
typeof smartRouting.openaiApiBaseUrl !== 'string' &&
typeof smartRouting.openaiApiKey !== 'string' &&
typeof smartRouting.openaiApiEmbeddingModel !== 'string'))
typeof smartRouting.openaiApiEmbeddingModel !== 'string')) &&
(!mcpRouter ||
(typeof mcpRouter.apiKey !== 'string' &&
typeof mcpRouter.referer !== 'string' &&
typeof mcpRouter.title !== 'string' &&
typeof mcpRouter.baseUrl !== 'string'))
) {
res.status(400).json({
success: false,
@@ -555,6 +560,12 @@ export const updateSystemConfig = (req: Request, res: Response): void => {
openaiApiKey: '',
openaiApiEmbeddingModel: '',
},
mcpRouter: {
apiKey: '',
referer: 'https://mcphub.app',
title: 'MCPHub',
baseUrl: 'https://api.mcprouter.to/v1',
},
};
}
@@ -586,6 +597,15 @@ export const updateSystemConfig = (req: Request, res: Response): void => {
};
}
if (!settings.systemConfig.mcpRouter) {
settings.systemConfig.mcpRouter = {
apiKey: '',
referer: 'https://mcphub.app',
title: 'MCPHub',
baseUrl: 'https://api.mcprouter.to/v1',
};
}
if (routing) {
if (typeof routing.enableGlobalRoute === 'boolean') {
settings.systemConfig.routing.enableGlobalRoute = routing.enableGlobalRoute;
@@ -676,6 +696,21 @@ export const updateSystemConfig = (req: Request, res: Response): void => {
needsSync = (!wasSmartRoutingEnabled && isNowEnabled) || (isNowEnabled && hasConfigChanged);
}
if (mcpRouter) {
if (typeof mcpRouter.apiKey === 'string') {
settings.systemConfig.mcpRouter.apiKey = mcpRouter.apiKey;
}
if (typeof mcpRouter.referer === 'string') {
settings.systemConfig.mcpRouter.referer = mcpRouter.referer;
}
if (typeof mcpRouter.title === 'string') {
settings.systemConfig.mcpRouter.title = mcpRouter.title;
}
if (typeof mcpRouter.baseUrl === 'string') {
settings.systemConfig.mcpRouter.baseUrl = mcpRouter.baseUrl;
}
}
if (saveSettings(settings, currentUser)) {
res.json({
success: true,

View File

@@ -53,8 +53,8 @@ export const initMiddlewares = (app: express.Application): void => {
// Protect API routes with authentication middleware, but exclude auth endpoints
app.use(`${config.basePath}/api`, (req, res, next) => {
// Skip authentication for login and register endpoints
if (req.path === '/auth/login' || req.path === '/auth/register') {
// Skip authentication for login endpoint
if (req.path === '/auth/login') {
next();
} else {
// Apply authentication middleware first

View File

@@ -43,16 +43,31 @@ import {
getMarketServersByCategory,
getMarketServersByTag,
} from '../controllers/marketController.js';
import {
getAllCloudServers,
getCloudServer,
getAllCloudCategories,
getAllCloudTags,
searchCloudServersByQuery,
getCloudServersByCategory,
getCloudServersByTag,
getCloudServerToolsList,
callCloudTool,
} from '../controllers/cloudController.js';
import { login, register, getCurrentUser, changePassword } from '../controllers/authController.js';
import { getAllLogs, clearLogs, streamLogs } from '../controllers/logController.js';
import { getRuntimeConfig, getPublicConfig } from '../controllers/configController.js';
import { callTool } from '../controllers/toolController.js';
import { uploadDxtFile, uploadMiddleware } from '../controllers/dxtController.js';
import { healthCheck } from '../controllers/healthController.js';
import { auth } from '../middlewares/auth.js';
const router = express.Router();
export const initRoutes = (app: express.Application): void => {
// Health check endpoint (no auth required, accessible at /health)
app.get('/health', healthCheck);
// API routes protected by auth middleware in middlewares/index.ts
router.get('/servers', getAllServers);
router.get('/settings', getAllSettings);
@@ -103,6 +118,17 @@ export const initRoutes = (app: express.Application): void => {
router.get('/market/tags', getAllMarketTags);
router.get('/market/tags/:tag', getMarketServersByTag);
// Cloud Market routes
router.get('/cloud/servers', getAllCloudServers);
router.get('/cloud/servers/search', searchCloudServersByQuery);
router.get('/cloud/servers/:name', getCloudServer);
router.get('/cloud/categories', getAllCloudCategories);
router.get('/cloud/categories/:category', getCloudServersByCategory);
router.get('/cloud/tags', getAllCloudTags);
router.get('/cloud/tags/:tag', getCloudServersByTag);
router.get('/cloud/servers/:serverName/tools', getCloudServerToolsList);
router.post('/cloud/servers/:serverName/tools/:toolName/call', callCloudTool);
// Log routes
router.get('/logs', getAllLogs);
router.delete('/logs', clearLogs);

View File

@@ -0,0 +1,273 @@
import axios, { AxiosRequestConfig } from 'axios';
import {
CloudServer,
CloudTool,
MCPRouterResponse,
MCPRouterListServersResponse,
MCPRouterListToolsResponse,
MCPRouterCallToolResponse,
} from '../types/index.js';
import { loadOriginalSettings } from '../config/index.js';
// MCPRouter API default base URL
const DEFAULT_MCPROUTER_API_BASE = 'https://api.mcprouter.to/v1';
// Get MCPRouter API config from system configuration
const getMCPRouterConfig = () => {
const settings = loadOriginalSettings();
const mcpRouterConfig = settings.systemConfig?.mcpRouter;
return {
apiKey: mcpRouterConfig?.apiKey || process.env.MCPROUTER_API_KEY || '',
referer: mcpRouterConfig?.referer || process.env.MCPROUTER_REFERER || 'https://mcphub.app',
title: mcpRouterConfig?.title || process.env.MCPROUTER_TITLE || 'MCPHub',
baseUrl:
mcpRouterConfig?.baseUrl || process.env.MCPROUTER_API_BASE || DEFAULT_MCPROUTER_API_BASE,
};
};
// Get axios config with MCPRouter headers
const getAxiosConfig = (): AxiosRequestConfig => {
const mcpRouterConfig = getMCPRouterConfig();
return {
headers: {
Authorization: mcpRouterConfig.apiKey ? `Bearer ${mcpRouterConfig.apiKey}` : '',
'HTTP-Referer': mcpRouterConfig.referer || 'https://mcphub.app',
'X-Title': mcpRouterConfig.title || 'MCPHub',
'Content-Type': 'application/json',
},
};
};
// List all available cloud servers
export const getCloudServers = async (): Promise<CloudServer[]> => {
try {
const axiosConfig = getAxiosConfig();
const mcpRouterConfig = getMCPRouterConfig();
const response = await axios.post<MCPRouterResponse<MCPRouterListServersResponse>>(
`${mcpRouterConfig.baseUrl}/list-servers`,
{},
axiosConfig,
);
const data = response.data;
if (data.code !== 0) {
throw new Error(data.message || 'Failed to fetch servers');
}
return data.data.servers || [];
} catch (error) {
console.error('Error fetching cloud market servers:', error);
throw error;
}
};
// Get a specific cloud server by name
export const getCloudServerByName = async (name: string): Promise<CloudServer | null> => {
try {
const servers = await getCloudServers();
return servers.find((server) => server.name === name || server.config_name === name) || null;
} catch (error) {
console.error(`Error fetching cloud server ${name}:`, error);
throw error;
}
};
// List tools for a specific cloud server
export const getCloudServerTools = async (serverKey: string): Promise<CloudTool[]> => {
try {
const axiosConfig = getAxiosConfig();
const mcpRouterConfig = getMCPRouterConfig();
if (
!axiosConfig.headers?.['Authorization'] ||
axiosConfig.headers['Authorization'] === 'Bearer '
) {
throw new Error('MCPROUTER_API_KEY_NOT_CONFIGURED');
}
const response = await axios.post<MCPRouterResponse<MCPRouterListToolsResponse>>(
`${mcpRouterConfig.baseUrl}/list-tools`,
{
server: serverKey,
},
axiosConfig,
);
const data = response.data;
if (data.code !== 0) {
throw new Error(data.message || 'Failed to fetch tools');
}
return data.data.tools || [];
} catch (error) {
console.error(`Error fetching tools for server ${serverKey}:`, error);
throw error;
}
};
// Call a tool on a cloud server
export const callCloudServerTool = async (
serverName: string,
toolName: string,
args: Record<string, any>,
): Promise<MCPRouterCallToolResponse> => {
try {
const axiosConfig = getAxiosConfig();
const mcpRouterConfig = getMCPRouterConfig();
if (
!axiosConfig.headers?.['Authorization'] ||
axiosConfig.headers['Authorization'] === 'Bearer '
) {
throw new Error('MCPROUTER_API_KEY_NOT_CONFIGURED');
}
const response = await axios.post<MCPRouterResponse<MCPRouterCallToolResponse>>(
`${mcpRouterConfig.baseUrl}/call-tool`,
{
server: serverName,
name: toolName,
arguments: args,
},
axiosConfig,
);
const data = response.data;
if (data.code !== 0) {
throw new Error(data.message || 'Failed to call tool');
}
return data.data;
} catch (error) {
console.error(`Error calling tool ${toolName} on server ${serverName}:`, error);
throw error;
}
};
// Get all categories from cloud servers
export const getCloudCategories = async (): Promise<string[]> => {
try {
const servers = await getCloudServers();
const categories = new Set<string>();
servers.forEach((server) => {
// Extract categories from content or description
// This is a simple implementation, you might want to parse the content more sophisticatedly
if (server.content) {
const categoryMatches = server.content.match(/category[:\s]*([^,\n]+)/gi);
if (categoryMatches) {
categoryMatches.forEach((match) => {
const category = match.replace(/category[:\s]*/i, '').trim();
if (category) categories.add(category);
});
}
}
});
return Array.from(categories).sort();
} catch (error) {
console.error('Error fetching cloud market categories:', error);
throw error;
}
};
// Get all tags from cloud servers
export const getCloudTags = async (): Promise<string[]> => {
try {
const servers = await getCloudServers();
const tags = new Set<string>();
servers.forEach((server) => {
// Extract tags from content or description
if (server.content) {
const tagMatches = server.content.match(/tag[s]?[:\s]*([^,\n]+)/gi);
if (tagMatches) {
tagMatches.forEach((match) => {
const tag = match.replace(/tag[s]?[:\s]*/i, '').trim();
if (tag) tags.add(tag);
});
}
}
});
return Array.from(tags).sort();
} catch (error) {
console.error('Error fetching cloud market tags:', error);
throw error;
}
};
// Search cloud servers by query
export const searchCloudServers = async (query: string): Promise<CloudServer[]> => {
try {
const servers = await getCloudServers();
const searchTerms = query
.toLowerCase()
.split(' ')
.filter((term) => term.length > 0);
if (searchTerms.length === 0) {
return servers;
}
return servers.filter((server) => {
const searchText = [
server.name,
server.title,
server.description,
server.content,
server.author_name,
]
.join(' ')
.toLowerCase();
return searchTerms.some((term) => searchText.includes(term));
});
} catch (error) {
console.error('Error searching cloud market servers:', error);
throw error;
}
};
// Filter cloud servers by category
export const filterCloudServersByCategory = async (category: string): Promise<CloudServer[]> => {
try {
const servers = await getCloudServers();
if (!category) {
return servers;
}
return servers.filter((server) => {
const content = (server.content || '').toLowerCase();
return content.includes(category.toLowerCase());
});
} catch (error) {
console.error('Error filtering cloud market servers by category:', error);
throw error;
}
};
// Filter cloud servers by tag
export const filterCloudServersByTag = async (tag: string): Promise<CloudServer[]> => {
try {
const servers = await getCloudServers();
if (!tag) {
return servers;
}
return servers.filter((server) => {
const content = (server.content || '').toLowerCase();
return content.includes(tag.toLowerCase());
});
} catch (error) {
console.error('Error filtering cloud market servers by tag:', error);
throw error;
}
};

View File

@@ -70,8 +70,8 @@ export const deleteMcpServer = (sessionId: string): void => {
delete servers[sessionId];
};
export const notifyToolChanged = async () => {
await registerAllTools(false);
export const notifyToolChanged = async (name?: string) => {
await registerAllTools(false, name);
Object.values(servers).forEach((server) => {
server
.sendToolListChanged()
@@ -114,9 +114,11 @@ const cleanInputSchema = (schema: any): any => {
// Store all server information
let serverInfos: ServerInfo[] = [];
// Returns true if all servers are connected
// Returns true if all enabled servers are connected
export const connected = (): boolean => {
return serverInfos.every((serverInfo) => serverInfo.status === 'connected');
return serverInfos
.filter((serverInfo) => serverInfo.enabled !== false)
.every((serverInfo) => serverInfo.status === 'connected');
};
// Global cleanup function to close all connections
@@ -323,7 +325,10 @@ const callToolWithReconnect = async (
};
// Initialize MCP server clients
export const initializeClientsFromSettings = async (isInit: boolean): Promise<ServerInfo[]> => {
export const initializeClientsFromSettings = async (
isInit: boolean,
serverName?: string,
): Promise<ServerInfo[]> => {
const settings = loadSettings();
const existingServerInfos = serverInfos;
serverInfos = [];
@@ -348,7 +353,7 @@ export const initializeClientsFromSettings = async (isInit: boolean): Promise<Se
const existingServer = existingServerInfos.find(
(s) => s.name === name && s.status === 'connected',
);
if (existingServer) {
if (existingServer && (!serverName || serverName !== name)) {
serverInfos.push({
...existingServer,
enabled: conf.enabled === undefined ? true : conf.enabled,
@@ -359,7 +364,6 @@ export const initializeClientsFromSettings = async (isInit: boolean): Promise<Se
let transport;
let openApiClient;
if (conf.type === 'openapi') {
// Handle OpenAPI type servers
if (!conf.openapi?.url && !conf.openapi?.schema) {
@@ -517,8 +521,8 @@ export const initializeClientsFromSettings = async (isInit: boolean): Promise<Se
};
// Register all MCP tools
export const registerAllTools = async (isInit: boolean): Promise<void> => {
await initializeClientsFromSettings(isInit);
export const registerAllTools = async (isInit: boolean, serverName?: string): Promise<void> => {
await initializeClientsFromSettings(isInit, serverName);
};
// Get all server information

View File

@@ -81,6 +81,49 @@ export interface MarketServer {
is_official?: boolean;
}
// Cloud Market Server types (for MCPRouter API)
export interface CloudServer {
created_at: string;
updated_at: string;
name: string;
author_name: string;
title: string;
description: string;
content: string;
server_key: string;
config_name: string;
tools?: CloudTool[];
}
export interface CloudTool {
name: string;
description: string;
inputSchema: Record<string, any>;
}
// MCPRouter API Response types
export interface MCPRouterResponse<T = any> {
code: number;
message: string;
data: T;
}
export interface MCPRouterListServersResponse {
servers: CloudServer[];
}
export interface MCPRouterListToolsResponse {
tools: CloudTool[];
}
export interface MCPRouterCallToolResponse {
content: Array<{
type: string;
text: string;
}>;
isError: boolean;
}
export interface SystemConfig {
routing?: {
enableGlobalRoute?: boolean; // Controls whether the /sse endpoint without group is enabled
@@ -95,6 +138,12 @@ export interface SystemConfig {
baseUrl?: string; // Base URL for group card copy operations
};
smartRouting?: SmartRoutingConfig;
mcpRouter?: {
apiKey?: string; // MCPRouter API key for authentication
referer?: string; // Referer header for MCPRouter API requests
title?: string; // Title header for MCPRouter API requests
baseUrl?: string; // Base URL for MCPRouter API (default: https://api.mcprouter.to/v1)
};
}
export interface UserConfig {