mirror of
https://github.com/samanhappy/mcphub.git
synced 2025-12-24 02:39:19 -05:00
Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
604fe4f71d | ||
|
|
907bca8aac | ||
|
|
8c58200dcc | ||
|
|
0b4dc453a5 | ||
|
|
35012f99fc | ||
|
|
22ad4f83f6 | ||
|
|
26720d9e49 |
@@ -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.
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 |
@@ -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
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
570
docs/zh/installation.mdx
Normal 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>
|
||||
@@ -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) 获取支持!
|
||||
|
||||
@@ -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>
|
||||
|
||||
144
frontend/src/components/CloudServerCard.tsx
Normal file
144
frontend/src/components/CloudServerCard.tsx
Normal 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;
|
||||
573
frontend/src/components/CloudServerDetail.tsx
Normal file
573
frontend/src/components/CloudServerDetail.tsx
Normal 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;
|
||||
92
frontend/src/components/MCPRouterApiKeyError.tsx
Normal file
92
frontend/src/components/MCPRouterApiKeyError.tsx
Normal 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;
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
|
||||
350
frontend/src/hooks/useCloudData.ts
Normal file
350
frontend/src/hooks/useCloudData.ts
Normal 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,
|
||||
};
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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 */
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -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')}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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')],
|
||||
};
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "上传",
|
||||
|
||||
@@ -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
12
pnpm-lock.yaml
generated
@@ -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
|
||||
|
||||
273
src/controllers/cloudController.ts
Normal file
273
src/controllers/cloudController.ts
Normal 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,
|
||||
});
|
||||
}
|
||||
};
|
||||
33
src/controllers/healthController.ts
Normal file
33
src/controllers/healthController.ts
Normal 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(),
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
273
src/services/cloudService.ts
Normal file
273
src/services/cloudService.ts
Normal 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;
|
||||
}
|
||||
};
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user