Compare commits
73 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a6cea2ad3f | ||
|
|
5bb2715094 | ||
|
|
9b40f7e101 | ||
|
|
df872823c1 | ||
|
|
9304653c34 | ||
|
|
b5685b7010 | ||
|
|
89c37b2f02 | ||
|
|
c316cb896e | ||
|
|
bc3c8facfa | ||
|
|
69afb865c0 | ||
|
|
ba30d88840 | ||
|
|
6d0d622bd8 | ||
|
|
ab50c7e9eb | ||
|
|
e507bea2e3 | ||
|
|
0f00ad7200 | ||
|
|
b0b0c93337 | ||
|
|
20fd355b87 | ||
|
|
4388084704 | ||
|
|
fe2535461d | ||
|
|
985598e529 | ||
|
|
b2b6d0588b | ||
|
|
64628ee3ed | ||
|
|
66d4142039 | ||
|
|
cf72295f99 | ||
|
|
89f85c73ff | ||
|
|
adabf1d92b | ||
|
|
c3a6dfadb4 | ||
|
|
d119be0f82 | ||
|
|
1e308ec4c5 | ||
|
|
1bd4fd6d9c | ||
|
|
4b3bb26301 | ||
|
|
40af398f68 | ||
|
|
4726f00a22 | ||
|
|
77f64b7b98 | ||
|
|
d9cbc5381a | ||
|
|
56c6447469 | ||
|
|
f8149c4b0b | ||
|
|
e259f30539 | ||
|
|
3a421bc476 | ||
|
|
503b60edb7 | ||
|
|
4039a85ee1 | ||
|
|
3a83b83a9e | ||
|
|
c1621805de | ||
|
|
ecf79eaef3 | ||
|
|
e2cb5fa467 | ||
|
|
7193ca4cdc | ||
|
|
9675cd8533 | ||
|
|
65c95aaa0b | ||
|
|
d2bbadea83 | ||
|
|
394945bbc5 | ||
|
|
bfc184afc6 | ||
|
|
1acacfab88 | ||
|
|
63c55875b5 | ||
|
|
80c6aae116 | ||
|
|
e2c5cc8ed1 | ||
|
|
b0a65cc6d0 | ||
|
|
e019c9e8b3 | ||
|
|
39b69bf550 | ||
|
|
a1047321d1 | ||
|
|
268ce5cce6 | ||
|
|
37bb3414c8 | ||
|
|
c3e1fa1eee | ||
|
|
edfbdb7123 | ||
|
|
9e5c2b5525 | ||
|
|
27b7e766af | ||
|
|
9c55f63bb1 | ||
|
|
b1674cc630 | ||
|
|
3570232ccc | ||
|
|
f50bb08816 | ||
|
|
721674e7eb | ||
|
|
1337eeed84 | ||
|
|
b14be9955b | ||
|
|
da45d49a5e |
16
.coveragerc
Normal file
@@ -0,0 +1,16 @@
|
||||
# Test coverage configuration
|
||||
# This file tells Jest what to include/exclude from coverage reports
|
||||
|
||||
# Coverage patterns
|
||||
- "src/**/*.{ts,tsx}"
|
||||
|
||||
# Exclusions
|
||||
- "!src/**/*.d.ts"
|
||||
- "!src/index.ts"
|
||||
- "!src/**/__tests__/**"
|
||||
- "!src/**/*.test.{ts,tsx}"
|
||||
- "!src/**/*.spec.{ts,tsx}"
|
||||
- "!**/node_modules/**"
|
||||
- "!coverage/**"
|
||||
- "!dist/**"
|
||||
- "!build/**"
|
||||
@@ -20,6 +20,6 @@
|
||||
}
|
||||
],
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
"no-undef": "off",
|
||||
"no-undef": "off"
|
||||
}
|
||||
}
|
||||
|
||||
50
.github/copilot-instructions.md
vendored
Normal file
@@ -0,0 +1,50 @@
|
||||
# MCPHub Coding Instructions
|
||||
|
||||
## Project Overview
|
||||
|
||||
MCPHub is a TypeScript/Node.js MCP server management hub that provides unified access through HTTP endpoints.
|
||||
|
||||
**Core Components:**
|
||||
|
||||
- **Backend**: Express.js + TypeScript + ESM (`src/server.ts`)
|
||||
- **Frontend**: React/Vite + Tailwind CSS (`frontend/`)
|
||||
- **MCP Integration**: Connects multiple MCP servers (`src/services/mcpService.ts`)
|
||||
|
||||
## Development Environment
|
||||
|
||||
```bash
|
||||
pnpm install
|
||||
pnpm dev # Start both backend and frontend
|
||||
pnpm backend:dev # Backend only
|
||||
pnpm frontend:dev # Frontend only
|
||||
```
|
||||
|
||||
## Project Conventions
|
||||
|
||||
### File Structure
|
||||
|
||||
- `src/services/` - Core business logic
|
||||
- `src/controllers/` - HTTP request handlers
|
||||
- `src/types/index.ts` - TypeScript type definitions
|
||||
- `src/config/index.ts` - Configuration management
|
||||
|
||||
### Key Notes
|
||||
|
||||
- Use ESM modules: Import with `.js` extensions, not `.ts`
|
||||
- Configuration file: `mcp_settings.json`
|
||||
- Endpoint formats: `/mcp/{group|server}` and `/mcp/$smart`
|
||||
- All code comments must be written in English
|
||||
- Frontend uses i18n with resource files in `locales/` folder
|
||||
- Server-side code should use appropriate abstraction layers for extensibility and replaceability
|
||||
|
||||
## Development Process
|
||||
|
||||
- For complex features, implement step by step and wait for confirmation before proceeding to the next step
|
||||
- After implementing features, no separate summary documentation is needed - update README.md and README.zh.md as appropriate
|
||||
|
||||
### Development Entry Points
|
||||
|
||||
- **MCP Servers**: Modify `src/services/mcpService.ts`
|
||||
- **API Endpoints**: Add routes in `src/routes/`, controllers in `src/controllers/`
|
||||
- **Frontend Features**: Start from `frontend/src/pages/`
|
||||
- **Testing**: Follow existing patterns in `tests/`
|
||||
11
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
# To get started with Dependabot version updates, you'll need to specify which
|
||||
# package ecosystems to update and where the package manifests are located.
|
||||
# Please see the documentation for all configuration options:
|
||||
# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
|
||||
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "npm" # See documentation for possible values
|
||||
directory: "/" # Location of package manifests
|
||||
schedule:
|
||||
interval: "monthly"
|
||||
17
.github/workflows/build.yml
vendored
@@ -8,6 +8,9 @@ on:
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
strategy:
|
||||
matrix:
|
||||
variant: ${{ startsWith(github.ref, 'refs/tags/') && fromJSON('["base", "full"]') || fromJSON('["base"]') }}
|
||||
@@ -30,16 +33,27 @@ jobs:
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to Docker Hub
|
||||
if: endsWith(github.repository, 'mcphub')
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
if: endsWith(github.repository, 'mcphubx')
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Docker metadata
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: samanhappy/mcphub
|
||||
images: |
|
||||
${{ endsWith(github.repository, 'mcphub') && github.repository || '' }}
|
||||
${{ endsWith(github.repository, 'mcphubx') && format('ghcr.io/{0}', github.repository) || '' }}
|
||||
tags: |
|
||||
type=raw,value=edge${{ matrix.variant == 'full' && '-full' || '' }},enable=${{ github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' }}
|
||||
type=semver,pattern={{version}}${{ matrix.variant == 'full' && '-full' || '' }},enable=${{ startsWith(github.ref, 'refs/tags/') }}
|
||||
@@ -48,6 +62,7 @@ jobs:
|
||||
latest=false
|
||||
|
||||
- name: Build and Push Docker Image
|
||||
if: endsWith(github.repository, 'mcphub') || endsWith(github.repository, 'mcphubx')
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
|
||||
112
.github/workflows/ci.yml
vendored
Normal file
@@ -0,0 +1,112 @@
|
||||
name: CI/CD Pipeline
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main, develop]
|
||||
pull_request:
|
||||
branches: [main, develop]
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [20.x]
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
|
||||
- name: Enable Corepack
|
||||
run: corepack enable
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Run linter
|
||||
run: pnpm lint
|
||||
|
||||
- name: Run type checking
|
||||
run: pnpm backend:build
|
||||
|
||||
- name: Run tests
|
||||
run: pnpm test:ci
|
||||
|
||||
- name: Upload coverage to Codecov
|
||||
uses: codecov/codecov-action@v4
|
||||
with:
|
||||
file: ./coverage/lcov.info
|
||||
flags: unittests
|
||||
name: codecov-umbrella
|
||||
|
||||
# build:
|
||||
# runs-on: ubuntu-latest
|
||||
# needs: test
|
||||
|
||||
# steps:
|
||||
# - name: Checkout code
|
||||
# uses: actions/checkout@v4
|
||||
|
||||
# - name: Setup Node.js
|
||||
# uses: actions/setup-node@v4
|
||||
# with:
|
||||
# node-version: '20.x'
|
||||
|
||||
# - name: Enable Corepack
|
||||
# run: corepack enable
|
||||
|
||||
# - name: Install dependencies
|
||||
# run: pnpm install --frozen-lockfile
|
||||
|
||||
# - name: Build application
|
||||
# run: pnpm build
|
||||
|
||||
# - name: Verify build artifacts
|
||||
# run: node scripts/verify-dist.js
|
||||
|
||||
# integration-test:
|
||||
# runs-on: ubuntu-latest
|
||||
# needs: test
|
||||
|
||||
# services:
|
||||
# postgres:
|
||||
# image: postgres:15
|
||||
# env:
|
||||
# POSTGRES_PASSWORD: postgres
|
||||
# POSTGRES_DB: mcphub_test
|
||||
# options: >-
|
||||
# --health-cmd pg_isready
|
||||
# --health-interval 10s
|
||||
# --health-timeout 5s
|
||||
# --health-retries 5
|
||||
|
||||
# steps:
|
||||
# - name: Checkout code
|
||||
# uses: actions/checkout@v4
|
||||
|
||||
# - name: Setup Node.js
|
||||
# uses: actions/setup-node@v4
|
||||
# with:
|
||||
# node-version: '20.x'
|
||||
|
||||
# - name: Enable Corepack
|
||||
# run: corepack enable
|
||||
|
||||
# - name: Install dependencies
|
||||
# run: pnpm install --frozen-lockfile
|
||||
|
||||
# - name: Build application
|
||||
# run: pnpm build
|
||||
|
||||
# - name: Run integration tests
|
||||
# run: |
|
||||
# export DATABASE_URL="postgresql://postgres:postgres@localhost:5432/mcphub_test"
|
||||
# node test-integration.ts
|
||||
# env:
|
||||
# NODE_ENV: test
|
||||
1
.github/workflows/npm-publish.yml
vendored
@@ -7,6 +7,7 @@ on:
|
||||
jobs:
|
||||
publish-npm:
|
||||
runs-on: ubuntu-latest
|
||||
if: endsWith(github.repository, 'mcphub')
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
2
.gitignore
vendored
@@ -24,3 +24,5 @@ yarn-error.log*
|
||||
.vscode/
|
||||
*.log
|
||||
coverage/
|
||||
|
||||
data/
|
||||
19
Dockerfile
@@ -18,20 +18,23 @@ RUN npm install -g pnpm
|
||||
ARG REQUEST_TIMEOUT=60000
|
||||
ENV REQUEST_TIMEOUT=$REQUEST_TIMEOUT
|
||||
|
||||
ARG BASE_PATH=""
|
||||
ENV BASE_PATH=$BASE_PATH
|
||||
|
||||
ENV PNPM_HOME=/usr/local/share/pnpm
|
||||
ENV PATH=$PNPM_HOME:$PATH
|
||||
RUN mkdir -p $PNPM_HOME && \
|
||||
pnpm add -g @amap/amap-maps-mcp-server @playwright/mcp@latest tavily-mcp@latest @modelcontextprotocol/server-github @modelcontextprotocol/server-slack
|
||||
pnpm add -g @amap/amap-maps-mcp-server @playwright/mcp@latest tavily-mcp@latest @modelcontextprotocol/server-github @modelcontextprotocol/server-slack
|
||||
|
||||
ARG INSTALL_EXT=false
|
||||
RUN if [ "$INSTALL_EXT" = "true" ]; then \
|
||||
ARCH=$(uname -m); \
|
||||
if [ "$ARCH" = "x86_64" ]; then \
|
||||
npx -y playwright install --with-deps chrome; \
|
||||
else \
|
||||
echo "Skipping Chrome installation on non-amd64 architecture: $ARCH"; \
|
||||
fi; \
|
||||
fi
|
||||
ARCH=$(uname -m); \
|
||||
if [ "$ARCH" = "x86_64" ]; then \
|
||||
npx -y playwright install --with-deps chrome; \
|
||||
else \
|
||||
echo "Skipping Chrome installation on non-amd64 architecture: $ARCH"; \
|
||||
fi; \
|
||||
fi
|
||||
|
||||
RUN uv tool install mcp-server-fetch
|
||||
|
||||
|
||||
56
README.md
@@ -1,14 +1,14 @@
|
||||
# MCPHub: Your Ultimate MCP Server Hub
|
||||
# MCPHub: The Unified Hub for Model Context Protocol (MCP) Servers
|
||||
|
||||
English | [中文版](README.zh.md)
|
||||
|
||||
MCPHub is a unified management platform that aggregates multiple MCP (Model Context Protocol) servers into separate Streamable HTTP (SSE) endpoints for different scenarios by group. It streamlines your AI tool integrations through an intuitive interface and robust protocol handling.
|
||||
MCPHub makes it easy to manage and scale multiple MCP (Model Context Protocol) servers by organizing them into flexible Streamable HTTP (SSE) endpoints—supporting access to all servers, individual servers, or logical server groups.
|
||||
|
||||

|
||||
|
||||
## 🚀 Features
|
||||
|
||||
- **Out-of-the-Box MCP Server Support**: Seamlessly integrate popular servers like `amap-maps`, `playwright`, `fetch`, `slack`, and more.
|
||||
- **Broadened MCP Server Support**: Seamlessly integrate any MCP server with minimal configuration.
|
||||
- **Centralized Dashboard**: Monitor real-time status and performance metrics from one sleek web UI.
|
||||
- **Flexible Protocol Handling**: Full compatibility with both stdio and SSE MCP protocols.
|
||||
- **Hot-Swappable Configuration**: Add, remove, or update MCP servers on the fly — no downtime required.
|
||||
@@ -18,7 +18,7 @@ MCPHub is a unified management platform that aggregates multiple MCP (Model Cont
|
||||
|
||||
## 🔧 Quick Start
|
||||
|
||||
### Optional Configuration
|
||||
### Configuration
|
||||
|
||||
Create a `mcp_settings.json` file to customize your server settings:
|
||||
|
||||
@@ -48,29 +48,16 @@ Create a `mcp_settings.json` file to customize your server settings:
|
||||
"SLACK_TEAM_ID": "your-team-id"
|
||||
}
|
||||
}
|
||||
},
|
||||
"users": [
|
||||
{
|
||||
"username": "admin",
|
||||
"password": "$2b$10$Vt7krIvjNgyN67LXqly0uOcTpN0LI55cYRbcKC71pUDAP0nJ7RPa.",
|
||||
"isAdmin": true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
> **Note**: Default credentials are `admin` / `admin123`. Passwords are securely hashed with bcrypt. Generate a new hash with:
|
||||
>
|
||||
> ```bash
|
||||
> npx bcryptjs your-password
|
||||
> ```
|
||||
|
||||
### Docker Deployment
|
||||
|
||||
**Recommended**: Mount your custom config:
|
||||
|
||||
```bash
|
||||
docker run -p 3000:3000 -v $(pwd)/mcp_settings.json:/app/mcp_settings.json samanhappy/mcphub
|
||||
docker run -p 3000:3000 -v ./mcp_settings.json:/app/mcp_settings.json -v ./data:/app/data samanhappy/mcphub
|
||||
```
|
||||
|
||||
or run with default settings:
|
||||
@@ -109,6 +96,31 @@ This endpoint provides a unified streamable HTTP interface for all your MCP serv
|
||||
- Easily integrate with various AI clients and tools
|
||||
- Use the same endpoint for all servers, simplifying your integration process
|
||||
|
||||
**Smart Routing (Experimental)**:
|
||||
|
||||
Smart Routing is MCPHub's intelligent tool discovery system that uses vector semantic search to automatically find the most relevant tools for any given task.
|
||||
|
||||
```
|
||||
http://localhost:3000/mcp/$smart
|
||||
```
|
||||
|
||||
**How it Works:**
|
||||
|
||||
1. **Tool Indexing**: All MCP tools are automatically converted to vector embeddings and stored in PostgreSQL with pgvector
|
||||
2. **Semantic Search**: User queries are converted to vectors and matched against tool embeddings using cosine similarity
|
||||
3. **Intelligent Filtering**: Dynamic thresholds ensure relevant results without noise
|
||||
4. **Precise Execution**: Found tools can be directly executed with proper parameter validation
|
||||
|
||||
**Setup Requirements:**
|
||||
|
||||

|
||||
|
||||
To enable Smart Routing, you need:
|
||||
|
||||
- PostgreSQL with pgvector extension
|
||||
- OpenAI API key (or compatible embedding service)
|
||||
- Enable Smart Routing in MCPHub settings
|
||||
|
||||
**Group-Specific Endpoints (Recommended)**:
|
||||
|
||||

|
||||
@@ -144,6 +156,12 @@ Connect AI clients (e.g., Claude Desktop, Cursor, DeepChat, etc.) via:
|
||||
http://localhost:3000/sse
|
||||
```
|
||||
|
||||
For smart routing, use:
|
||||
|
||||
```
|
||||
http://localhost:3000/sse/$smart
|
||||
```
|
||||
|
||||
For targeted access to specific server groups, use the group-based SSE endpoint:
|
||||
|
||||
```
|
||||
|
||||
54
README.zh.md
@@ -2,13 +2,13 @@
|
||||
|
||||
[English Version](README.md) | 中文版
|
||||
|
||||
MCPHub 是一个统一的 MCP(Model Context Protocol,模型上下文协议)服务器聚合平台,可以根据场景将多个服务器聚合到不同的流式 HTTP(SSE)端点。它通过直观的界面和强大的协议处理能力,简化了您的 AI 工具集成流程。
|
||||
MCPHub 通过将多个 MCP(Model Context Protocol)服务器组织为灵活的流式 HTTP(SSE)端点,简化了管理与扩展工作。系统支持按需访问全部服务器、单个服务器或按场景分组的服务器集合。
|
||||
|
||||

|
||||
|
||||
## 🚀 功能亮点
|
||||
|
||||
- **开箱即用的 MCP 服务器支持**:无缝集成 `amap-maps`、`playwright`、`fetch`、`slack` 等常见服务器。
|
||||
- **广泛的 MCP 服务器支持**:无缝集成任何 MCP 服务器,配置简单。
|
||||
- **集中式管理控制台**:在一个简洁的 Web UI 中实时监控所有服务器的状态和性能指标。
|
||||
- **灵活的协议兼容**:完全支持 stdio 和 SSE 两种 MCP 协议。
|
||||
- **热插拔式配置**:在运行时动态添加、移除或更新服务器配置,无需停机。
|
||||
@@ -18,7 +18,7 @@ MCPHub 是一个统一的 MCP(Model Context Protocol,模型上下文协议
|
||||
|
||||
## 🔧 快速开始
|
||||
|
||||
### 可选配置
|
||||
### 配置
|
||||
|
||||
通过创建 `mcp_settings.json` 自定义服务器设置:
|
||||
|
||||
@@ -48,29 +48,16 @@ MCPHub 是一个统一的 MCP(Model Context Protocol,模型上下文协议
|
||||
"SLACK_TEAM_ID": "your-team-id"
|
||||
}
|
||||
}
|
||||
},
|
||||
"users": [
|
||||
{
|
||||
"username": "admin",
|
||||
"password": "$2b$10$Vt7krIvjNgyN67LXqly0uOcTpN0LI55cYRbcKC71pUDAP0nJ7RPa.",
|
||||
"isAdmin": true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
> **提示**:默认用户名/密码为 `admin` / `admin123`。密码已通过 bcrypt 安全哈希。生成新密码哈希:
|
||||
>
|
||||
> ```bash
|
||||
> npx bcryptjs your-password
|
||||
> ```
|
||||
|
||||
### Docker 部署
|
||||
|
||||
**推荐**:挂载自定义配置:
|
||||
|
||||
```bash
|
||||
docker run -p 3000:3000 -v $(pwd)/mcp_settings.json:/app/mcp_settings.json samanhappy/mcphub
|
||||
docker run -p 3000:3000 -v ./mcp_settings.json:/app/mcp_settings.json -v ./data:/app/data samanhappy/mcphub
|
||||
```
|
||||
|
||||
或使用默认配置运行:
|
||||
@@ -109,6 +96,31 @@ http://localhost:3000/mcp
|
||||
- 轻松与各种 AI 客户端和工具集成
|
||||
- 对所有服务器使用相同的端点,简化集成过程
|
||||
|
||||
**智能路由(实验性功能)**:
|
||||
|
||||
智能路由是 MCPHub 的智能工具发现系统,使用向量语义搜索自动为任何给定任务找到最相关的工具。
|
||||
|
||||
```
|
||||
http://localhost:3000/mcp/$smart
|
||||
```
|
||||
|
||||
**工作原理:**
|
||||
|
||||
1. **工具索引**:所有 MCP 工具自动转换为向量嵌入并存储在 PostgreSQL 与 pgvector 中
|
||||
2. **语义搜索**:用户查询转换为向量并使用余弦相似度与工具嵌入匹配
|
||||
3. **智能筛选**:动态阈值确保相关结果且无噪声
|
||||
4. **精确执行**:找到的工具可以直接执行并进行适当的参数验证
|
||||
|
||||
**设置要求:**
|
||||
|
||||

|
||||
|
||||
为了启用智能路由,您需要:
|
||||
|
||||
- 支持 pgvector 扩展的 PostgreSQL
|
||||
- OpenAI API 密钥(或兼容的嵌入服务)
|
||||
- 在 MCPHub 设置中启用智能路由
|
||||
|
||||
**基于分组的 HTTP 端点(推荐)**:
|
||||

|
||||
要针对特定服务器分组进行访问,请使用基于分组的 HTTP 端点:
|
||||
@@ -144,6 +156,12 @@ http://localhost:3000/mcp/{server}
|
||||
http://localhost:3000/sse
|
||||
```
|
||||
|
||||
要启用智能路由,请使用:
|
||||
|
||||
```
|
||||
http://localhost:3000/sse/$smart
|
||||
```
|
||||
|
||||
要针对特定服务器分组进行访问,请使用基于分组的 SSE 端点:
|
||||
|
||||
```
|
||||
|
||||
BIN
articals/assets/sr-conf.png
Normal file
|
After Width: | Height: | Size: 33 KiB |
BIN
articals/assets/sr-dc.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
articals/assets/sr-map-call.png
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
articals/assets/sr-map-result.png
Normal file
|
After Width: | Height: | Size: 30 KiB |
BIN
articals/assets/sr-map-search.png
Normal file
|
After Width: | Height: | Size: 32 KiB |
BIN
articals/assets/sr-servers.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
articals/assets/sr-time.png
Normal file
|
After Width: | Height: | Size: 30 KiB |
BIN
articals/assets/sr-tools.png
Normal file
|
After Width: | Height: | Size: 57 KiB |
BIN
articals/assets/sr-web.png
Normal file
|
After Width: | Height: | Size: 44 KiB |
177
articals/smart-routing.md
Normal file
@@ -0,0 +1,177 @@
|
||||
# 无限工具,智能路由:MCPHub 引领 AI 工具使用新范式
|
||||
|
||||
## 概述
|
||||
|
||||
在现代 AI 应用中,随着 MCP 服务器数量的快速增长和工具种类的不断丰富,如何从数百个可用工具中快速定位最适合当前任务的工具,成为开发者和 AI 助手面临的一项重要挑战。
|
||||
|
||||
传统做法要么将所有工具暴露给 AI 助手处理,导致 token 消耗巨大、响应延迟严重;要么依赖开发者手动分组配置,灵活性和智能性不足。
|
||||
|
||||
MCPHub 推出的智能路由功能,基于向量语义搜索技术,实现了自然语言驱动的工具发现与精准推荐。它让 AI 助手能够像人类专家一样,根据任务描述自动选择最优工具组合,大幅提升了系统效率和用户体验。
|
||||
|
||||
## 什么是智能路由
|
||||
|
||||
### 技术原理
|
||||
|
||||
智能路由是 MCPHub 的核心功能之一。它将每个 MCP 工具的名称和描述嵌入为高维语义向量。当用户发起自然语言任务请求时,系统会将该请求也转换为向量,通过计算相似度,快速返回最相关的工具列表。
|
||||
|
||||
这一过程摒弃了传统的关键词匹配,具备更强的语义理解能力,能够处理自然语言的模糊性和多样性。
|
||||
|
||||
### 核心组件
|
||||
|
||||
- **向量嵌入引擎**:支持如 `text-embedding-3-small`、`bge-m3` 等主流模型,将文本描述转为语义向量。
|
||||
- **PostgreSQL + pgvector**:使用开源向量数据库方案,支持高效的向量索引和搜索。
|
||||
- **两步工作流分离**:
|
||||
- `search_tools`:负责语义工具发现
|
||||
- `call_tool`:执行实际工具调用逻辑
|
||||
|
||||
## 为什么需要智能路由
|
||||
|
||||
### 1. 减少认知负荷
|
||||
|
||||
- 当工具数量超过 100 个,AI 模型难以处理所有工具上下文。
|
||||
- 智能路由通过语义压缩,将候选工具缩小至 5~10 个,提高决策效率。
|
||||
|
||||
### 2. 显著降低 token 消耗
|
||||
|
||||
- 传统做法传入全量工具描述,可能消耗上千 token。
|
||||
- 使用智能路由,通常可将 token 使用降低 70~90%。
|
||||
|
||||
### 3. 提升调用准确率
|
||||
|
||||
- 理解任务语义:如“图片增强”→选择图像处理工具,而不是依赖命名关键词。
|
||||
- 上下文感知:考虑输入/输出格式和工具组合能力,匹配更合理的执行链路。
|
||||
|
||||
## 智能路由配置指南
|
||||
|
||||
### 1. 启动支持 `pgvector` 的 PostgreSQL 数据库
|
||||
|
||||
```bash
|
||||
docker run --name mcphub-postgres \
|
||||
-e POSTGRES_DB=mcphub \
|
||||
-e POSTGRES_USER=mcphub \
|
||||
-e POSTGRES_PASSWORD=your_password \
|
||||
-p 5432:5432 \
|
||||
-d pgvector/pgvector:pg17
|
||||
```
|
||||
|
||||
如已部署 PostgreSQL,可直接创建数据库并启用 `pgvector` 扩展:
|
||||
|
||||
```sql
|
||||
CREATE DATABASE mcphub;
|
||||
CREATE EXTENSION vector;
|
||||
```
|
||||
|
||||
### 2. 获取 embedding 模型的 API Key
|
||||
|
||||
前往 OpenAI 或其他提供商获取嵌入模型的 API Key。国内用户推荐使用硅基流动 `bge-m3` 免费模型,没有注册过的用户可以使用我的邀请链接:[https://cloud.siliconflow.cn/i/TQhVYBvA](https://cloud.siliconflow.cn/i/TQhVYBvA)。
|
||||
|
||||
### 3. 控制台配置
|
||||
|
||||

|
||||
|
||||
在 MCPHub 控制台中,进入「智能路由」配置页面,填写以下信息:
|
||||
|
||||
- **数据库 URL**:`postgresql://mcphub:your_password@localhost:5432/mcphub`
|
||||
- **OpenAI API Key** :填写你获取的嵌入模型 API Key
|
||||
- **OpenAI 基础 URL**:`https://api.siliconflow.cn/v1`
|
||||
- **嵌入模型**:推荐使用 `BAAI/bge-m3`
|
||||
|
||||
开启「启用智能路由」后系统会自动:
|
||||
|
||||
- 对所有工具生成向量嵌入
|
||||
- 构建向量索引
|
||||
- 自动监听新增工具,更新索引
|
||||
|
||||
## 工具定义
|
||||
|
||||
### search_tools - 工具搜索
|
||||
|
||||
```ts
|
||||
{
|
||||
"name": "search_tools",
|
||||
"arguments": {
|
||||
"query": "help me resize and convert images",
|
||||
"limit": 10
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### call_tool - 工具执行
|
||||
|
||||
```ts
|
||||
{
|
||||
"name": "call_tool",
|
||||
"arguments": {
|
||||
"toolName": "image_resizer",
|
||||
"arguments": {
|
||||
"input_path": "/path/to/image.png",
|
||||
"width": 800,
|
||||
"height": 600
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 演示
|
||||
|
||||
下面我将通过几个示例来展示如何使用智能路由。
|
||||
|
||||
首先,我们需要在 mcphub 添加几个不同类型的 MCP 服务器:`amap`、`time-map`、`fetch`。
|
||||
|
||||

|
||||
|
||||
然后我们需要选择一个支持 MCP 的客户端,这里选择国产的 DeepChat,聊天模型选择 `Qwen3-14B`。
|
||||
|
||||
接着,在 DeepChat 中添加 mcphub 的智能路由端点:
|
||||
|
||||

|
||||
|
||||
添加成功后,就可以在工具中看到 `search_tools` 和 `call_tool` 两个工具了:
|
||||
|
||||

|
||||
|
||||
### 示例 1:地图导航
|
||||
|
||||
输入:从北京如何导航到上海。
|
||||
|
||||
结果:
|
||||
|
||||

|
||||
|
||||
可以看到,DeepChat 先调用了 `search_tools` 工具:
|
||||
|
||||

|
||||
|
||||
然后再调用 `call_tool` 查询具体的导航信息:
|
||||
|
||||

|
||||
|
||||
### 示例 2:查询时间
|
||||
|
||||
输入:使用工具查询美国现在的时间是几点
|
||||
|
||||
结果:
|
||||
|
||||

|
||||
|
||||
需要说明的是,由于不同的模型对工具调用的支持程度不同,可能会出现一些差异。比如在这个例子中,为了提高准确性,我在输入中明确提到了“使用工具”。
|
||||
|
||||
### 示例 3:查看网页
|
||||
|
||||
输入:打开 baidu.com 看看有什么
|
||||
|
||||
结果:
|
||||
|
||||

|
||||
|
||||
可以看到,DeepChat 成功调用了工具,不过由于百度的 robots.txt 限制,无法获取到具体内容。
|
||||
|
||||
## 结语
|
||||
|
||||
借助 MCPHub 的智能路由功能,AI 助手能够更高效地处理复杂任务,显著减少不必要的 token 消耗,同时提升工具调用的准确性与灵活性。作为面向未来的 AI 工具发现与调用基础设施,智能路由不仅使 AI 更聪明地选择和组合工具,还为多 Agent 协同提供了清晰、可控且可扩展的底层能力支撑。
|
||||
|
||||
> MCPHub 只是我一时兴起开发的小项目,没想到收获了这么多关注,非常感谢大家的支持!目前 MCPHub 还有不少地方需要优化和完善,我也专门建了个交流群,感兴趣的可以添加下面的微信。
|
||||
|
||||

|
||||
|
||||
> 同时,欢迎大家一起参与建设!项目地址为:[https://github.com/samanhappy/mcphub](https://github.com/samanhappy/mcphub)。
|
||||
BIN
assets/smart-routing.png
Normal file
|
After Width: | Height: | Size: 32 KiB |
BIN
assets/smart-routing.zh.png
Normal file
|
After Width: | Height: | Size: 32 KiB |
539
docs/configuration/docker-setup.mdx
Normal file
@@ -0,0 +1,539 @@
|
||||
---
|
||||
title: 'Docker Setup'
|
||||
description: 'Deploy MCPHub using Docker and Docker Compose'
|
||||
---
|
||||
|
||||
# Docker Setup
|
||||
|
||||
This guide covers deploying MCPHub using Docker, including development and production configurations.
|
||||
|
||||
## Quick Start with Docker
|
||||
|
||||
### Using Pre-built Image
|
||||
|
||||
```bash
|
||||
# Pull the latest image
|
||||
docker pull mcphub/mcphub:latest
|
||||
|
||||
# Run with default configuration
|
||||
docker run -d \
|
||||
--name mcphub \
|
||||
-p 3000:3000 \
|
||||
-v $(pwd)/mcp_settings.json:/app/mcp_settings.json \
|
||||
mcphub/mcphub:latest
|
||||
```
|
||||
|
||||
### Building from Source
|
||||
|
||||
```bash
|
||||
# Clone the repository
|
||||
git clone https://github.com/your-username/mcphub.git
|
||||
cd mcphub
|
||||
|
||||
# Build the Docker image
|
||||
docker build -t mcphub:local .
|
||||
|
||||
# Run the container
|
||||
docker run -d \
|
||||
--name mcphub \
|
||||
-p 3000:3000 \
|
||||
-v $(pwd)/mcp_settings.json:/app/mcp_settings.json \
|
||||
mcphub:local
|
||||
```
|
||||
|
||||
## Docker Compose Setup
|
||||
|
||||
### Basic Configuration
|
||||
|
||||
Create a `docker-compose.yml` file:
|
||||
|
||||
```yaml
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
mcphub:
|
||||
image: mcphub/mcphub:latest
|
||||
# For local development, use:
|
||||
# build: .
|
||||
container_name: mcphub
|
||||
ports:
|
||||
- '3000:3000'
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
- PORT=3000
|
||||
- JWT_SECRET=${JWT_SECRET:-your-jwt-secret}
|
||||
- DATABASE_URL=postgresql://mcphub:password@postgres:5432/mcphub
|
||||
volumes:
|
||||
- ./mcp_settings.json:/app/mcp_settings.json:ro
|
||||
- ./servers.json:/app/servers.json:ro
|
||||
- mcphub_data:/app/data
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- mcphub-network
|
||||
|
||||
postgres:
|
||||
image: postgres:15-alpine
|
||||
container_name: mcphub-postgres
|
||||
environment:
|
||||
- POSTGRES_DB=mcphub
|
||||
- POSTGRES_USER=mcphub
|
||||
- POSTGRES_PASSWORD=password
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
- ./scripts/init-db.sql:/docker-entrypoint-initdb.d/init-db.sql:ro
|
||||
ports:
|
||||
- '5432:5432'
|
||||
healthcheck:
|
||||
test: ['CMD-SHELL', 'pg_isready -U mcphub -d mcphub']
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- mcphub-network
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
mcphub_data:
|
||||
|
||||
networks:
|
||||
mcphub-network:
|
||||
driver: bridge
|
||||
```
|
||||
|
||||
### Production Configuration with Nginx
|
||||
|
||||
```yaml
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
nginx:
|
||||
image: nginx:alpine
|
||||
container_name: mcphub-nginx
|
||||
ports:
|
||||
- '80:80'
|
||||
- '443:443'
|
||||
volumes:
|
||||
- ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
|
||||
- ./ssl:/etc/nginx/ssl:ro
|
||||
- nginx_logs:/var/log/nginx
|
||||
depends_on:
|
||||
- mcphub
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- mcphub-network
|
||||
|
||||
mcphub:
|
||||
image: mcphub/mcphub:latest
|
||||
container_name: mcphub-app
|
||||
expose:
|
||||
- '3000'
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
- PORT=3000
|
||||
- JWT_SECRET=${JWT_SECRET}
|
||||
- JWT_EXPIRES_IN=${JWT_EXPIRES_IN:-24h}
|
||||
- DATABASE_URL=postgresql://mcphub:${POSTGRES_PASSWORD}@postgres:5432/mcphub
|
||||
- OPENAI_API_KEY=${OPENAI_API_KEY}
|
||||
- REDIS_URL=redis://redis:6379
|
||||
volumes:
|
||||
- ./mcp_settings.json:/app/mcp_settings.json:ro
|
||||
- ./servers.json:/app/servers.json:ro
|
||||
- mcphub_data:/app/data
|
||||
- mcphub_logs:/app/logs
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
redis:
|
||||
condition: service_healthy
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- mcphub-network
|
||||
healthcheck:
|
||||
test: ['CMD', 'wget', '--quiet', '--tries=1', '--spider', 'http://localhost:3000/health']
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
|
||||
postgres:
|
||||
image: postgres:15-alpine
|
||||
container_name: mcphub-postgres
|
||||
environment:
|
||||
- POSTGRES_DB=mcphub
|
||||
- POSTGRES_USER=mcphub
|
||||
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
- ./backups:/backups
|
||||
healthcheck:
|
||||
test: ['CMD-SHELL', 'pg_isready -U mcphub -d mcphub']
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- mcphub-network
|
||||
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
container_name: mcphub-redis
|
||||
command: redis-server --appendonly yes --requirepass ${REDIS_PASSWORD}
|
||||
volumes:
|
||||
- redis_data:/data
|
||||
healthcheck:
|
||||
test: ['CMD', 'redis-cli', 'ping']
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- mcphub-network
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
redis_data:
|
||||
mcphub_data:
|
||||
mcphub_logs:
|
||||
nginx_logs:
|
||||
|
||||
networks:
|
||||
mcphub-network:
|
||||
driver: bridge
|
||||
```
|
||||
|
||||
### Environment Variables
|
||||
|
||||
Create a `.env` file for Docker Compose:
|
||||
|
||||
```env
|
||||
# Application
|
||||
NODE_ENV=production
|
||||
JWT_SECRET=your-super-secret-jwt-key-change-this
|
||||
JWT_EXPIRES_IN=24h
|
||||
|
||||
# Database
|
||||
POSTGRES_PASSWORD=your-secure-database-password
|
||||
|
||||
# Redis
|
||||
REDIS_PASSWORD=your-secure-redis-password
|
||||
|
||||
# External APIs
|
||||
OPENAI_API_KEY=your-openai-api-key
|
||||
|
||||
# Optional: Custom port
|
||||
# PORT=3000
|
||||
```
|
||||
|
||||
## Development Setup
|
||||
|
||||
### Development Docker Compose
|
||||
|
||||
Create `docker-compose.dev.yml`:
|
||||
|
||||
```yaml
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
mcphub-dev:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile.dev
|
||||
container_name: mcphub-dev
|
||||
ports:
|
||||
- '3000:3000'
|
||||
- '5173:5173' # Frontend dev server
|
||||
- '9229:9229' # Debug port
|
||||
environment:
|
||||
- NODE_ENV=development
|
||||
- PORT=3000
|
||||
- DATABASE_URL=postgresql://mcphub:password@postgres:5432/mcphub
|
||||
volumes:
|
||||
- .:/app
|
||||
- /app/node_modules
|
||||
- /app/frontend/node_modules
|
||||
depends_on:
|
||||
- postgres
|
||||
command: pnpm dev
|
||||
networks:
|
||||
- mcphub-dev
|
||||
|
||||
postgres:
|
||||
image: postgres:15-alpine
|
||||
container_name: mcphub-postgres-dev
|
||||
environment:
|
||||
- POSTGRES_DB=mcphub
|
||||
- POSTGRES_USER=mcphub
|
||||
- POSTGRES_PASSWORD=password
|
||||
ports:
|
||||
- '5432:5432'
|
||||
volumes:
|
||||
- postgres_dev_data:/var/lib/postgresql/data
|
||||
networks:
|
||||
- mcphub-dev
|
||||
|
||||
volumes:
|
||||
postgres_dev_data:
|
||||
|
||||
networks:
|
||||
mcphub-dev:
|
||||
driver: bridge
|
||||
```
|
||||
|
||||
### Development Dockerfile
|
||||
|
||||
Create `Dockerfile.dev`:
|
||||
|
||||
```dockerfile
|
||||
FROM node:20-alpine
|
||||
|
||||
# Install pnpm
|
||||
RUN npm install -g pnpm
|
||||
|
||||
# Set working directory
|
||||
WORKDIR /app
|
||||
|
||||
# Copy package files
|
||||
COPY package.json pnpm-lock.yaml ./
|
||||
COPY frontend/package.json ./frontend/
|
||||
|
||||
# Install dependencies
|
||||
RUN pnpm install
|
||||
|
||||
# Copy source code
|
||||
COPY . .
|
||||
|
||||
# Expose ports
|
||||
EXPOSE 3000 5173 9229
|
||||
|
||||
# Start development server
|
||||
CMD ["pnpm", "dev"]
|
||||
```
|
||||
|
||||
## Running the Application
|
||||
|
||||
### Development Mode
|
||||
|
||||
```bash
|
||||
# Start development environment
|
||||
docker-compose -f docker-compose.dev.yml up -d
|
||||
|
||||
# View logs
|
||||
docker-compose -f docker-compose.dev.yml logs -f mcphub-dev
|
||||
|
||||
# Stop development environment
|
||||
docker-compose -f docker-compose.dev.yml down
|
||||
```
|
||||
|
||||
### Production Mode
|
||||
|
||||
```bash
|
||||
# Start production environment
|
||||
docker-compose up -d
|
||||
|
||||
# View logs
|
||||
docker-compose logs -f mcphub
|
||||
|
||||
# Stop production environment
|
||||
docker-compose down
|
||||
```
|
||||
|
||||
## Configuration Management
|
||||
|
||||
### MCP Settings Volume Mount
|
||||
|
||||
Create your `mcp_settings.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"fetch": {
|
||||
"command": "uvx",
|
||||
"args": ["mcp-server-fetch"]
|
||||
},
|
||||
"playwright": {
|
||||
"command": "npx",
|
||||
"args": ["@playwright/mcp@latest", "--headless"]
|
||||
},
|
||||
"amap": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "@amap/amap-maps-mcp-server"],
|
||||
"env": {
|
||||
"AMAP_MAPS_API_KEY": "your-api-key"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Secrets Management
|
||||
|
||||
For production, use Docker secrets:
|
||||
|
||||
```yaml
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
mcphub:
|
||||
image: mcphub/mcphub:latest
|
||||
environment:
|
||||
- JWT_SECRET_FILE=/run/secrets/jwt_secret
|
||||
- DATABASE_PASSWORD_FILE=/run/secrets/db_password
|
||||
secrets:
|
||||
- jwt_secret
|
||||
- db_password
|
||||
|
||||
secrets:
|
||||
jwt_secret:
|
||||
file: ./secrets/jwt_secret.txt
|
||||
db_password:
|
||||
file: ./secrets/db_password.txt
|
||||
```
|
||||
|
||||
## Data Persistence
|
||||
|
||||
### Database Backups
|
||||
|
||||
Add backup service to your `docker-compose.yml`:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
backup:
|
||||
image: postgres:15-alpine
|
||||
container_name: mcphub-backup
|
||||
environment:
|
||||
- PGPASSWORD=${POSTGRES_PASSWORD}
|
||||
volumes:
|
||||
- ./backups:/backups
|
||||
- ./scripts/backup.sh:/backup.sh:ro
|
||||
command: /bin/sh -c "chmod +x /backup.sh && /backup.sh"
|
||||
depends_on:
|
||||
- postgres
|
||||
profiles:
|
||||
- backup
|
||||
networks:
|
||||
- mcphub-network
|
||||
```
|
||||
|
||||
Create `scripts/backup.sh`:
|
||||
|
||||
```bash
|
||||
#!/bin/sh
|
||||
BACKUP_FILE="/backups/mcphub_$(date +%Y%m%d_%H%M%S).sql"
|
||||
pg_dump -h postgres -U mcphub -d mcphub > "$BACKUP_FILE"
|
||||
echo "Backup created: $BACKUP_FILE"
|
||||
|
||||
# Keep only last 7 days of backups
|
||||
find /backups -name "mcphub_*.sql" -mtime +7 -delete
|
||||
```
|
||||
|
||||
Run backup:
|
||||
|
||||
```bash
|
||||
docker-compose --profile backup run --rm backup
|
||||
```
|
||||
|
||||
## Monitoring and Health Checks
|
||||
|
||||
### Health Check Endpoint
|
||||
|
||||
Add to your application:
|
||||
|
||||
```javascript
|
||||
// In your Express app
|
||||
app.get('/health', (req, res) => {
|
||||
res.json({
|
||||
status: 'healthy',
|
||||
timestamp: new Date().toISOString(),
|
||||
uptime: process.uptime(),
|
||||
memory: process.memoryUsage(),
|
||||
version: process.env.npm_package_version,
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Docker Health Checks
|
||||
|
||||
```yaml
|
||||
services:
|
||||
mcphub:
|
||||
# ... other config
|
||||
healthcheck:
|
||||
test: ['CMD', 'wget', '--quiet', '--tries=1', '--spider', 'http://localhost:3000/health']
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 60s
|
||||
```
|
||||
|
||||
### Monitoring with Watchtower
|
||||
|
||||
Add automatic updates:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
watchtower:
|
||||
image: containrrr/watchtower
|
||||
container_name: mcphub-watchtower
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
environment:
|
||||
- WATCHTOWER_CLEANUP=true
|
||||
- WATCHTOWER_POLL_INTERVAL=3600
|
||||
- WATCHTOWER_INCLUDE_STOPPED=true
|
||||
restart: unless-stopped
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
**Container fails to start**: Check logs with `docker-compose logs mcphub`
|
||||
|
||||
**Database connection errors**: Ensure PostgreSQL is healthy and accessible
|
||||
|
||||
**Port conflicts**: Check if ports 3000/5432 are already in use
|
||||
|
||||
**Volume mount issues**: Verify file paths and permissions
|
||||
|
||||
### Debug Commands
|
||||
|
||||
```bash
|
||||
# Check container status
|
||||
docker-compose ps
|
||||
|
||||
# View logs
|
||||
docker-compose logs -f [service_name]
|
||||
|
||||
# Execute commands in container
|
||||
docker-compose exec mcphub sh
|
||||
|
||||
# Check database connection
|
||||
docker-compose exec postgres psql -U mcphub -d mcphub
|
||||
|
||||
# Restart specific service
|
||||
docker-compose restart mcphub
|
||||
|
||||
# Rebuild and restart
|
||||
docker-compose up --build -d
|
||||
```
|
||||
|
||||
### Performance Optimization
|
||||
|
||||
```yaml
|
||||
services:
|
||||
mcphub:
|
||||
# ... other config
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
memory: 512M
|
||||
cpus: '0.5'
|
||||
reservations:
|
||||
memory: 256M
|
||||
cpus: '0.25'
|
||||
```
|
||||
|
||||
This Docker setup provides a complete containerized environment for MCPHub with development and production configurations.
|
||||
389
docs/configuration/environment-variables.mdx
Normal file
@@ -0,0 +1,389 @@
|
||||
---
|
||||
title: 'Environment Variables'
|
||||
description: 'Configure MCPHub using environment variables'
|
||||
---
|
||||
|
||||
# Environment Variables
|
||||
|
||||
MCPHub uses environment variables for configuration. This guide covers all available variables and their usage.
|
||||
|
||||
## Core Application Settings
|
||||
|
||||
### Server Configuration
|
||||
|
||||
| Variable | Default | Description |
|
||||
| ----------- | ------------- | ------------------------------------------------------------- |
|
||||
| `PORT` | `3000` | Port number for the HTTP server |
|
||||
| `HOST` | `0.0.0.0` | Host address to bind the server |
|
||||
| `NODE_ENV` | `development` | Application environment (`development`, `production`, `test`) |
|
||||
| `LOG_LEVEL` | `info` | Logging level (`error`, `warn`, `info`, `debug`) |
|
||||
|
||||
```env
|
||||
PORT=3000
|
||||
HOST=0.0.0.0
|
||||
NODE_ENV=production
|
||||
LOG_LEVEL=info
|
||||
```
|
||||
|
||||
### Database Configuration
|
||||
|
||||
| Variable | Default | Description |
|
||||
| -------------- | ----------- | ---------------------------------- |
|
||||
| `DATABASE_URL` | - | PostgreSQL connection string |
|
||||
| `DB_HOST` | `localhost` | Database host |
|
||||
| `DB_PORT` | `5432` | Database port |
|
||||
| `DB_NAME` | `mcphub` | Database name |
|
||||
| `DB_USER` | `mcphub` | Database username |
|
||||
| `DB_PASSWORD` | - | Database password |
|
||||
| `DB_SSL` | `false` | Enable SSL for database connection |
|
||||
| `DB_POOL_MIN` | `2` | Minimum database pool size |
|
||||
| `DB_POOL_MAX` | `10` | Maximum database pool size |
|
||||
|
||||
```env
|
||||
# Option 1: Full connection string
|
||||
DATABASE_URL=postgresql://username:password@localhost:5432/mcphub
|
||||
|
||||
# Option 2: Individual components
|
||||
DB_HOST=localhost
|
||||
DB_PORT=5432
|
||||
DB_NAME=mcphub
|
||||
DB_USER=mcphub
|
||||
DB_PASSWORD=your-password
|
||||
DB_SSL=false
|
||||
```
|
||||
|
||||
## Authentication & Security
|
||||
|
||||
### JWT Configuration
|
||||
|
||||
| Variable | Default | Description |
|
||||
| ------------------------ | ------- | ------------------------------------------- |
|
||||
| `JWT_SECRET` | - | Secret key for JWT token signing (required) |
|
||||
| `JWT_EXPIRES_IN` | `24h` | JWT token expiration time |
|
||||
| `JWT_REFRESH_EXPIRES_IN` | `7d` | Refresh token expiration time |
|
||||
| `JWT_ALGORITHM` | `HS256` | JWT signing algorithm |
|
||||
|
||||
```env
|
||||
JWT_SECRET=your-super-secret-key-change-this-in-production
|
||||
JWT_EXPIRES_IN=24h
|
||||
JWT_REFRESH_EXPIRES_IN=7d
|
||||
```
|
||||
|
||||
### Session & Security
|
||||
|
||||
| Variable | Default | Description |
|
||||
| ------------------- | ------- | ------------------------------- |
|
||||
| `SESSION_SECRET` | - | Session encryption secret |
|
||||
| `BCRYPT_ROUNDS` | `12` | bcrypt hashing rounds |
|
||||
| `RATE_LIMIT_WINDOW` | `15` | Rate limiting window in minutes |
|
||||
| `RATE_LIMIT_MAX` | `100` | Maximum requests per window |
|
||||
| `CORS_ORIGIN` | `*` | Allowed CORS origins |
|
||||
|
||||
```env
|
||||
SESSION_SECRET=your-session-secret
|
||||
BCRYPT_ROUNDS=12
|
||||
RATE_LIMIT_WINDOW=15
|
||||
RATE_LIMIT_MAX=100
|
||||
CORS_ORIGIN=https://your-domain.com,https://admin.your-domain.com
|
||||
```
|
||||
|
||||
## External Services
|
||||
|
||||
### OpenAI Configuration
|
||||
|
||||
| Variable | Default | Description |
|
||||
| ------------------------ | ------------------------ | -------------------------------- |
|
||||
| `OPENAI_API_KEY` | - | OpenAI API key for smart routing |
|
||||
| `OPENAI_MODEL` | `gpt-3.5-turbo` | OpenAI model for embeddings |
|
||||
| `OPENAI_EMBEDDING_MODEL` | `text-embedding-ada-002` | Model for vector embeddings |
|
||||
| `OPENAI_MAX_TOKENS` | `1000` | Maximum tokens per request |
|
||||
| `OPENAI_TEMPERATURE` | `0.1` | Temperature for AI responses |
|
||||
|
||||
```env
|
||||
OPENAI_API_KEY=sk-your-openai-api-key
|
||||
OPENAI_MODEL=gpt-3.5-turbo
|
||||
OPENAI_EMBEDDING_MODEL=text-embedding-ada-002
|
||||
OPENAI_MAX_TOKENS=1000
|
||||
OPENAI_TEMPERATURE=0.1
|
||||
```
|
||||
|
||||
### Redis Configuration (Optional)
|
||||
|
||||
| Variable | Default | Description |
|
||||
| ---------------- | ----------- | ----------------------- |
|
||||
| `REDIS_URL` | - | Redis connection string |
|
||||
| `REDIS_HOST` | `localhost` | Redis host |
|
||||
| `REDIS_PORT` | `6379` | Redis port |
|
||||
| `REDIS_PASSWORD` | - | Redis password |
|
||||
| `REDIS_DB` | `0` | Redis database number |
|
||||
| `REDIS_PREFIX` | `mcphub:` | Key prefix for Redis |
|
||||
|
||||
```env
|
||||
# Option 1: Full connection string
|
||||
REDIS_URL=redis://username:password@localhost:6379/0
|
||||
|
||||
# Option 2: Individual components
|
||||
REDIS_HOST=localhost
|
||||
REDIS_PORT=6379
|
||||
REDIS_PASSWORD=your-redis-password
|
||||
REDIS_DB=0
|
||||
REDIS_PREFIX=mcphub:
|
||||
```
|
||||
|
||||
## MCP Server Configuration
|
||||
|
||||
### Default Settings
|
||||
|
||||
| Variable | Default | Description |
|
||||
| ------------------- | ------------------- | -------------------------------------------- |
|
||||
| `MCP_SETTINGS_FILE` | `mcp_settings.json` | Path to MCP settings file |
|
||||
| `MCP_SERVERS_FILE` | `servers.json` | Path to servers configuration |
|
||||
| `MCP_TIMEOUT` | `30000` | Default timeout for MCP operations (ms) |
|
||||
| `MCP_MAX_RETRIES` | `3` | Maximum retry attempts for failed operations |
|
||||
| `MCP_RESTART_DELAY` | `5000` | Delay before restarting failed servers (ms) |
|
||||
|
||||
```env
|
||||
MCP_SETTINGS_FILE=./config/mcp_settings.json
|
||||
MCP_SERVERS_FILE=./config/servers.json
|
||||
MCP_TIMEOUT=30000
|
||||
MCP_MAX_RETRIES=3
|
||||
MCP_RESTART_DELAY=5000
|
||||
```
|
||||
|
||||
### Smart Routing
|
||||
|
||||
| Variable | Default | Description |
|
||||
| --------------------------- | ------- | -------------------------------- |
|
||||
| `SMART_ROUTING_ENABLED` | `true` | Enable AI-powered smart routing |
|
||||
| `SMART_ROUTING_THRESHOLD` | `0.7` | Similarity threshold for routing |
|
||||
| `SMART_ROUTING_MAX_RESULTS` | `5` | Maximum tools to return |
|
||||
| `VECTOR_CACHE_TTL` | `3600` | Vector cache TTL in seconds |
|
||||
|
||||
```env
|
||||
SMART_ROUTING_ENABLED=true
|
||||
SMART_ROUTING_THRESHOLD=0.7
|
||||
SMART_ROUTING_MAX_RESULTS=5
|
||||
VECTOR_CACHE_TTL=3600
|
||||
```
|
||||
|
||||
## File Storage & Uploads
|
||||
|
||||
| Variable | Default | Description |
|
||||
| -------------------- | ---------------- | ----------------------------------- |
|
||||
| `UPLOAD_DIR` | `./uploads` | Directory for file uploads |
|
||||
| `MAX_FILE_SIZE` | `10485760` | Maximum file size in bytes (10MB) |
|
||||
| `ALLOWED_FILE_TYPES` | `image/*,text/*` | Allowed MIME types |
|
||||
| `STORAGE_TYPE` | `local` | Storage type (`local`, `s3`, `gcs`) |
|
||||
|
||||
```env
|
||||
UPLOAD_DIR=./data/uploads
|
||||
MAX_FILE_SIZE=10485760
|
||||
ALLOWED_FILE_TYPES=image/*,text/*,application/json
|
||||
STORAGE_TYPE=local
|
||||
```
|
||||
|
||||
### S3 Storage (Optional)
|
||||
|
||||
| Variable | Default | Description |
|
||||
| ---------------------- | ----------- | ------------------ |
|
||||
| `S3_BUCKET` | - | S3 bucket name |
|
||||
| `S3_REGION` | `us-east-1` | S3 region |
|
||||
| `S3_ACCESS_KEY_ID` | - | S3 access key |
|
||||
| `S3_SECRET_ACCESS_KEY` | - | S3 secret key |
|
||||
| `S3_ENDPOINT` | - | Custom S3 endpoint |
|
||||
|
||||
```env
|
||||
S3_BUCKET=mcphub-uploads
|
||||
S3_REGION=us-east-1
|
||||
S3_ACCESS_KEY_ID=your-access-key
|
||||
S3_SECRET_ACCESS_KEY=your-secret-key
|
||||
```
|
||||
|
||||
## Monitoring & Logging
|
||||
|
||||
### Application Monitoring
|
||||
|
||||
| Variable | Default | Description |
|
||||
| ------------------------ | ------- | ----------------------------- |
|
||||
| `METRICS_ENABLED` | `true` | Enable metrics collection |
|
||||
| `METRICS_PORT` | `9090` | Port for metrics endpoint |
|
||||
| `HEALTH_CHECK_INTERVAL` | `30000` | Health check interval (ms) |
|
||||
| `PERFORMANCE_MONITORING` | `false` | Enable performance monitoring |
|
||||
|
||||
```env
|
||||
METRICS_ENABLED=true
|
||||
METRICS_PORT=9090
|
||||
HEALTH_CHECK_INTERVAL=30000
|
||||
PERFORMANCE_MONITORING=true
|
||||
```
|
||||
|
||||
### Logging Configuration
|
||||
|
||||
| Variable | Default | Description |
|
||||
| ------------------ | ------------ | --------------------------------------- |
|
||||
| `LOG_FORMAT` | `json` | Log format (`json`, `text`) |
|
||||
| `LOG_FILE` | - | Log file path (if file logging enabled) |
|
||||
| `LOG_MAX_SIZE` | `10m` | Maximum log file size |
|
||||
| `LOG_MAX_FILES` | `5` | Maximum number of log files |
|
||||
| `LOG_DATE_PATTERN` | `YYYY-MM-DD` | Date pattern for log rotation |
|
||||
|
||||
```env
|
||||
LOG_FORMAT=json
|
||||
LOG_FILE=./logs/mcphub.log
|
||||
LOG_MAX_SIZE=10m
|
||||
LOG_MAX_FILES=5
|
||||
LOG_DATE_PATTERN=YYYY-MM-DD
|
||||
```
|
||||
|
||||
## Development & Debug
|
||||
|
||||
| Variable | Default | Description |
|
||||
| ------------------------ | ------- | ----------------------------------- |
|
||||
| `DEBUG` | - | Debug namespaces (e.g., `mcphub:*`) |
|
||||
| `DEV_TOOLS_ENABLED` | `false` | Enable development tools |
|
||||
| `HOT_RELOAD` | `true` | Enable hot reload in development |
|
||||
| `MOCK_EXTERNAL_SERVICES` | `false` | Mock external API calls |
|
||||
|
||||
```env
|
||||
DEBUG=mcphub:*
|
||||
DEV_TOOLS_ENABLED=true
|
||||
HOT_RELOAD=true
|
||||
MOCK_EXTERNAL_SERVICES=false
|
||||
```
|
||||
|
||||
## Production Optimization
|
||||
|
||||
| Variable | Default | Description |
|
||||
| ------------------ | ------- | -------------------------------------- |
|
||||
| `CLUSTER_MODE` | `false` | Enable cluster mode |
|
||||
| `WORKER_PROCESSES` | `0` | Number of worker processes (0 = auto) |
|
||||
| `MEMORY_LIMIT` | - | Memory limit per process |
|
||||
| `CPU_LIMIT` | - | CPU limit per process |
|
||||
| `GC_OPTIMIZE` | `false` | Enable garbage collection optimization |
|
||||
|
||||
```env
|
||||
CLUSTER_MODE=true
|
||||
WORKER_PROCESSES=4
|
||||
MEMORY_LIMIT=512M
|
||||
GC_OPTIMIZE=true
|
||||
```
|
||||
|
||||
## Configuration Examples
|
||||
|
||||
### Development Environment
|
||||
|
||||
```env
|
||||
# .env.development
|
||||
NODE_ENV=development
|
||||
PORT=3000
|
||||
LOG_LEVEL=debug
|
||||
|
||||
# Database
|
||||
DATABASE_URL=postgresql://mcphub:password@localhost:5432/mcphub_dev
|
||||
|
||||
# Auth
|
||||
JWT_SECRET=dev-secret-key
|
||||
JWT_EXPIRES_IN=24h
|
||||
|
||||
# OpenAI (optional for development)
|
||||
# OPENAI_API_KEY=your-dev-key
|
||||
|
||||
# Debug
|
||||
DEBUG=mcphub:*
|
||||
DEV_TOOLS_ENABLED=true
|
||||
HOT_RELOAD=true
|
||||
```
|
||||
|
||||
### Production Environment
|
||||
|
||||
```env
|
||||
# .env.production
|
||||
NODE_ENV=production
|
||||
PORT=3000
|
||||
LOG_LEVEL=info
|
||||
LOG_FORMAT=json
|
||||
|
||||
# Database
|
||||
DATABASE_URL=postgresql://mcphub:secure-password@db.example.com:5432/mcphub
|
||||
DB_SSL=true
|
||||
DB_POOL_MAX=20
|
||||
|
||||
# Security
|
||||
JWT_SECRET=your-super-secure-production-secret
|
||||
SESSION_SECRET=your-session-secret
|
||||
BCRYPT_ROUNDS=14
|
||||
|
||||
# External Services
|
||||
OPENAI_API_KEY=your-production-openai-key
|
||||
REDIS_URL=redis://redis.example.com:6379
|
||||
|
||||
# Monitoring
|
||||
METRICS_ENABLED=true
|
||||
PERFORMANCE_MONITORING=true
|
||||
|
||||
# Optimization
|
||||
CLUSTER_MODE=true
|
||||
GC_OPTIMIZE=true
|
||||
```
|
||||
|
||||
### Docker Environment
|
||||
|
||||
```env
|
||||
# .env.docker
|
||||
NODE_ENV=production
|
||||
HOST=0.0.0.0
|
||||
PORT=3000
|
||||
|
||||
# Use service names for Docker networking
|
||||
DATABASE_URL=postgresql://mcphub:password@postgres:5432/mcphub
|
||||
REDIS_URL=redis://redis:6379
|
||||
|
||||
# Security
|
||||
JWT_SECRET_FILE=/run/secrets/jwt_secret
|
||||
DB_PASSWORD_FILE=/run/secrets/db_password
|
||||
|
||||
# File paths in container
|
||||
MCP_SETTINGS_FILE=/app/mcp_settings.json
|
||||
UPLOAD_DIR=/app/data/uploads
|
||||
LOG_FILE=/app/logs/mcphub.log
|
||||
```
|
||||
|
||||
## Environment Variable Loading
|
||||
|
||||
MCPHub loads environment variables in the following order:
|
||||
|
||||
1. System environment variables
|
||||
2. `.env.local` (ignored by git)
|
||||
3. `.env.{NODE_ENV}` (e.g., `.env.production`)
|
||||
4. `.env`
|
||||
|
||||
### Using dotenv-expand
|
||||
|
||||
MCPHub supports variable expansion:
|
||||
|
||||
```env
|
||||
BASE_URL=https://api.example.com
|
||||
API_ENDPOINT=${BASE_URL}/v1
|
||||
DATABASE_URL=postgresql://${DB_USER}:${DB_PASSWORD}@${DB_HOST}:${DB_PORT}/${DB_NAME}
|
||||
```
|
||||
|
||||
## Security Best Practices
|
||||
|
||||
1. **Never commit secrets** to version control
|
||||
2. **Use strong, unique secrets** for production
|
||||
3. **Rotate secrets regularly**
|
||||
4. **Use environment-specific files**
|
||||
5. **Validate all environment variables** at startup
|
||||
6. **Use Docker secrets** for container deployments
|
||||
|
||||
## Validation
|
||||
|
||||
MCPHub validates environment variables at startup. Invalid configurations will prevent the application from starting with helpful error messages.
|
||||
|
||||
Required variables for production:
|
||||
|
||||
- `JWT_SECRET`
|
||||
- `DATABASE_URL` or individual DB components
|
||||
- `OPENAI_API_KEY` (if smart routing is enabled)
|
||||
|
||||
This comprehensive environment configuration ensures MCPHub can be properly configured for any deployment scenario.
|
||||
564
docs/configuration/mcp-settings.mdx
Normal file
@@ -0,0 +1,564 @@
|
||||
---
|
||||
title: 'MCP Settings Configuration'
|
||||
description: 'Configure MCP servers and their settings for MCPHub'
|
||||
---
|
||||
|
||||
# MCP Settings Configuration
|
||||
|
||||
This guide explains how to configure MCP servers in MCPHub using the `mcp_settings.json` file and related configurations.
|
||||
|
||||
## Configuration Files Overview
|
||||
|
||||
MCPHub uses several configuration files:
|
||||
|
||||
- **`mcp_settings.json`**: Main MCP server configurations
|
||||
- **`servers.json`**: Server metadata and grouping
|
||||
- **`.env`**: Environment variables and secrets
|
||||
|
||||
## Basic MCP Settings Structure
|
||||
|
||||
### mcp_settings.json
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"server-name": {
|
||||
"command": "command-to-run",
|
||||
"args": ["arg1", "arg2"],
|
||||
"env": {
|
||||
"ENV_VAR": "value"
|
||||
},
|
||||
"cwd": "/working/directory",
|
||||
"timeout": 30000,
|
||||
"restart": true
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Example Configuration
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"fetch": {
|
||||
"command": "uvx",
|
||||
"args": ["mcp-server-fetch"],
|
||||
"env": {
|
||||
"USER_AGENT": "MCPHub/1.0"
|
||||
}
|
||||
},
|
||||
"playwright": {
|
||||
"command": "npx",
|
||||
"args": ["@playwright/mcp@latest", "--headless"],
|
||||
"timeout": 60000
|
||||
},
|
||||
"slack": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "@modelcontextprotocol/server-slack"],
|
||||
"env": {
|
||||
"SLACK_BOT_TOKEN": "${SLACK_BOT_TOKEN}",
|
||||
"SLACK_TEAM_ID": "${SLACK_TEAM_ID}"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Server Configuration Options
|
||||
|
||||
### Required Fields
|
||||
|
||||
| Field | Type | Description |
|
||||
| --------- | ------ | -------------------------- |
|
||||
| `command` | string | Executable command or path |
|
||||
| `args` | array | Command-line arguments |
|
||||
|
||||
### Optional Fields
|
||||
|
||||
| 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
|
||||
|
||||
### Web and API Servers
|
||||
|
||||
#### Fetch Server
|
||||
|
||||
```json
|
||||
{
|
||||
"fetch": {
|
||||
"command": "uvx",
|
||||
"args": ["mcp-server-fetch"],
|
||||
"env": {
|
||||
"USER_AGENT": "MCPHub/1.0",
|
||||
"MAX_REDIRECTS": "10"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Web Scraping with Playwright
|
||||
|
||||
```json
|
||||
{
|
||||
"playwright": {
|
||||
"command": "npx",
|
||||
"args": ["@playwright/mcp@latest", "--headless"],
|
||||
"timeout": 60000,
|
||||
"env": {
|
||||
"PLAYWRIGHT_BROWSERS_PATH": "/tmp/browsers"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### File and System Servers
|
||||
|
||||
#### Filesystem Server
|
||||
|
||||
```json
|
||||
{
|
||||
"filesystem": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "@modelcontextprotocol/server-filesystem", "/allowed/path"],
|
||||
"env": {
|
||||
"ALLOWED_OPERATIONS": "read,write,list"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### SQLite Server
|
||||
|
||||
```json
|
||||
{
|
||||
"sqlite": {
|
||||
"command": "uvx",
|
||||
"args": ["mcp-server-sqlite", "--db-path", "/path/to/database.db"],
|
||||
"env": {
|
||||
"SQLITE_READONLY": "false"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Communication Servers
|
||||
|
||||
#### Slack Server
|
||||
|
||||
```json
|
||||
{
|
||||
"slack": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "@modelcontextprotocol/server-slack"],
|
||||
"env": {
|
||||
"SLACK_BOT_TOKEN": "${SLACK_BOT_TOKEN}",
|
||||
"SLACK_TEAM_ID": "${SLACK_TEAM_ID}",
|
||||
"SLACK_APP_TOKEN": "${SLACK_APP_TOKEN}"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Email Server
|
||||
|
||||
```json
|
||||
{
|
||||
"email": {
|
||||
"command": "python",
|
||||
"args": ["-m", "mcp_server_email"],
|
||||
"env": {
|
||||
"SMTP_HOST": "smtp.gmail.com",
|
||||
"SMTP_PORT": "587",
|
||||
"EMAIL_USER": "${EMAIL_USER}",
|
||||
"EMAIL_PASSWORD": "${EMAIL_PASSWORD}"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Development and API Servers
|
||||
|
||||
#### GitHub Server
|
||||
|
||||
```json
|
||||
{
|
||||
"github": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "@modelcontextprotocol/server-github"],
|
||||
"env": {
|
||||
"GITHUB_PERSONAL_ACCESS_TOKEN": "${GITHUB_TOKEN}"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Google Drive Server
|
||||
|
||||
```json
|
||||
{
|
||||
"gdrive": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "@google/mcp-server-gdrive"],
|
||||
"env": {
|
||||
"GOOGLE_CLIENT_ID": "${GOOGLE_CLIENT_ID}",
|
||||
"GOOGLE_CLIENT_SECRET": "${GOOGLE_CLIENT_SECRET}",
|
||||
"GOOGLE_REFRESH_TOKEN": "${GOOGLE_REFRESH_TOKEN}"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Map and Location Services
|
||||
|
||||
#### Amap (高德地图) Server
|
||||
|
||||
```json
|
||||
{
|
||||
"amap": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "@amap/amap-maps-mcp-server"],
|
||||
"env": {
|
||||
"AMAP_MAPS_API_KEY": "${AMAP_API_KEY}",
|
||||
"AMAP_LANGUAGE": "zh-cn"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### OpenStreetMap Server
|
||||
|
||||
```json
|
||||
{
|
||||
"osm": {
|
||||
"command": "python",
|
||||
"args": ["-m", "mcp_server_osm"],
|
||||
"env": {
|
||||
"OSM_USER_AGENT": "MCPHub/1.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Advanced Configuration
|
||||
|
||||
### Environment Variable Substitution
|
||||
|
||||
MCPHub supports environment variable substitution using `${VAR_NAME}` syntax:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"api-server": {
|
||||
"command": "python",
|
||||
"args": ["-m", "api_server"],
|
||||
"env": {
|
||||
"API_KEY": "${API_KEY}",
|
||||
"API_URL": "${API_BASE_URL}/v1",
|
||||
"DEBUG": "${NODE_ENV:development}"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
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
|
||||
|
||||
#### Local Python Server
|
||||
|
||||
```json
|
||||
{
|
||||
"custom-python": {
|
||||
"command": "python",
|
||||
"args": ["./servers/custom_server.py"],
|
||||
"cwd": "/app/custom-servers",
|
||||
"env": {
|
||||
"PYTHONPATH": "/app/custom-servers",
|
||||
"CONFIG_FILE": "./config.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Local Node.js Server
|
||||
|
||||
```json
|
||||
{
|
||||
"custom-node": {
|
||||
"command": "node",
|
||||
"args": ["./servers/custom-server.js"],
|
||||
"cwd": "/app/custom-servers",
|
||||
"env": {
|
||||
"NODE_ENV": "production"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Server Metadata Configuration
|
||||
|
||||
### servers.json
|
||||
|
||||
Complement `mcp_settings.json` with server metadata:
|
||||
|
||||
```json
|
||||
{
|
||||
"servers": {
|
||||
"fetch": {
|
||||
"name": "Fetch Server",
|
||||
"description": "HTTP client for web requests",
|
||||
"category": "web",
|
||||
"tags": ["http", "api", "web"],
|
||||
"version": "1.0.0",
|
||||
"author": "MCPHub Team",
|
||||
"documentation": "https://docs.mcphub.com/servers/fetch",
|
||||
"enabled": true
|
||||
},
|
||||
"playwright": {
|
||||
"name": "Playwright Browser",
|
||||
"description": "Web automation and scraping",
|
||||
"category": "automation",
|
||||
"tags": ["browser", "scraping", "automation"],
|
||||
"version": "2.0.0",
|
||||
"enabled": true
|
||||
}
|
||||
},
|
||||
"groups": {
|
||||
"web-tools": {
|
||||
"name": "Web Tools",
|
||||
"description": "Tools for web interaction",
|
||||
"servers": ["fetch", "playwright"],
|
||||
"access": "public"
|
||||
},
|
||||
"admin-tools": {
|
||||
"name": "Admin Tools",
|
||||
"description": "Administrative utilities",
|
||||
"servers": ["filesystem", "database"],
|
||||
"access": "admin"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Group Management
|
||||
|
||||
### Group Configuration
|
||||
|
||||
```json
|
||||
{
|
||||
"groups": {
|
||||
"production": {
|
||||
"name": "Production Tools",
|
||||
"description": "Stable production servers",
|
||||
"servers": ["fetch", "slack", "github"],
|
||||
"access": "authenticated",
|
||||
"rateLimit": {
|
||||
"requestsPerMinute": 100,
|
||||
"burstLimit": 20
|
||||
}
|
||||
},
|
||||
"experimental": {
|
||||
"name": "Experimental Features",
|
||||
"description": "Beta and experimental servers",
|
||||
"servers": ["experimental-ai", "beta-search"],
|
||||
"access": "admin",
|
||||
"enabled": false
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Access Control
|
||||
|
||||
| Access Level | Description |
|
||||
| --------------- | -------------------------- |
|
||||
| `public` | No authentication required |
|
||||
| `authenticated` | Valid JWT token required |
|
||||
| `admin` | Admin role required |
|
||||
| `custom` | Custom permission logic |
|
||||
|
||||
## Dynamic Configuration
|
||||
|
||||
### Hot Reloading
|
||||
|
||||
MCPHub supports hot reloading of configurations:
|
||||
|
||||
```bash
|
||||
# Reload configurations without restart
|
||||
curl -X POST http://localhost:3000/api/admin/reload-config \
|
||||
-H "Authorization: Bearer your-admin-token"
|
||||
```
|
||||
|
||||
### Configuration Validation
|
||||
|
||||
MCPHub validates configurations on startup and reload:
|
||||
|
||||
```json
|
||||
{
|
||||
"validation": {
|
||||
"strict": true,
|
||||
"allowUnknownServers": false,
|
||||
"requireDocumentation": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### Security
|
||||
|
||||
1. **Use environment variables** for sensitive data:
|
||||
|
||||
```json
|
||||
{
|
||||
"env": {
|
||||
"API_KEY": "${API_KEY}",
|
||||
"DATABASE_PASSWORD": "${DB_PASSWORD}"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
2. **Limit server permissions**:
|
||||
```json
|
||||
{
|
||||
"filesystem": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "@modelcontextprotocol/server-filesystem", "/restricted/path"],
|
||||
"env": {
|
||||
"READONLY": "true"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Performance
|
||||
|
||||
1. **Set appropriate timeouts**:
|
||||
|
||||
```json
|
||||
{
|
||||
"timeout": 30000,
|
||||
"maxRestarts": 3,
|
||||
"restartDelay": 5000
|
||||
}
|
||||
```
|
||||
|
||||
2. **Resource limits**:
|
||||
```json
|
||||
{
|
||||
"env": {
|
||||
"NODE_OPTIONS": "--max-old-space-size=512",
|
||||
"MEMORY_LIMIT": "512MB"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Monitoring
|
||||
|
||||
1. **Enable health checks**:
|
||||
|
||||
```json
|
||||
{
|
||||
"healthCheck": {
|
||||
"enabled": true,
|
||||
"interval": 30000,
|
||||
"timeout": 5000
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
2. **Logging configuration**:
|
||||
```json
|
||||
{
|
||||
"env": {
|
||||
"LOG_LEVEL": "info",
|
||||
"LOG_FORMAT": "json"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
**Server won't start**: Check command and arguments
|
||||
|
||||
```bash
|
||||
# Test command manually
|
||||
uvx mcp-server-fetch
|
||||
```
|
||||
|
||||
**Environment variables not found**: Verify `.env` file
|
||||
|
||||
```bash
|
||||
# Check environment
|
||||
printenv | grep API_KEY
|
||||
```
|
||||
|
||||
**Permission errors**: Check file permissions and paths
|
||||
|
||||
```bash
|
||||
# Verify executable permissions
|
||||
ls -la /path/to/server
|
||||
```
|
||||
|
||||
### Debug Configuration
|
||||
|
||||
Enable debug mode for detailed logging:
|
||||
|
||||
```json
|
||||
{
|
||||
"debug": {
|
||||
"enabled": true,
|
||||
"logLevel": "debug",
|
||||
"includeEnv": false,
|
||||
"logStartup": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Validation Errors
|
||||
|
||||
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
|
||||
|
||||
This comprehensive guide covers all aspects of configuring MCP servers in MCPHub for various use cases and environments.
|
||||
373
docs/configuration/nginx.mdx
Normal file
@@ -0,0 +1,373 @@
|
||||
---
|
||||
title: 'Nginx Configuration'
|
||||
description: 'Configure Nginx as a reverse proxy for MCPHub'
|
||||
---
|
||||
|
||||
# Nginx Configuration
|
||||
|
||||
This guide explains how to configure Nginx as a reverse proxy for MCPHub, including SSL termination, load balancing, and caching strategies.
|
||||
|
||||
## Basic Reverse Proxy Setup
|
||||
|
||||
### Configuration File
|
||||
|
||||
Create or update your Nginx configuration file (`/etc/nginx/sites-available/mcphub`):
|
||||
|
||||
```nginx
|
||||
server {
|
||||
listen 80;
|
||||
server_name your-domain.com;
|
||||
|
||||
# Redirect HTTP to HTTPS
|
||||
return 301 https://$server_name$request_uri;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 443 ssl http2;
|
||||
server_name your-domain.com;
|
||||
|
||||
# SSL Configuration
|
||||
ssl_certificate /path/to/your/certificate.crt;
|
||||
ssl_certificate_key /path/to/your/private.key;
|
||||
ssl_protocols TLSv1.2 TLSv1.3;
|
||||
ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384;
|
||||
ssl_prefer_server_ciphers off;
|
||||
|
||||
# Security Headers
|
||||
add_header X-Frame-Options DENY;
|
||||
add_header X-Content-Type-Options nosniff;
|
||||
add_header X-XSS-Protection "1; mode=block";
|
||||
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
|
||||
|
||||
# Gzip Compression
|
||||
gzip on;
|
||||
gzip_vary on;
|
||||
gzip_min_length 1024;
|
||||
gzip_types
|
||||
text/plain
|
||||
text/css
|
||||
text/xml
|
||||
text/javascript
|
||||
application/json
|
||||
application/javascript
|
||||
application/xml+rss
|
||||
application/atom+xml
|
||||
image/svg+xml;
|
||||
|
||||
# Main application
|
||||
location / {
|
||||
proxy_pass http://127.0.0.1:3000;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection 'upgrade';
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_cache_bypass $http_upgrade;
|
||||
proxy_read_timeout 86400;
|
||||
}
|
||||
|
||||
# API endpoints with longer timeout for MCP operations
|
||||
location /api/ {
|
||||
proxy_pass http://127.0.0.1:3000;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection 'upgrade';
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_cache_bypass $http_upgrade;
|
||||
proxy_read_timeout 300;
|
||||
proxy_connect_timeout 60;
|
||||
proxy_send_timeout 60;
|
||||
}
|
||||
|
||||
# Static assets caching
|
||||
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
|
||||
proxy_pass http://127.0.0.1:3000;
|
||||
proxy_cache_valid 200 1d;
|
||||
proxy_cache_valid 404 1m;
|
||||
add_header Cache-Control "public, immutable";
|
||||
expires 1y;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Enable the Configuration
|
||||
|
||||
```bash
|
||||
# Create symbolic link to enable the site
|
||||
sudo ln -s /etc/nginx/sites-available/mcphub /etc/nginx/sites-enabled/
|
||||
|
||||
# Test configuration
|
||||
sudo nginx -t
|
||||
|
||||
# Reload Nginx
|
||||
sudo systemctl reload nginx
|
||||
```
|
||||
|
||||
## Load Balancing Configuration
|
||||
|
||||
For high-availability setups with multiple MCPHub instances:
|
||||
|
||||
```nginx
|
||||
upstream mcphub_backend {
|
||||
least_conn;
|
||||
server 127.0.0.1:3000 weight=1 max_fails=3 fail_timeout=30s;
|
||||
server 127.0.0.1:3001 weight=1 max_fails=3 fail_timeout=30s;
|
||||
server 127.0.0.1:3002 weight=1 max_fails=3 fail_timeout=30s;
|
||||
|
||||
# Health check (Nginx Plus feature)
|
||||
# health_check interval=5s fails=3 passes=2;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 443 ssl http2;
|
||||
server_name your-domain.com;
|
||||
|
||||
# SSL and other configurations...
|
||||
|
||||
location / {
|
||||
proxy_pass http://mcphub_backend;
|
||||
# Other proxy settings...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Caching Configuration
|
||||
|
||||
### Browser Caching
|
||||
|
||||
```nginx
|
||||
# Cache static assets
|
||||
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
|
||||
proxy_pass http://127.0.0.1:3000;
|
||||
expires 1y;
|
||||
add_header Cache-Control "public, immutable";
|
||||
}
|
||||
|
||||
# Cache API responses (be careful with dynamic content)
|
||||
location /api/public/ {
|
||||
proxy_pass http://127.0.0.1:3000;
|
||||
proxy_cache mcphub_cache;
|
||||
proxy_cache_valid 200 5m;
|
||||
proxy_cache_key "$scheme$request_method$host$request_uri";
|
||||
add_header X-Cache-Status $upstream_cache_status;
|
||||
}
|
||||
```
|
||||
|
||||
### Nginx Proxy Cache
|
||||
|
||||
Add to the `http` block in `nginx.conf`:
|
||||
|
||||
```nginx
|
||||
http {
|
||||
# Proxy cache configuration
|
||||
proxy_cache_path /var/cache/nginx/mcphub
|
||||
levels=1:2
|
||||
keys_zone=mcphub_cache:10m
|
||||
max_size=1g
|
||||
inactive=60m
|
||||
use_temp_path=off;
|
||||
|
||||
# Other configurations...
|
||||
}
|
||||
```
|
||||
|
||||
## WebSocket Support
|
||||
|
||||
For real-time features and SSE (Server-Sent Events):
|
||||
|
||||
```nginx
|
||||
location /api/stream {
|
||||
proxy_pass http://127.0.0.1:3000;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
|
||||
# Disable buffering for real-time responses
|
||||
proxy_buffering off;
|
||||
proxy_cache off;
|
||||
|
||||
# Timeouts for long-lived connections
|
||||
proxy_read_timeout 24h;
|
||||
proxy_send_timeout 24h;
|
||||
}
|
||||
```
|
||||
|
||||
## Security Configuration
|
||||
|
||||
### Rate Limiting
|
||||
|
||||
```nginx
|
||||
http {
|
||||
# Define rate limiting zones
|
||||
limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;
|
||||
limit_req_zone $binary_remote_addr zone=login:10m rate=1r/s;
|
||||
|
||||
server {
|
||||
# Apply rate limiting to API endpoints
|
||||
location /api/ {
|
||||
limit_req zone=api burst=20 nodelay;
|
||||
# Other configurations...
|
||||
}
|
||||
|
||||
# Strict rate limiting for login endpoints
|
||||
location /api/auth/login {
|
||||
limit_req zone=login burst=5;
|
||||
# Other configurations...
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### IP Whitelisting
|
||||
|
||||
```nginx
|
||||
# Allow specific IPs for admin endpoints
|
||||
location /api/admin/ {
|
||||
allow 192.168.1.0/24;
|
||||
allow 10.0.0.0/8;
|
||||
deny all;
|
||||
|
||||
proxy_pass http://127.0.0.1:3000;
|
||||
# Other proxy settings...
|
||||
}
|
||||
```
|
||||
|
||||
## Monitoring and Logging
|
||||
|
||||
### Access Logs
|
||||
|
||||
```nginx
|
||||
http {
|
||||
# Custom log format
|
||||
log_format mcphub_format '$remote_addr - $remote_user [$time_local] '
|
||||
'"$request" $status $body_bytes_sent '
|
||||
'"$http_referer" "$http_user_agent" '
|
||||
'$request_time $upstream_response_time';
|
||||
|
||||
server {
|
||||
# Enable access logging
|
||||
access_log /var/log/nginx/mcphub_access.log mcphub_format;
|
||||
error_log /var/log/nginx/mcphub_error.log;
|
||||
|
||||
# Other configurations...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Status Page
|
||||
|
||||
```nginx
|
||||
location /nginx_status {
|
||||
stub_status;
|
||||
allow 127.0.0.1;
|
||||
deny all;
|
||||
}
|
||||
```
|
||||
|
||||
## Docker Integration
|
||||
|
||||
When running MCPHub in Docker, update the proxy configuration:
|
||||
|
||||
```nginx
|
||||
upstream mcphub_docker {
|
||||
server mcphub:3000; # Docker service name
|
||||
}
|
||||
|
||||
server {
|
||||
location / {
|
||||
proxy_pass http://mcphub_docker;
|
||||
# Other proxy settings...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Complete Example Configuration
|
||||
|
||||
Here's a production-ready example using the provided `nginx.conf.example`:
|
||||
|
||||
```bash
|
||||
# Copy the example configuration
|
||||
cp nginx.conf.example /etc/nginx/sites-available/mcphub
|
||||
|
||||
# Update the configuration with your domain and paths
|
||||
sudo nano /etc/nginx/sites-available/mcphub
|
||||
|
||||
# Enable the site
|
||||
sudo ln -s /etc/nginx/sites-available/mcphub /etc/nginx/sites-enabled/
|
||||
|
||||
# Test and reload
|
||||
sudo nginx -t && sudo systemctl reload nginx
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
**502 Bad Gateway**: Check if MCPHub is running and accessible
|
||||
|
||||
**504 Gateway Timeout**: Increase `proxy_read_timeout` for long-running operations
|
||||
|
||||
**WebSocket connection failures**: Ensure proper `Upgrade` and `Connection` headers
|
||||
|
||||
**Cache issues**: Clear proxy cache or disable for development
|
||||
|
||||
### Debug Commands
|
||||
|
||||
```bash
|
||||
# Test Nginx configuration
|
||||
sudo nginx -t
|
||||
|
||||
# Check Nginx status
|
||||
sudo systemctl status nginx
|
||||
|
||||
# View error logs
|
||||
sudo tail -f /var/log/nginx/error.log
|
||||
|
||||
# Check if MCPHub is responding
|
||||
curl -I http://localhost:3000
|
||||
```
|
||||
|
||||
## Performance Optimization
|
||||
|
||||
### Worker Processes
|
||||
|
||||
```nginx
|
||||
# In nginx.conf
|
||||
worker_processes auto;
|
||||
worker_connections 1024;
|
||||
```
|
||||
|
||||
### Buffer Sizes
|
||||
|
||||
```nginx
|
||||
proxy_buffering on;
|
||||
proxy_buffer_size 128k;
|
||||
proxy_buffers 4 256k;
|
||||
proxy_busy_buffers_size 256k;
|
||||
```
|
||||
|
||||
### Keep-Alive
|
||||
|
||||
```nginx
|
||||
upstream mcphub_backend {
|
||||
server 127.0.0.1:3000;
|
||||
keepalive 32;
|
||||
}
|
||||
|
||||
location / {
|
||||
proxy_pass http://mcphub_backend;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Connection "";
|
||||
}
|
||||
```
|
||||
|
||||
This configuration provides a solid foundation for running MCPHub behind Nginx with proper security, performance, and reliability features.
|
||||
724
docs/development/architecture.mdx
Normal file
@@ -0,0 +1,724 @@
|
||||
---
|
||||
title: 'Architecture Overview'
|
||||
description: "Understand MCPHub's system architecture and design principles"
|
||||
---
|
||||
|
||||
## System Overview
|
||||
|
||||
MCPHub is designed as a scalable, modular platform for managing Model Context Protocol (MCP) servers. The architecture follows modern web application patterns with clear separation of concerns, microservices-ready design, and extensibility in mind.
|
||||
|
||||
## High-Level Architecture
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
subgraph "Client Layer"
|
||||
WEB[Web Dashboard]
|
||||
API[External APIs]
|
||||
CLI[CLI Tools]
|
||||
end
|
||||
|
||||
subgraph "Application Layer"
|
||||
LB[Load Balancer/Nginx]
|
||||
APP[MCPHub Server]
|
||||
WS[WebSocket Server]
|
||||
end
|
||||
|
||||
subgraph "Service Layer"
|
||||
MCP[MCP Service]
|
||||
AUTH[Auth Service]
|
||||
ROUTE[Smart Routing]
|
||||
MON[Monitoring Service]
|
||||
end
|
||||
|
||||
subgraph "Data Layer"
|
||||
PG[(PostgreSQL)]
|
||||
REDIS[(Redis)]
|
||||
VECTOR[(Vector Store)]
|
||||
end
|
||||
|
||||
subgraph "MCP Servers"
|
||||
GITHUB[GitHub MCP]
|
||||
FS[Filesystem MCP]
|
||||
DB[Database MCP]
|
||||
CUSTOM[Custom MCP]
|
||||
end
|
||||
|
||||
WEB --> LB
|
||||
API --> LB
|
||||
CLI --> LB
|
||||
LB --> APP
|
||||
APP --> WS
|
||||
APP --> MCP
|
||||
APP --> AUTH
|
||||
APP --> ROUTE
|
||||
APP --> MON
|
||||
|
||||
MCP --> GITHUB
|
||||
MCP --> FS
|
||||
MCP --> DB
|
||||
MCP --> CUSTOM
|
||||
|
||||
AUTH --> PG
|
||||
AUTH --> REDIS
|
||||
ROUTE --> VECTOR
|
||||
MON --> PG
|
||||
MON --> REDIS
|
||||
```
|
||||
|
||||
## Core Components
|
||||
|
||||
### 1. Application Server
|
||||
|
||||
The main Node.js/Express application that handles all HTTP requests and coordinates between services.
|
||||
|
||||
```typescript
|
||||
// src/server.ts - Main application entry point
|
||||
class MCPHubServer {
|
||||
private app: Express;
|
||||
private httpServer: Server;
|
||||
private wsServer: WebSocketServer;
|
||||
|
||||
async start(): Promise<void> {
|
||||
await this.initializeDatabase();
|
||||
await this.initializeServices();
|
||||
await this.setupRoutes();
|
||||
await this.startServer();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Key Responsibilities:**
|
||||
|
||||
- HTTP request handling
|
||||
- WebSocket connections for real-time features
|
||||
- Service coordination
|
||||
- Middleware chain management
|
||||
- Error handling and logging
|
||||
|
||||
### 2. MCP Service Layer
|
||||
|
||||
Manages the lifecycle and communication with MCP servers.
|
||||
|
||||
```typescript
|
||||
// src/services/mcpService.ts
|
||||
class MCPService {
|
||||
private servers: Map<string, MCPServerInstance> = new Map();
|
||||
private processManager: ProcessManager;
|
||||
|
||||
async startServer(config: MCPServerConfig): Promise<void> {
|
||||
const instance = await this.processManager.spawn(config);
|
||||
this.servers.set(config.name, instance);
|
||||
await this.waitForHealthy(instance);
|
||||
}
|
||||
|
||||
async executeRequest(serverName: string, request: MCPRequest): Promise<MCPResponse> {
|
||||
const server = this.servers.get(serverName);
|
||||
return await server.sendRequest(request);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Key Features:**
|
||||
|
||||
- Process lifecycle management
|
||||
- Health monitoring
|
||||
- Request routing
|
||||
- Error recovery
|
||||
- Resource management
|
||||
|
||||
### 3. Smart Routing Engine
|
||||
|
||||
Provides AI-powered tool discovery and routing using vector embeddings.
|
||||
|
||||
```typescript
|
||||
// src/services/smartRouting.ts
|
||||
class SmartRoutingService {
|
||||
private vectorStore: VectorStore;
|
||||
private embeddingService: EmbeddingService;
|
||||
|
||||
async findRelevantTools(query: string): Promise<ToolMatch[]> {
|
||||
const queryEmbedding = await this.embeddingService.embed(query);
|
||||
const matches = await this.vectorStore.similaritySearch(queryEmbedding);
|
||||
return this.rankResults(matches, query);
|
||||
}
|
||||
|
||||
async indexTool(tool: ToolDefinition): Promise<void> {
|
||||
const embedding = await this.embeddingService.embed(tool.description);
|
||||
await this.vectorStore.upsert(tool.id, embedding, tool);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Components:**
|
||||
|
||||
- Vector embedding generation
|
||||
- Similarity search
|
||||
- Result ranking and filtering
|
||||
- Tool metadata management
|
||||
|
||||
### 4. Authentication & Authorization
|
||||
|
||||
Handles user authentication, session management, and access control.
|
||||
|
||||
```typescript
|
||||
// src/services/authService.ts
|
||||
class AuthService {
|
||||
async authenticate(credentials: Credentials): Promise<User> {
|
||||
const user = await this.validateCredentials(credentials);
|
||||
const token = await this.generateJWT(user);
|
||||
await this.createSession(user, token);
|
||||
return user;
|
||||
}
|
||||
|
||||
async authorize(user: User, resource: string, action: string): Promise<boolean> {
|
||||
const permissions = await this.getUserPermissions(user);
|
||||
return this.checkPermission(permissions, resource, action);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Features:**
|
||||
|
||||
- JWT-based authentication
|
||||
- Role-based access control (RBAC)
|
||||
- Session management
|
||||
- API key authentication
|
||||
- Group-based permissions
|
||||
|
||||
### 5. Monitoring & Logging
|
||||
|
||||
Provides comprehensive monitoring, metrics collection, and logging.
|
||||
|
||||
```typescript
|
||||
// src/services/monitoringService.ts
|
||||
class MonitoringService {
|
||||
private metricsCollector: MetricsCollector;
|
||||
private alertManager: AlertManager;
|
||||
|
||||
async collectMetrics(): Promise<void> {
|
||||
const systemMetrics = await this.getSystemMetrics();
|
||||
const serverMetrics = await this.getMCPServerMetrics();
|
||||
await this.metricsCollector.record(systemMetrics, serverMetrics);
|
||||
await this.checkAlerts(systemMetrics, serverMetrics);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Capabilities:**
|
||||
|
||||
- Real-time metrics collection
|
||||
- Performance monitoring
|
||||
- Error tracking
|
||||
- Alert management
|
||||
- Audit logging
|
||||
|
||||
## Data Architecture
|
||||
|
||||
### Database Schema
|
||||
|
||||
```sql
|
||||
-- Core entities
|
||||
CREATE TABLE users (
|
||||
id UUID PRIMARY KEY,
|
||||
username VARCHAR UNIQUE NOT NULL,
|
||||
email VARCHAR UNIQUE NOT NULL,
|
||||
password_hash VARCHAR NOT NULL,
|
||||
role VARCHAR NOT NULL DEFAULT 'user',
|
||||
created_at TIMESTAMP DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE TABLE servers (
|
||||
id UUID PRIMARY KEY,
|
||||
name VARCHAR UNIQUE NOT NULL,
|
||||
command VARCHAR NOT NULL,
|
||||
args JSONB NOT NULL DEFAULT '[]',
|
||||
env JSONB DEFAULT '{}',
|
||||
group_name VARCHAR,
|
||||
status VARCHAR DEFAULT 'stopped',
|
||||
config JSONB DEFAULT '{}',
|
||||
created_at TIMESTAMP DEFAULT NOW(),
|
||||
updated_at TIMESTAMP DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE TABLE groups (
|
||||
id UUID PRIMARY KEY,
|
||||
name VARCHAR UNIQUE NOT NULL,
|
||||
description TEXT,
|
||||
config JSONB DEFAULT '{}',
|
||||
created_at TIMESTAMP DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- Vector search for smart routing
|
||||
CREATE TABLE tool_embeddings (
|
||||
id UUID PRIMARY KEY,
|
||||
server_name VARCHAR NOT NULL,
|
||||
tool_name VARCHAR NOT NULL,
|
||||
description TEXT,
|
||||
embedding vector(1536),
|
||||
metadata JSONB DEFAULT '{}',
|
||||
created_at TIMESTAMP DEFAULT NOW(),
|
||||
updated_at TIMESTAMP DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- Monitoring and logging
|
||||
CREATE TABLE request_logs (
|
||||
id UUID PRIMARY KEY,
|
||||
user_id UUID REFERENCES users(id),
|
||||
server_name VARCHAR NOT NULL,
|
||||
tool_name VARCHAR,
|
||||
request_data JSONB,
|
||||
response_data JSONB,
|
||||
status VARCHAR NOT NULL,
|
||||
duration_ms INTEGER,
|
||||
created_at TIMESTAMP DEFAULT NOW()
|
||||
);
|
||||
```
|
||||
|
||||
### Caching Strategy
|
||||
|
||||
```typescript
|
||||
// src/services/cacheService.ts
|
||||
class CacheService {
|
||||
// Multi-layer caching strategy
|
||||
private memoryCache: Map<string, CacheEntry> = new Map();
|
||||
private redisCache: Redis;
|
||||
|
||||
async get<T>(key: string): Promise<T | null> {
|
||||
// L1: Memory cache
|
||||
const memoryEntry = this.memoryCache.get(key);
|
||||
if (memoryEntry && !this.isExpired(memoryEntry)) {
|
||||
return memoryEntry.value;
|
||||
}
|
||||
|
||||
// L2: Redis cache
|
||||
const redisValue = await this.redisCache.get(key);
|
||||
if (redisValue) {
|
||||
const value = JSON.parse(redisValue);
|
||||
this.memoryCache.set(key, { value, expiry: Date.now() + 60000 });
|
||||
return value;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Cache Layers:**
|
||||
|
||||
- **L1 (Memory)**: Fast access for frequently used data
|
||||
- **L2 (Redis)**: Shared cache across instances
|
||||
- **L3 (Database)**: Persistent storage with query optimization
|
||||
|
||||
## Communication Patterns
|
||||
|
||||
### Request Flow
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Client
|
||||
participant API
|
||||
participant Auth
|
||||
participant Router
|
||||
participant MCP
|
||||
participant Server
|
||||
|
||||
Client->>API: HTTP Request
|
||||
API->>Auth: Validate Token
|
||||
Auth-->>API: User Context
|
||||
API->>Router: Route Request
|
||||
Router->>Router: Find Target Server
|
||||
Router->>MCP: Execute Request
|
||||
MCP->>Server: MCP Protocol
|
||||
Server-->>MCP: MCP Response
|
||||
MCP-->>Router: Formatted Response
|
||||
Router-->>API: Response Data
|
||||
API-->>Client: HTTP Response
|
||||
```
|
||||
|
||||
### WebSocket Communication
|
||||
|
||||
```typescript
|
||||
// src/services/websocketService.ts
|
||||
class WebSocketService {
|
||||
private connections: Map<string, WebSocket> = new Map();
|
||||
|
||||
handleConnection(ws: WebSocket, userId: string): void {
|
||||
this.connections.set(userId, ws);
|
||||
|
||||
ws.on('message', async (data) => {
|
||||
const message = JSON.parse(data.toString());
|
||||
await this.handleMessage(userId, message);
|
||||
});
|
||||
|
||||
ws.on('close', () => {
|
||||
this.connections.delete(userId);
|
||||
});
|
||||
}
|
||||
|
||||
broadcast(event: string, data: any): void {
|
||||
this.connections.forEach((ws) => {
|
||||
ws.send(JSON.stringify({ event, data }));
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Event-Driven Architecture
|
||||
|
||||
```typescript
|
||||
// src/events/eventBus.ts
|
||||
class EventBus {
|
||||
private listeners: Map<string, EventListener[]> = new Map();
|
||||
|
||||
emit(event: string, data: any): void {
|
||||
const handlers = this.listeners.get(event) || [];
|
||||
handlers.forEach((handler) => handler(data));
|
||||
}
|
||||
|
||||
on(event: string, handler: EventListener): void {
|
||||
const handlers = this.listeners.get(event) || [];
|
||||
handlers.push(handler);
|
||||
this.listeners.set(event, handlers);
|
||||
}
|
||||
}
|
||||
|
||||
// Usage
|
||||
eventBus.on('server.started', (data) => {
|
||||
logger.info(`Server ${data.name} started`);
|
||||
monitoringService.updateServerStatus(data.name, 'running');
|
||||
});
|
||||
```
|
||||
|
||||
## Security Architecture
|
||||
|
||||
### Authentication Flow
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
A[Client] --> B[Login Request]
|
||||
B --> C[Validate Credentials]
|
||||
C --> D[Generate JWT]
|
||||
D --> E[Create Session]
|
||||
E --> F[Return Token]
|
||||
F --> G[Store in Cookie/Header]
|
||||
G --> H[Subsequent Requests]
|
||||
H --> I[Validate Token]
|
||||
I --> J[Check Permissions]
|
||||
J --> K[Allow/Deny Access]
|
||||
```
|
||||
|
||||
### Authorization Model
|
||||
|
||||
```typescript
|
||||
// Role-Based Access Control (RBAC)
|
||||
interface Permission {
|
||||
resource: string; // e.g., 'servers', 'groups', 'users'
|
||||
action: string; // e.g., 'create', 'read', 'update', 'delete'
|
||||
scope?: string; // e.g., 'own', 'group', 'all'
|
||||
}
|
||||
|
||||
interface Role {
|
||||
name: string;
|
||||
permissions: Permission[];
|
||||
}
|
||||
|
||||
const roles: Role[] = [
|
||||
{
|
||||
name: 'admin',
|
||||
permissions: [{ resource: '*', action: '*', scope: 'all' }],
|
||||
},
|
||||
{
|
||||
name: 'manager',
|
||||
permissions: [
|
||||
{ resource: 'servers', action: '*', scope: 'group' },
|
||||
{ resource: 'groups', action: 'read', scope: 'all' },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'user',
|
||||
permissions: [
|
||||
{ resource: 'servers', action: 'read', scope: 'group' },
|
||||
{ resource: 'tools', action: 'execute', scope: 'group' },
|
||||
],
|
||||
},
|
||||
];
|
||||
```
|
||||
|
||||
## Scalability Considerations
|
||||
|
||||
### Horizontal Scaling
|
||||
|
||||
```yaml
|
||||
# Kubernetes deployment for scaling
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: mcphub
|
||||
spec:
|
||||
replicas: 3
|
||||
selector:
|
||||
matchLabels:
|
||||
app: mcphub
|
||||
template:
|
||||
spec:
|
||||
containers:
|
||||
- name: mcphub
|
||||
image: mcphub:latest
|
||||
resources:
|
||||
requests:
|
||||
memory: '256Mi'
|
||||
cpu: '200m'
|
||||
limits:
|
||||
memory: '512Mi'
|
||||
cpu: '500m'
|
||||
```
|
||||
|
||||
### Load Balancing Strategy
|
||||
|
||||
```typescript
|
||||
// src/services/loadBalancer.ts
|
||||
class LoadBalancer {
|
||||
private servers: ServerInstance[] = [];
|
||||
private algorithm: 'round-robin' | 'least-connections' | 'weighted';
|
||||
|
||||
selectServer(): ServerInstance {
|
||||
switch (this.algorithm) {
|
||||
case 'round-robin':
|
||||
return this.roundRobin();
|
||||
case 'least-connections':
|
||||
return this.leastConnections();
|
||||
case 'weighted':
|
||||
return this.weighted();
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Database Scaling
|
||||
|
||||
```typescript
|
||||
// Database connection management
|
||||
class DatabaseManager {
|
||||
private readPool: Pool; // Read replicas
|
||||
private writePool: Pool; // Primary database
|
||||
|
||||
async query(sql: string, params: any[]): Promise<any> {
|
||||
if (this.isReadOperation(sql)) {
|
||||
return this.readPool.query(sql, params);
|
||||
} else {
|
||||
return this.writePool.query(sql, params);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Performance Optimization
|
||||
|
||||
### Query Optimization
|
||||
|
||||
```sql
|
||||
-- Optimized queries with proper indexing
|
||||
CREATE INDEX CONCURRENTLY idx_servers_status_group
|
||||
ON servers(status, group_name)
|
||||
WHERE status IN ('running', 'starting');
|
||||
|
||||
CREATE INDEX CONCURRENTLY idx_tool_embeddings_similarity
|
||||
ON tool_embeddings USING ivfflat (embedding vector_cosine_ops)
|
||||
WITH (lists = 100);
|
||||
|
||||
CREATE INDEX CONCURRENTLY idx_request_logs_performance
|
||||
ON request_logs(created_at, status, duration_ms)
|
||||
WHERE created_at > NOW() - INTERVAL '30 days';
|
||||
```
|
||||
|
||||
### Caching Strategy
|
||||
|
||||
```typescript
|
||||
// Multi-level caching
|
||||
class CacheManager {
|
||||
// Cache server configurations
|
||||
@Cache({ ttl: 300, key: 'server-config' })
|
||||
async getServerConfig(name: string): Promise<ServerConfig> {
|
||||
return this.database.getServerConfig(name);
|
||||
}
|
||||
|
||||
// Cache tool metadata for smart routing
|
||||
@Cache({ ttl: 3600, key: 'tool-metadata' })
|
||||
async getToolMetadata(): Promise<ToolMetadata[]> {
|
||||
return this.database.getToolMetadata();
|
||||
}
|
||||
|
||||
// Cache user permissions
|
||||
@Cache({ ttl: 600, key: 'user-permissions' })
|
||||
async getUserPermissions(userId: string): Promise<Permission[]> {
|
||||
return this.authService.getUserPermissions(userId);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Monitoring & Observability
|
||||
|
||||
### Metrics Collection
|
||||
|
||||
```typescript
|
||||
// src/services/metricsService.ts
|
||||
class MetricsService {
|
||||
private prometheus: PrometheusRegistry;
|
||||
|
||||
constructor() {
|
||||
this.initializeMetrics();
|
||||
}
|
||||
|
||||
private initializeMetrics(): void {
|
||||
// Request metrics
|
||||
this.requestCount = new Counter({
|
||||
name: 'mcphub_requests_total',
|
||||
help: 'Total number of requests',
|
||||
labelNames: ['method', 'route', 'status'],
|
||||
});
|
||||
|
||||
// Server metrics
|
||||
this.serverStatus = new Gauge({
|
||||
name: 'mcphub_server_status',
|
||||
help: 'Status of MCP servers',
|
||||
labelNames: ['server_name', 'status'],
|
||||
});
|
||||
|
||||
// Performance metrics
|
||||
this.responseTime = new Histogram({
|
||||
name: 'mcphub_response_time_seconds',
|
||||
help: 'Response time in seconds',
|
||||
labelNames: ['route'],
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Distributed Tracing
|
||||
|
||||
```typescript
|
||||
// OpenTelemetry integration
|
||||
import { trace } from '@opentelemetry/api';
|
||||
|
||||
class MCPService {
|
||||
async executeRequest(serverName: string, request: MCPRequest): Promise<MCPResponse> {
|
||||
const span = trace.getActiveSpan();
|
||||
span?.setAttributes({
|
||||
'mcp.server': serverName,
|
||||
'mcp.tool': request.tool,
|
||||
'mcp.request_id': request.id,
|
||||
});
|
||||
|
||||
try {
|
||||
const response = await this.sendRequest(serverName, request);
|
||||
span?.setStatus({ code: SpanStatusCode.OK });
|
||||
return response;
|
||||
} catch (error) {
|
||||
span?.setStatus({
|
||||
code: SpanStatusCode.ERROR,
|
||||
message: error.message,
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Extension Points
|
||||
|
||||
### Plugin Architecture
|
||||
|
||||
```typescript
|
||||
// Plugin interface
|
||||
interface MCPHubPlugin {
|
||||
name: string;
|
||||
version: string;
|
||||
init(context: PluginContext): Promise<void>;
|
||||
destroy(): Promise<void>;
|
||||
}
|
||||
|
||||
// Plugin manager
|
||||
class PluginManager {
|
||||
private plugins: Map<string, MCPHubPlugin> = new Map();
|
||||
|
||||
async loadPlugin(plugin: MCPHubPlugin): Promise<void> {
|
||||
await plugin.init(this.createContext());
|
||||
this.plugins.set(plugin.name, plugin);
|
||||
}
|
||||
|
||||
private createContext(): PluginContext {
|
||||
return {
|
||||
eventBus: this.eventBus,
|
||||
logger: this.logger,
|
||||
database: this.database,
|
||||
// ... other services
|
||||
};
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Custom Middleware
|
||||
|
||||
```typescript
|
||||
// Custom middleware registration
|
||||
class MiddlewareManager {
|
||||
register(middleware: Middleware): void {
|
||||
this.app.use(middleware);
|
||||
}
|
||||
|
||||
registerRoute(path: string, middleware: Middleware): void {
|
||||
this.app.use(path, middleware);
|
||||
}
|
||||
}
|
||||
|
||||
// Example custom middleware
|
||||
const customAuthMiddleware: Middleware = (req, res, next) => {
|
||||
// Custom authentication logic
|
||||
next();
|
||||
};
|
||||
```
|
||||
|
||||
## Deployment Architecture
|
||||
|
||||
### Container Strategy
|
||||
|
||||
```dockerfile
|
||||
# Multi-stage build for optimized images
|
||||
FROM node:18-alpine AS builder
|
||||
WORKDIR /app
|
||||
COPY package*.json ./
|
||||
RUN npm ci --only=production
|
||||
|
||||
FROM node:18-alpine AS runtime
|
||||
RUN addgroup -g 1001 -S nodejs
|
||||
RUN adduser -S mcphub -u 1001
|
||||
WORKDIR /app
|
||||
COPY --from=builder --chown=mcphub:nodejs /app .
|
||||
USER mcphub
|
||||
EXPOSE 3000
|
||||
CMD ["node", "dist/server.js"]
|
||||
```
|
||||
|
||||
### Infrastructure as Code
|
||||
|
||||
```terraform
|
||||
# Terraform configuration for AWS deployment
|
||||
resource "aws_ecs_cluster" "mcphub" {
|
||||
name = "mcphub-cluster"
|
||||
}
|
||||
|
||||
resource "aws_ecs_service" "mcphub" {
|
||||
name = "mcphub"
|
||||
cluster = aws_ecs_cluster.mcphub.id
|
||||
task_definition = aws_ecs_task_definition.mcphub.arn
|
||||
desired_count = 3
|
||||
|
||||
load_balancer {
|
||||
target_group_arn = aws_lb_target_group.mcphub.arn
|
||||
container_name = "mcphub"
|
||||
container_port = 3000
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This architecture provides a solid foundation for building a scalable, maintainable, and extensible MCP server management platform while following modern software development best practices.
|
||||
597
docs/development/contributing.mdx
Normal file
@@ -0,0 +1,597 @@
|
||||
---
|
||||
title: 'Contributing to MCPHub'
|
||||
description: 'Learn how to contribute to the MCPHub project'
|
||||
---
|
||||
|
||||
## Welcome Contributors! 🎉
|
||||
|
||||
Thank you for your interest in contributing to MCPHub! This guide will help you get started with contributing to the project, whether you're fixing bugs, adding features, improving documentation, or helping with testing.
|
||||
|
||||
## Quick Start
|
||||
|
||||
1. **Fork the repository** on GitHub
|
||||
2. **Clone your fork** locally
|
||||
3. **Create a branch** for your changes
|
||||
4. **Make your changes** following our guidelines
|
||||
5. **Test your changes** thoroughly
|
||||
6. **Submit a pull request**
|
||||
|
||||
## Ways to Contribute
|
||||
|
||||
### 🐛 Bug Reports
|
||||
|
||||
Help us improve MCPHub by reporting bugs:
|
||||
|
||||
- **Search existing issues** first to avoid duplicates
|
||||
- **Use the bug report template** when creating issues
|
||||
- **Provide detailed information** including steps to reproduce
|
||||
- **Include system information** (OS, Node.js version, etc.)
|
||||
|
||||
```markdown
|
||||
## Bug Report Template
|
||||
|
||||
**Description**
|
||||
A clear description of what the bug is.
|
||||
|
||||
**Steps to Reproduce**
|
||||
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
**Expected Behavior**
|
||||
What you expected to happen.
|
||||
|
||||
**Actual Behavior**
|
||||
What actually happened.
|
||||
|
||||
**Environment**
|
||||
|
||||
- OS: [e.g. macOS 12.0]
|
||||
- Node.js: [e.g. 18.17.0]
|
||||
- MCPHub Version: [e.g. 1.2.3]
|
||||
- Browser: [e.g. Chrome 91.0]
|
||||
|
||||
**Additional Context**
|
||||
Any other context about the problem.
|
||||
```
|
||||
|
||||
### ✨ Feature Requests
|
||||
|
||||
We welcome feature suggestions:
|
||||
|
||||
- **Check existing feature requests** to avoid duplicates
|
||||
- **Use the feature request template**
|
||||
- **Explain the use case** and why it would be valuable
|
||||
- **Consider implementation complexity**
|
||||
|
||||
### 🔧 Code Contributions
|
||||
|
||||
Ready to write some code? Here's how:
|
||||
|
||||
#### Setting Up Development Environment
|
||||
|
||||
```bash
|
||||
# 1. Fork and clone the repository
|
||||
git clone https://github.com/YOUR_USERNAME/mcphub.git
|
||||
cd mcphub
|
||||
|
||||
# 2. Add upstream remote
|
||||
git remote add upstream https://github.com/mcphub/mcphub.git
|
||||
|
||||
# 3. Install dependencies
|
||||
pnpm install
|
||||
|
||||
# 4. Set up environment
|
||||
cp .env.example .env.development
|
||||
|
||||
# 5. Start development environment
|
||||
docker-compose -f docker-compose.dev.yml up -d
|
||||
pnpm run migrate
|
||||
pnpm run seed
|
||||
|
||||
# 6. Start development server
|
||||
pnpm run dev
|
||||
```
|
||||
|
||||
#### Branch Naming Convention
|
||||
|
||||
Use descriptive branch names with prefixes:
|
||||
|
||||
```bash
|
||||
# Features
|
||||
git checkout -b feature/smart-routing-improvements
|
||||
git checkout -b feature/user-authentication
|
||||
|
||||
# Bug fixes
|
||||
git checkout -b fix/server-startup-error
|
||||
git checkout -b fix/memory-leak-in-cache
|
||||
|
||||
# Documentation
|
||||
git checkout -b docs/api-reference-update
|
||||
git checkout -b docs/deployment-guide
|
||||
|
||||
# Refactoring
|
||||
git checkout -b refactor/auth-service-cleanup
|
||||
git checkout -b refactor/database-queries
|
||||
```
|
||||
|
||||
### 📚 Documentation
|
||||
|
||||
Help improve our documentation:
|
||||
|
||||
- **Fix typos and grammar**
|
||||
- **Improve existing guides**
|
||||
- **Add missing documentation**
|
||||
- **Create tutorials and examples**
|
||||
- **Translate documentation**
|
||||
|
||||
## Development Guidelines
|
||||
|
||||
### Code Style
|
||||
|
||||
We use ESLint and Prettier to maintain code quality:
|
||||
|
||||
```bash
|
||||
# Check code style
|
||||
pnpm run lint
|
||||
|
||||
# Fix automatically fixable issues
|
||||
pnpm run lint:fix
|
||||
|
||||
# Format code
|
||||
pnpm run format
|
||||
|
||||
# Type check
|
||||
pnpm run type-check
|
||||
```
|
||||
|
||||
#### TypeScript Best Practices
|
||||
|
||||
```typescript
|
||||
// ✅ Good: Use proper types
|
||||
interface MCPServerConfig {
|
||||
name: string;
|
||||
command: string;
|
||||
args: string[];
|
||||
env?: Record<string, string>;
|
||||
}
|
||||
|
||||
// ✅ Good: Use async/await
|
||||
async function startServer(config: MCPServerConfig): Promise<void> {
|
||||
try {
|
||||
await mcpService.start(config);
|
||||
} catch (error) {
|
||||
logger.error('Failed to start server', { error, config });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// ❌ Bad: Using any type
|
||||
function processData(data: any): any {
|
||||
return data.something();
|
||||
}
|
||||
|
||||
// ❌ Bad: Not handling errors
|
||||
async function riskyOperation(): Promise<void> {
|
||||
await dangerousFunction(); // Could throw
|
||||
}
|
||||
```
|
||||
|
||||
#### React/Frontend Guidelines
|
||||
|
||||
```tsx
|
||||
// ✅ Good: Functional components with proper typing
|
||||
interface ServerCardProps {
|
||||
server: MCPServer;
|
||||
onStart: (serverId: string) => void;
|
||||
onStop: (serverId: string) => void;
|
||||
}
|
||||
|
||||
const ServerCard: React.FC<ServerCardProps> = ({ server, onStart, onStop }) => {
|
||||
const handleStart = useCallback(() => {
|
||||
onStart(server.id);
|
||||
}, [server.id, onStart]);
|
||||
|
||||
return (
|
||||
<Card data-testid={`server-card-${server.id}`}>
|
||||
<CardHeader>
|
||||
<CardTitle>{server.name}</CardTitle>
|
||||
<Badge variant={server.status === 'running' ? 'success' : 'secondary'}>
|
||||
{server.status}
|
||||
</Badge>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p>{server.description}</p>
|
||||
</CardContent>
|
||||
<CardActions>
|
||||
{server.status === 'stopped' ? (
|
||||
<Button onClick={handleStart}>Start</Button>
|
||||
) : (
|
||||
<Button onClick={() => onStop(server.id)}>Stop</Button>
|
||||
)}
|
||||
</CardActions>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
### Testing Requirements
|
||||
|
||||
All contributions must include appropriate tests:
|
||||
|
||||
#### Unit Tests
|
||||
|
||||
```typescript
|
||||
// src/services/__tests__/mcpService.test.ts
|
||||
import { MCPService } from '../mcpService';
|
||||
import { mockLogger, mockDatabase } from '../../__mocks__';
|
||||
|
||||
describe('MCPService', () => {
|
||||
let service: MCPService;
|
||||
|
||||
beforeEach(() => {
|
||||
service = new MCPService(mockLogger, mockDatabase);
|
||||
});
|
||||
|
||||
describe('startServer', () => {
|
||||
it('should start a server successfully', async () => {
|
||||
const config = {
|
||||
name: 'test-server',
|
||||
command: 'node',
|
||||
args: ['server.js'],
|
||||
};
|
||||
|
||||
await service.startServer(config);
|
||||
|
||||
expect(service.getServerStatus('test-server')).toBe('running');
|
||||
});
|
||||
|
||||
it('should handle server startup failures', async () => {
|
||||
const invalidConfig = {
|
||||
name: 'invalid-server',
|
||||
command: 'invalid-command',
|
||||
args: [],
|
||||
};
|
||||
|
||||
await expect(service.startServer(invalidConfig)).rejects.toThrow(
|
||||
'Failed to start server: Command not found',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
#### Integration Tests
|
||||
|
||||
```typescript
|
||||
// src/__tests__/integration/server-api.test.ts
|
||||
import request from 'supertest';
|
||||
import { app } from '../../app';
|
||||
import { setupTestDatabase, teardownTestDatabase } from '../helpers/database';
|
||||
|
||||
describe('Server API Integration', () => {
|
||||
beforeAll(async () => {
|
||||
await setupTestDatabase();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await teardownTestDatabase();
|
||||
});
|
||||
|
||||
describe('POST /api/servers', () => {
|
||||
it('should create a new server', async () => {
|
||||
const serverData = {
|
||||
name: 'test-server',
|
||||
command: 'node',
|
||||
args: ['server.js'],
|
||||
group: 'development',
|
||||
};
|
||||
|
||||
const response = await request(app).post('/api/servers').send(serverData).expect(201);
|
||||
|
||||
expect(response.body).toMatchObject({
|
||||
name: 'test-server',
|
||||
status: 'stopped',
|
||||
group: 'development',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
#### End-to-End Tests
|
||||
|
||||
```typescript
|
||||
// tests/e2e/server-management.spec.ts
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
test.describe('Server Management', () => {
|
||||
test('should create and manage MCP servers', async ({ page }) => {
|
||||
await page.goto('/dashboard');
|
||||
|
||||
// Create new server
|
||||
await page.click('[data-testid="add-server-button"]');
|
||||
await page.fill('[data-testid="server-name-input"]', 'test-server');
|
||||
await page.fill('[data-testid="server-command-input"]', 'node server.js');
|
||||
await page.click('[data-testid="save-server-button"]');
|
||||
|
||||
// Verify server appears in list
|
||||
await expect(page.locator('[data-testid="server-list"]')).toContainText('test-server');
|
||||
|
||||
// Start the server
|
||||
await page.click('[data-testid="start-server-test-server"]');
|
||||
|
||||
// Verify server is running
|
||||
await expect(page.locator('[data-testid="server-status-test-server"]')).toContainText(
|
||||
'running',
|
||||
);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Commit Guidelines
|
||||
|
||||
We follow [Conventional Commits](https://www.conventionalcommits.org/):
|
||||
|
||||
```bash
|
||||
# Format: <type>[optional scope]: <description>
|
||||
|
||||
# Features
|
||||
git commit -m "feat(auth): add JWT token refresh functionality"
|
||||
git commit -m "feat(ui): implement server status dashboard"
|
||||
|
||||
# Bug fixes
|
||||
git commit -m "fix(api): resolve memory leak in server manager"
|
||||
git commit -m "fix(db): handle connection timeout gracefully"
|
||||
|
||||
# Documentation
|
||||
git commit -m "docs(api): add examples for server endpoints"
|
||||
git commit -m "docs(readme): update installation instructions"
|
||||
|
||||
# Refactoring
|
||||
git commit -m "refactor(services): extract auth logic into separate module"
|
||||
|
||||
# Tests
|
||||
git commit -m "test(api): add integration tests for server management"
|
||||
|
||||
# Chores
|
||||
git commit -m "chore(deps): update dependencies to latest versions"
|
||||
```
|
||||
|
||||
#### Commit Types
|
||||
|
||||
- **feat**: New feature
|
||||
- **fix**: Bug fix
|
||||
- **docs**: Documentation changes
|
||||
- **style**: Code style changes (formatting, etc.)
|
||||
- **refactor**: Code refactoring
|
||||
- **test**: Adding or updating tests
|
||||
- **chore**: Maintenance tasks
|
||||
- **perf**: Performance improvements
|
||||
- **ci**: CI/CD changes
|
||||
|
||||
### Pull Request Process
|
||||
|
||||
#### Before Submitting
|
||||
|
||||
```bash
|
||||
# 1. Sync with upstream
|
||||
git fetch upstream
|
||||
git checkout main
|
||||
git merge upstream/main
|
||||
|
||||
# 2. Rebase your feature branch
|
||||
git checkout feature/your-feature
|
||||
git rebase main
|
||||
|
||||
# 3. Run all checks
|
||||
pnpm run lint
|
||||
pnpm run type-check
|
||||
pnpm run test
|
||||
pnpm run build
|
||||
|
||||
# 4. Update documentation if needed
|
||||
# 5. Add/update tests for your changes
|
||||
```
|
||||
|
||||
#### Pull Request Template
|
||||
|
||||
```markdown
|
||||
## Description
|
||||
|
||||
Brief description of the changes and motivation.
|
||||
|
||||
## Type of Change
|
||||
|
||||
- [ ] Bug fix (non-breaking change which fixes an issue)
|
||||
- [ ] New feature (non-breaking change which adds functionality)
|
||||
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
|
||||
- [ ] Documentation update
|
||||
|
||||
## Testing
|
||||
|
||||
- [ ] Unit tests pass
|
||||
- [ ] Integration tests pass
|
||||
- [ ] E2E tests pass (if applicable)
|
||||
- [ ] Manual testing completed
|
||||
|
||||
## Documentation
|
||||
|
||||
- [ ] Code is self-documenting
|
||||
- [ ] API documentation updated
|
||||
- [ ] User documentation updated
|
||||
- [ ] README updated (if needed)
|
||||
|
||||
## Checklist
|
||||
|
||||
- [ ] My code follows the project's style guidelines
|
||||
- [ ] I have performed a self-review of my code
|
||||
- [ ] I have commented my code, particularly in hard-to-understand areas
|
||||
- [ ] I have made corresponding changes to the documentation
|
||||
- [ ] My changes generate no new warnings
|
||||
- [ ] I have added tests that prove my fix is effective or that my feature works
|
||||
- [ ] New and existing unit tests pass locally with my changes
|
||||
|
||||
## Screenshots (if applicable)
|
||||
|
||||
Add screenshots to help explain your changes.
|
||||
|
||||
## Additional Notes
|
||||
|
||||
Any additional information that reviewers should know.
|
||||
```
|
||||
|
||||
### Code Review Process
|
||||
|
||||
#### For Contributors
|
||||
|
||||
- **Be patient**: Reviews take time, and reviewers may have questions
|
||||
- **Be responsive**: Address feedback promptly and clearly
|
||||
- **Be open**: Accept constructive criticism and suggestions
|
||||
- **Ask questions**: If feedback is unclear, ask for clarification
|
||||
|
||||
#### For Reviewers
|
||||
|
||||
- **Be constructive**: Provide helpful suggestions, not just criticism
|
||||
- **Be specific**: Point out exact issues and suggest solutions
|
||||
- **Be timely**: Review PRs within a reasonable timeframe
|
||||
- **Be encouraging**: Recognize good work and improvements
|
||||
|
||||
## Community Guidelines
|
||||
|
||||
### Code of Conduct
|
||||
|
||||
We are committed to providing a welcoming and inspiring community for all:
|
||||
|
||||
- **Be respectful**: Treat everyone with respect and kindness
|
||||
- **Be inclusive**: Welcome people of all backgrounds and skill levels
|
||||
- **Be collaborative**: Work together towards common goals
|
||||
- **Be patient**: Help others learn and grow
|
||||
- **Be professional**: Maintain professional communication
|
||||
|
||||
### Communication Channels
|
||||
|
||||
- **GitHub Issues**: Bug reports and feature requests
|
||||
- **GitHub Discussions**: General questions and community chat
|
||||
- **Discord**: Real-time community chat (link in README)
|
||||
- **Email**: Security issues and private matters
|
||||
|
||||
## Getting Help
|
||||
|
||||
### Documentation
|
||||
|
||||
- [Getting Started Guide](./getting-started.mdx)
|
||||
- [Architecture Overview](./architecture.mdx)
|
||||
- [API Reference](../api-reference/introduction.mdx)
|
||||
- [Configuration Guide](../configuration/mcp-settings.mdx)
|
||||
|
||||
### Common Issues
|
||||
|
||||
**Build Failures**
|
||||
|
||||
```bash
|
||||
# Clear and reinstall dependencies
|
||||
rm -rf node_modules pnpm-lock.yaml
|
||||
pnpm install
|
||||
|
||||
# Clear build cache
|
||||
rm -rf dist/
|
||||
pnpm run build
|
||||
```
|
||||
|
||||
**Test Failures**
|
||||
|
||||
```bash
|
||||
# Run tests with verbose output
|
||||
pnpm run test -- --verbose
|
||||
|
||||
# Run specific test file
|
||||
pnpm test src/services/mcpService.test.ts
|
||||
|
||||
# Debug tests
|
||||
pnpm run test:debug
|
||||
```
|
||||
|
||||
**Database Issues**
|
||||
|
||||
```bash
|
||||
# Reset database
|
||||
pnpm run db:reset
|
||||
|
||||
# Run migrations
|
||||
pnpm run migrate
|
||||
|
||||
# Seed development data
|
||||
pnpm run seed
|
||||
```
|
||||
|
||||
### Getting Support
|
||||
|
||||
If you need help:
|
||||
|
||||
1. **Check the documentation** first
|
||||
2. **Search existing issues** on GitHub
|
||||
3. **Ask in GitHub Discussions** for general questions
|
||||
4. **Create an issue** if you found a bug
|
||||
5. **Join our Discord** for real-time help
|
||||
|
||||
## Recognition
|
||||
|
||||
Contributors will be recognized in several ways:
|
||||
|
||||
- **Contributors file**: All contributors are listed in CONTRIBUTORS.md
|
||||
- **Release notes**: Significant contributions are mentioned in release notes
|
||||
- **GitHub badges**: Active contributors receive special recognition
|
||||
- **Community showcase**: Outstanding contributions are featured in our blog
|
||||
|
||||
## Advanced Topics
|
||||
|
||||
### Maintainer Guidelines
|
||||
|
||||
For project maintainers:
|
||||
|
||||
#### Release Process
|
||||
|
||||
```bash
|
||||
# 1. Create release branch
|
||||
git checkout -b release/v1.2.0
|
||||
|
||||
# 2. Update version
|
||||
npm version 1.2.0 --no-git-tag-version
|
||||
|
||||
# 3. Update changelog
|
||||
# Edit CHANGELOG.md
|
||||
|
||||
# 4. Commit changes
|
||||
git add .
|
||||
git commit -m "chore(release): prepare v1.2.0"
|
||||
|
||||
# 5. Create PR for review
|
||||
# 6. After merge, tag release
|
||||
git tag v1.2.0
|
||||
git push origin v1.2.0
|
||||
```
|
||||
|
||||
#### Security Handling
|
||||
|
||||
For security issues:
|
||||
|
||||
1. **Do not** create public issues
|
||||
2. **Email** security@mcphub.dev
|
||||
3. **Wait** for response before disclosure
|
||||
4. **Coordinate** with maintainers on fixes
|
||||
|
||||
### Architectural Decisions
|
||||
|
||||
When making significant changes:
|
||||
|
||||
1. **Create an RFC** (Request for Comments) issue
|
||||
2. **Discuss** with the community
|
||||
3. **Get approval** from maintainers
|
||||
4. **Document** decisions in ADR (Architecture Decision Records)
|
||||
|
||||
## Thank You! 🙏
|
||||
|
||||
Thank you for taking the time to contribute to MCPHub! Every contribution, no matter how small, helps make the project better for everyone. We look forward to collaborating with you!
|
||||
244
docs/development/getting-started.mdx
Normal file
@@ -0,0 +1,244 @@
|
||||
---
|
||||
title: 'Getting Started with Development'
|
||||
description: 'Learn how to set up your development environment for MCPHub'
|
||||
---
|
||||
|
||||
# Getting Started with Development
|
||||
|
||||
This guide will help you set up your development environment for contributing to MCPHub.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Before you begin, ensure you have the following installed:
|
||||
|
||||
- **Node.js** (version 18 or higher)
|
||||
- **pnpm** (recommended package manager)
|
||||
- **Git**
|
||||
- **Docker** (optional, for containerized development)
|
||||
|
||||
## Setting Up the Development Environment
|
||||
|
||||
### 1. Clone the Repository
|
||||
|
||||
```bash
|
||||
git clone https://github.com/your-username/mcphub.git
|
||||
cd mcphub
|
||||
```
|
||||
|
||||
### 2. Install Dependencies
|
||||
|
||||
```bash
|
||||
pnpm install
|
||||
```
|
||||
|
||||
### 3. Environment Configuration
|
||||
|
||||
Create a `.env` file in the root directory:
|
||||
|
||||
```bash
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
Configure the following environment variables:
|
||||
|
||||
```env
|
||||
# Server Configuration
|
||||
PORT=3000
|
||||
NODE_ENV=development
|
||||
|
||||
# Database Configuration
|
||||
DATABASE_URL=postgresql://username:password@localhost:5432/mcphub
|
||||
|
||||
# JWT Configuration
|
||||
JWT_SECRET=your-secret-key
|
||||
JWT_EXPIRES_IN=24h
|
||||
|
||||
# OpenAI Configuration (for smart routing)
|
||||
OPENAI_API_KEY=your-openai-api-key
|
||||
```
|
||||
|
||||
### 4. Database Setup
|
||||
|
||||
If using PostgreSQL, create a database:
|
||||
|
||||
```bash
|
||||
createdb mcphub
|
||||
```
|
||||
|
||||
### 5. MCP Server Configuration
|
||||
|
||||
Create or modify `mcp_settings.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"fetch": {
|
||||
"command": "uvx",
|
||||
"args": ["mcp-server-fetch"]
|
||||
},
|
||||
"playwright": {
|
||||
"command": "npx",
|
||||
"args": ["@playwright/mcp@latest", "--headless"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Development Workflow
|
||||
|
||||
### Running the Development Server
|
||||
|
||||
Start both backend and frontend in development mode:
|
||||
|
||||
```bash
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
This will start:
|
||||
|
||||
- Backend server on `http://localhost:3000`
|
||||
- Frontend development server on `http://localhost:5173`
|
||||
|
||||
### Running Backend Only
|
||||
|
||||
```bash
|
||||
pnpm backend:dev
|
||||
```
|
||||
|
||||
### Running Frontend Only
|
||||
|
||||
```bash
|
||||
pnpm frontend:dev
|
||||
```
|
||||
|
||||
### Building the Project
|
||||
|
||||
Build both backend and frontend:
|
||||
|
||||
```bash
|
||||
pnpm build
|
||||
```
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
mcphub/
|
||||
├── src/ # Backend source code
|
||||
│ ├── controllers/ # Express controllers
|
||||
│ ├── routes/ # API routes
|
||||
│ ├── services/ # Business logic
|
||||
│ ├── models/ # Database models
|
||||
│ └── utils/ # Utility functions
|
||||
├── frontend/ # Frontend React application
|
||||
│ ├── src/
|
||||
│ │ ├── components/ # React components
|
||||
│ │ ├── pages/ # Page components
|
||||
│ │ ├── services/ # API services
|
||||
│ │ └── utils/ # Frontend utilities
|
||||
├── docs/ # Documentation
|
||||
├── bin/ # CLI scripts
|
||||
└── scripts/ # Build and utility scripts
|
||||
```
|
||||
|
||||
## Development Tools
|
||||
|
||||
### Linting and Formatting
|
||||
|
||||
```bash
|
||||
# Run ESLint
|
||||
pnpm lint
|
||||
|
||||
# Format code with Prettier
|
||||
pnpm format
|
||||
```
|
||||
|
||||
### Testing
|
||||
|
||||
```bash
|
||||
# Run tests
|
||||
pnpm test
|
||||
|
||||
# Run tests in watch mode
|
||||
pnpm test --watch
|
||||
```
|
||||
|
||||
### Debugging
|
||||
|
||||
To debug the backend with Node.js inspector:
|
||||
|
||||
```bash
|
||||
pnpm backend:debug
|
||||
```
|
||||
|
||||
Then attach your debugger to `http://localhost:9229`.
|
||||
|
||||
## Making Changes
|
||||
|
||||
### Backend Development
|
||||
|
||||
1. **Controllers**: Handle HTTP requests and responses
|
||||
2. **Services**: Implement business logic
|
||||
3. **Models**: Define database schemas
|
||||
4. **Routes**: Define API endpoints
|
||||
|
||||
### Frontend Development
|
||||
|
||||
1. **Components**: Reusable React components
|
||||
2. **Pages**: Route-specific components
|
||||
3. **Services**: API communication
|
||||
4. **Hooks**: Custom React hooks
|
||||
|
||||
### Adding New MCP Servers
|
||||
|
||||
1. Update `mcp_settings.json` with the new server configuration
|
||||
2. Test the server integration
|
||||
3. Update documentation if needed
|
||||
|
||||
## Common Development Tasks
|
||||
|
||||
### Adding a New API Endpoint
|
||||
|
||||
1. Create a controller in `src/controllers/`
|
||||
2. Define the route in `src/routes/`
|
||||
3. Add any necessary middleware
|
||||
4. Write tests for the new endpoint
|
||||
|
||||
### Adding a New Frontend Feature
|
||||
|
||||
1. Create components in `frontend/src/components/`
|
||||
2. Add routes if needed
|
||||
3. Implement API integration
|
||||
4. Style with Tailwind CSS
|
||||
|
||||
### Database Migrations
|
||||
|
||||
When modifying database schemas:
|
||||
|
||||
1. Update models in `src/models/`
|
||||
2. Create migration scripts if using TypeORM
|
||||
3. Test migrations locally
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
**Port conflicts**: Ensure ports 3000 and 5173 are available
|
||||
|
||||
**Database connection**: Verify PostgreSQL is running and credentials are correct
|
||||
|
||||
**MCP server startup**: Check server configurations in `mcp_settings.json`
|
||||
|
||||
**Permission issues**: Ensure MCP servers have necessary permissions
|
||||
|
||||
### Getting Help
|
||||
|
||||
- Check the [Contributing Guide](/development/contributing)
|
||||
- Review [Architecture Documentation](/development/architecture)
|
||||
- Open an issue on GitHub for bugs
|
||||
- Join our community discussions
|
||||
|
||||
## Next Steps
|
||||
|
||||
- Read the [Architecture Overview](/development/architecture)
|
||||
- Learn about [Contributing Guidelines](/development/contributing)
|
||||
- Explore [Configuration Options](/configuration/environment-variables)
|
||||
128
docs/docs.json
@@ -1,35 +1,92 @@
|
||||
{
|
||||
"$schema": "https://mintlify.com/docs.json",
|
||||
"theme": "mint",
|
||||
"name": "Mint Starter Kit",
|
||||
"name": "MCPHub Documentation",
|
||||
"description": "The Unified Hub for Model Context Protocol (MCP) Servers",
|
||||
"colors": {
|
||||
"primary": "#16A34A",
|
||||
"light": "#07C983",
|
||||
"dark": "#15803D"
|
||||
},
|
||||
"favicon": "/favicon.svg",
|
||||
"favicon": "/favicon.ico",
|
||||
"navigation": {
|
||||
"tabs": [
|
||||
{
|
||||
"tab": "Guides",
|
||||
"tab": "English",
|
||||
"groups": [
|
||||
{
|
||||
"group": "Get Started",
|
||||
"pages": [
|
||||
"index",
|
||||
"quickstart",
|
||||
"development"
|
||||
"installation"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Essentials",
|
||||
"group": "Core Features",
|
||||
"pages": [
|
||||
"essentials/markdown",
|
||||
"essentials/code",
|
||||
"essentials/images",
|
||||
"essentials/settings",
|
||||
"essentials/navigation",
|
||||
"essentials/reusable-snippets"
|
||||
"features/server-management",
|
||||
"features/group-management",
|
||||
"features/smart-routing",
|
||||
"features/authentication",
|
||||
"features/monitoring"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Configuration",
|
||||
"pages": [
|
||||
"configuration/mcp-settings",
|
||||
"configuration/environment-variables",
|
||||
"configuration/docker-setup",
|
||||
"configuration/nginx"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Development",
|
||||
"pages": [
|
||||
"development/getting-started",
|
||||
"development/architecture",
|
||||
"development/contributing"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tab": "中文",
|
||||
"groups": [
|
||||
{
|
||||
"group": "开始使用",
|
||||
"pages": [
|
||||
"zh/index",
|
||||
"zh/quickstart",
|
||||
"zh/installation"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "核心功能",
|
||||
"pages": [
|
||||
"zh/features/server-management",
|
||||
"zh/features/group-management",
|
||||
"zh/features/smart-routing",
|
||||
"zh/features/authentication",
|
||||
"zh/features/monitoring"
|
||||
]
|
||||
},
|
||||
{
|
||||
"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"
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -38,18 +95,22 @@
|
||||
"tab": "API Reference",
|
||||
"groups": [
|
||||
{
|
||||
"group": "API Documentation",
|
||||
"group": "MCP Endpoints",
|
||||
"pages": [
|
||||
"api-reference/introduction"
|
||||
"api-reference/introduction",
|
||||
"api-reference/mcp-http",
|
||||
"api-reference/mcp-sse",
|
||||
"api-reference/smart-routing"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Endpoint Examples",
|
||||
"group": "Management API",
|
||||
"pages": [
|
||||
"api-reference/endpoint/get",
|
||||
"api-reference/endpoint/create",
|
||||
"api-reference/endpoint/delete",
|
||||
"api-reference/endpoint/webhook"
|
||||
"api-reference/servers",
|
||||
"api-reference/groups",
|
||||
"api-reference/auth",
|
||||
"api-reference/logs",
|
||||
"api-reference/config"
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -58,19 +119,19 @@
|
||||
"global": {
|
||||
"anchors": [
|
||||
{
|
||||
"anchor": "Documentation",
|
||||
"href": "https://mintlify.com/docs",
|
||||
"icon": "book-open-cover"
|
||||
"anchor": "GitHub",
|
||||
"href": "https://github.com/samanhappy/mcphub",
|
||||
"icon": "github"
|
||||
},
|
||||
{
|
||||
"anchor": "Community",
|
||||
"href": "https://mintlify.com/community",
|
||||
"icon": "slack"
|
||||
"anchor": "Discord",
|
||||
"href": "https://discord.gg/qMKNsn5Q",
|
||||
"icon": "discord"
|
||||
},
|
||||
{
|
||||
"anchor": "Blog",
|
||||
"href": "https://mintlify.com/blog",
|
||||
"icon": "newspaper"
|
||||
"anchor": "Sponsor",
|
||||
"href": "https://ko-fi.com/samanhappy",
|
||||
"icon": "heart"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -82,21 +143,20 @@
|
||||
"navbar": {
|
||||
"links": [
|
||||
{
|
||||
"label": "Support",
|
||||
"href": "mailto:hi@mintlify.com"
|
||||
"label": "Demo",
|
||||
"href": "http://localhost:3000"
|
||||
}
|
||||
],
|
||||
"primary": {
|
||||
"type": "button",
|
||||
"label": "Dashboard",
|
||||
"href": "https://dashboard.mintlify.com"
|
||||
"label": "Get Started",
|
||||
"href": "https://docs.hubmcp.dev/quickstart"
|
||||
}
|
||||
},
|
||||
"footer": {
|
||||
"socials": {
|
||||
"x": "https://x.com/mintlify",
|
||||
"github": "https://github.com/mintlify",
|
||||
"linkedin": "https://linkedin.com/company/mintlify"
|
||||
"github": "https://github.com/samanhappy/mcphub",
|
||||
"discord": "https://discord.gg/qMKNsn5Q"
|
||||
}
|
||||
}
|
||||
}
|
||||
BIN
docs/favicon.ico
Executable file
|
After Width: | Height: | Size: 5.1 KiB |
338
docs/features/authentication.mdx
Normal file
@@ -0,0 +1,338 @@
|
||||
---
|
||||
title: 'Authentication & Security'
|
||||
description: 'Configure authentication and security settings for MCPHub'
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
MCPHub provides flexible authentication mechanisms to secure your MCP server management platform. The system supports multiple authentication methods and role-based access control.
|
||||
|
||||
## Authentication Methods
|
||||
|
||||
### Environment-based Authentication
|
||||
|
||||
Configure basic authentication using environment variables:
|
||||
|
||||
```bash
|
||||
# Basic auth credentials
|
||||
AUTH_USERNAME=admin
|
||||
AUTH_PASSWORD=your-secure-password
|
||||
|
||||
# JWT settings
|
||||
JWT_SECRET=your-jwt-secret-key
|
||||
JWT_EXPIRES_IN=24h
|
||||
```
|
||||
|
||||
### Database Authentication
|
||||
|
||||
For production deployments, enable database-backed user management:
|
||||
|
||||
```json
|
||||
{
|
||||
"auth": {
|
||||
"provider": "database",
|
||||
"database": {
|
||||
"url": "postgresql://user:pass@localhost:5432/mcphub",
|
||||
"userTable": "users"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## User Management
|
||||
|
||||
### Creating Users
|
||||
|
||||
Create users via the admin interface or API:
|
||||
|
||||
```bash
|
||||
# Via API
|
||||
curl -X POST http://localhost:3000/api/auth/users \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer $ADMIN_TOKEN" \
|
||||
-d '{
|
||||
"username": "newuser",
|
||||
"email": "user@example.com",
|
||||
"password": "securepassword",
|
||||
"role": "user"
|
||||
}'
|
||||
```
|
||||
|
||||
### User Roles
|
||||
|
||||
MCPHub supports role-based access control:
|
||||
|
||||
- **Admin**: Full system access, user management, server configuration
|
||||
- **Manager**: Server management, group management, monitoring
|
||||
- **User**: Basic server access within assigned groups
|
||||
- **Viewer**: Read-only access to assigned resources
|
||||
|
||||
## Group-based Access Control
|
||||
|
||||
### Assigning Users to Groups
|
||||
|
||||
```bash
|
||||
# Add user to group
|
||||
curl -X POST http://localhost:3000/api/groups/{groupId}/users \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-d '{"userId": "user123"}'
|
||||
```
|
||||
|
||||
### Group Permissions
|
||||
|
||||
Configure group-level permissions:
|
||||
|
||||
```json
|
||||
{
|
||||
"groupId": "dev-team",
|
||||
"permissions": {
|
||||
"servers": ["read", "write", "execute"],
|
||||
"tools": ["read", "execute"],
|
||||
"logs": ["read"],
|
||||
"config": ["read"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## API Authentication
|
||||
|
||||
### JWT Token Authentication
|
||||
|
||||
```javascript
|
||||
// Login to get token
|
||||
const response = await fetch('/api/auth/login', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
username: 'your-username',
|
||||
password: 'your-password',
|
||||
}),
|
||||
});
|
||||
|
||||
const { token } = await response.json();
|
||||
|
||||
// Use token for authenticated requests
|
||||
const serversResponse = await fetch('/api/servers', {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
});
|
||||
```
|
||||
|
||||
### API Key Authentication
|
||||
|
||||
For service-to-service communication:
|
||||
|
||||
```bash
|
||||
# Generate API key
|
||||
curl -X POST http://localhost:3000/api/auth/api-keys \
|
||||
-H "Authorization: Bearer $ADMIN_TOKEN" \
|
||||
-d '{
|
||||
"name": "my-service",
|
||||
"permissions": ["servers:read", "tools:execute"]
|
||||
}'
|
||||
|
||||
# Use API key
|
||||
curl -H "X-API-Key: your-api-key" \
|
||||
http://localhost:3000/api/servers
|
||||
```
|
||||
|
||||
## Security Configuration
|
||||
|
||||
### HTTPS Setup
|
||||
|
||||
Configure HTTPS for production:
|
||||
|
||||
```yaml
|
||||
# docker-compose.yml
|
||||
services:
|
||||
mcphub:
|
||||
environment:
|
||||
- HTTPS_ENABLED=true
|
||||
- SSL_CERT_PATH=/certs/cert.pem
|
||||
- SSL_KEY_PATH=/certs/key.pem
|
||||
volumes:
|
||||
- ./certs:/certs:ro
|
||||
```
|
||||
|
||||
### CORS Configuration
|
||||
|
||||
Configure CORS for web applications:
|
||||
|
||||
```json
|
||||
{
|
||||
"cors": {
|
||||
"origin": ["https://your-frontend.com"],
|
||||
"credentials": true,
|
||||
"methods": ["GET", "POST", "PUT", "DELETE"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Rate Limiting
|
||||
|
||||
Protect against abuse with rate limiting:
|
||||
|
||||
```json
|
||||
{
|
||||
"rateLimit": {
|
||||
"windowMs": 900000,
|
||||
"max": 100,
|
||||
"message": "Too many requests from this IP"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Session Management
|
||||
|
||||
### Session Configuration
|
||||
|
||||
```json
|
||||
{
|
||||
"session": {
|
||||
"secret": "your-session-secret",
|
||||
"cookie": {
|
||||
"secure": true,
|
||||
"httpOnly": true,
|
||||
"maxAge": 86400000
|
||||
},
|
||||
"store": "redis",
|
||||
"redis": {
|
||||
"host": "localhost",
|
||||
"port": 6379
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Logout and Session Cleanup
|
||||
|
||||
```javascript
|
||||
// Logout endpoint
|
||||
app.post('/api/auth/logout', (req, res) => {
|
||||
req.session.destroy();
|
||||
res.json({ message: 'Logged out successfully' });
|
||||
});
|
||||
```
|
||||
|
||||
## Security Best Practices
|
||||
|
||||
### Password Security
|
||||
|
||||
- Use strong password requirements
|
||||
- Implement password hashing with bcrypt
|
||||
- Support password reset functionality
|
||||
- Enable two-factor authentication (2FA)
|
||||
|
||||
### Token Security
|
||||
|
||||
- Use secure JWT secrets
|
||||
- Implement token rotation
|
||||
- Set appropriate expiration times
|
||||
- Store tokens securely in httpOnly cookies
|
||||
|
||||
### Network Security
|
||||
|
||||
- Use HTTPS in production
|
||||
- Implement proper CORS policies
|
||||
- Enable request validation
|
||||
- Use security headers (helmet.js)
|
||||
|
||||
### Monitoring Security Events
|
||||
|
||||
```javascript
|
||||
// Log security events
|
||||
const auditLog = {
|
||||
event: 'login_attempt',
|
||||
user: username,
|
||||
ip: req.ip,
|
||||
userAgent: req.headers['user-agent'],
|
||||
success: true,
|
||||
timestamp: new Date(),
|
||||
};
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Authentication Issues
|
||||
|
||||
**Invalid Credentials**
|
||||
|
||||
```bash
|
||||
# Check user exists and password is correct
|
||||
curl -X POST http://localhost:3000/api/auth/verify \
|
||||
-d '{"username": "user", "password": "pass"}'
|
||||
```
|
||||
|
||||
**Token Expiration**
|
||||
|
||||
```javascript
|
||||
// Handle token refresh
|
||||
if (response.status === 401) {
|
||||
const newToken = await refreshToken();
|
||||
// Retry request with new token
|
||||
}
|
||||
```
|
||||
|
||||
**Permission Denied**
|
||||
|
||||
```bash
|
||||
# Check user permissions
|
||||
curl -H "Authorization: Bearer $TOKEN" \
|
||||
http://localhost:3000/api/auth/permissions
|
||||
```
|
||||
|
||||
### Debug Authentication
|
||||
|
||||
Enable authentication debugging:
|
||||
|
||||
```bash
|
||||
DEBUG=mcphub:auth npm start
|
||||
```
|
||||
|
||||
## Integration Examples
|
||||
|
||||
### Frontend Integration
|
||||
|
||||
```javascript
|
||||
// React authentication hook
|
||||
const useAuth = () => {
|
||||
const [user, setUser] = useState(null);
|
||||
|
||||
const login = async (credentials) => {
|
||||
const response = await fetch('/api/auth/login', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(credentials),
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const userData = await response.json();
|
||||
setUser(userData.user);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
return { user, login };
|
||||
};
|
||||
```
|
||||
|
||||
### Middleware Integration
|
||||
|
||||
```javascript
|
||||
// Express middleware
|
||||
const authMiddleware = (req, res, next) => {
|
||||
const token = req.headers.authorization?.split(' ')[1];
|
||||
|
||||
if (!token) {
|
||||
return res.status(401).json({ error: 'No token provided' });
|
||||
}
|
||||
|
||||
try {
|
||||
const decoded = jwt.verify(token, process.env.JWT_SECRET);
|
||||
req.user = decoded;
|
||||
next();
|
||||
} catch (error) {
|
||||
res.status(401).json({ error: 'Invalid token' });
|
||||
}
|
||||
};
|
||||
```
|
||||
588
docs/features/group-management.mdx
Normal file
@@ -0,0 +1,588 @@
|
||||
---
|
||||
title: 'Group Management'
|
||||
description: 'Organize servers into logical groups for streamlined access control'
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
Group Management in MCPHub allows you to organize your MCP servers into logical collections based on functionality, use case, or access requirements. This enables fine-grained control over which tools are available to different AI clients and users.
|
||||
|
||||
## Core Concepts
|
||||
|
||||
### What are Groups?
|
||||
|
||||
Groups are named collections of MCP servers that can be accessed through dedicated endpoints. Instead of connecting to all servers at once, AI clients can connect to specific groups to access only relevant tools.
|
||||
|
||||
### Benefits of Groups
|
||||
|
||||
- **Focused Tool Access**: AI clients see only relevant tools for their use case
|
||||
- **Better Performance**: Reduced tool discovery overhead
|
||||
- **Enhanced Security**: Limit access to sensitive tools
|
||||
- **Improved Organization**: Logical separation of functionality
|
||||
- **Simplified Management**: Easier to manage related servers together
|
||||
|
||||
## Creating Groups
|
||||
|
||||
### Via Dashboard
|
||||
|
||||
1. **Navigate to Groups Section**: Click "Groups" in the main navigation
|
||||
2. **Click "Create Group"**: Start the group creation process
|
||||
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
|
||||
|
||||
### Via API
|
||||
|
||||
Create groups programmatically:
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:3000/api/groups \
|
||||
-H "Content-Type: application/json" \
|
||||
-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"
|
||||
}'
|
||||
```
|
||||
|
||||
### Via Configuration File
|
||||
|
||||
Define groups in your `mcp_settings.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"fetch": { "command": "uvx", "args": ["mcp-server-fetch"] },
|
||||
"playwright": { "command": "npx", "args": ["@playwright/mcp@latest"] },
|
||||
"slack": { "command": "npx", "args": ["@modelcontextprotocol/server-slack"] }
|
||||
},
|
||||
"groups": {
|
||||
"web-tools": {
|
||||
"displayName": "Web Tools",
|
||||
"description": "Web scraping and browser automation",
|
||||
"servers": ["fetch", "playwright"],
|
||||
"accessLevel": "public"
|
||||
},
|
||||
"communication": {
|
||||
"displayName": "Communication Tools",
|
||||
"description": "Messaging and collaboration tools",
|
||||
"servers": ["slack"],
|
||||
"accessLevel": "private"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Group Types and Use Cases
|
||||
|
||||
<AccordionGroup>
|
||||
<Accordion title="Web Automation Group">
|
||||
**Purpose**: Browser automation and web scraping
|
||||
|
||||
**Servers**:
|
||||
- `playwright`: Browser automation
|
||||
- `fetch`: HTTP requests and web scraping
|
||||
- `selenium`: Alternative browser automation
|
||||
|
||||
**Use Cases**:
|
||||
- Automated testing
|
||||
- Data collection
|
||||
- Web monitoring
|
||||
- Content analysis
|
||||
|
||||
**Endpoint**: `http://localhost:3000/mcp/web-automation`
|
||||
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Data Processing Group">
|
||||
**Purpose**: Data manipulation and analysis
|
||||
|
||||
**Servers**:
|
||||
- `sqlite`: Database operations
|
||||
- `filesystem`: File operations
|
||||
- `spreadsheet`: Excel/CSV processing
|
||||
|
||||
**Use Cases**:
|
||||
- Data analysis
|
||||
- Report generation
|
||||
- File processing
|
||||
- Database queries
|
||||
|
||||
**Endpoint**: `http://localhost:3000/mcp/data-processing`
|
||||
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Communication Group">
|
||||
**Purpose**: Messaging and collaboration
|
||||
|
||||
**Servers**:
|
||||
- `slack`: Slack integration
|
||||
- `discord`: Discord bot
|
||||
- `email`: Email sending
|
||||
- `sms`: SMS notifications
|
||||
|
||||
**Use Cases**:
|
||||
- Team notifications
|
||||
- Customer communication
|
||||
- Alert systems
|
||||
- Social media management
|
||||
|
||||
**Endpoint**: `http://localhost:3000/mcp/communication`
|
||||
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Development Group">
|
||||
**Purpose**: Software development tools
|
||||
|
||||
**Servers**:
|
||||
- `github`: GitHub operations
|
||||
- `gitlab`: GitLab integration
|
||||
- `docker`: Container management
|
||||
- `kubernetes`: K8s operations
|
||||
|
||||
**Use Cases**:
|
||||
- Code deployment
|
||||
- Repository management
|
||||
- CI/CD operations
|
||||
- Infrastructure management
|
||||
|
||||
**Endpoint**: `http://localhost:3000/mcp/development`
|
||||
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="AI/ML Group">
|
||||
**Purpose**: Machine learning and AI tools
|
||||
|
||||
**Servers**:
|
||||
- `openai`: OpenAI API integration
|
||||
- `huggingface`: Hugging Face models
|
||||
- `vector-db`: Vector database operations
|
||||
|
||||
**Use Cases**:
|
||||
- Model inference
|
||||
- Data embeddings
|
||||
- Natural language processing
|
||||
- Computer vision
|
||||
|
||||
**Endpoint**: `http://localhost:3000/mcp/ai-ml`
|
||||
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
|
||||
## Group Access Control
|
||||
|
||||
### Access Levels
|
||||
|
||||
<Tabs>
|
||||
<Tab title="Public">
|
||||
**Public Groups**:
|
||||
- Accessible to all authenticated users
|
||||
- No additional permissions required
|
||||
- Visible in group listings
|
||||
- Default access level
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "public-tools",
|
||||
"accessLevel": "public",
|
||||
"servers": ["fetch", "calculator"]
|
||||
}
|
||||
```
|
||||
|
||||
</Tab>
|
||||
|
||||
<Tab title="Private">
|
||||
**Private Groups**:
|
||||
- Only visible to group members
|
||||
- Requires explicit user assignment
|
||||
- Hidden from public listings
|
||||
- Admin-controlled membership
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "internal-tools",
|
||||
"accessLevel": "private",
|
||||
"members": ["user1", "user2"],
|
||||
"servers": ["internal-api", "database"]
|
||||
}
|
||||
```
|
||||
|
||||
</Tab>
|
||||
|
||||
<Tab title="Restricted">
|
||||
**Restricted Groups**:
|
||||
- Role-based access control
|
||||
- Requires specific permissions
|
||||
- Audit logging enabled
|
||||
- Time-limited access
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "admin-tools",
|
||||
"accessLevel": "restricted",
|
||||
"requiredRoles": ["admin", "operator"],
|
||||
"servers": ["system-control", "user-management"]
|
||||
}
|
||||
```
|
||||
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
### User Management
|
||||
|
||||
Assign users to groups:
|
||||
|
||||
```bash
|
||||
# Add user to group
|
||||
curl -X POST http://localhost:3000/api/groups/web-tools/members \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer YOUR_JWT_TOKEN" \
|
||||
-d '{"userId": "user123"}'
|
||||
|
||||
# Remove user from group
|
||||
curl -X DELETE http://localhost:3000/api/groups/web-tools/members/user123 \
|
||||
-H "Authorization: Bearer YOUR_JWT_TOKEN"
|
||||
|
||||
# List group members
|
||||
curl http://localhost:3000/api/groups/web-tools/members \
|
||||
-H "Authorization: Bearer YOUR_JWT_TOKEN"
|
||||
```
|
||||
|
||||
## Group Endpoints
|
||||
|
||||
### Accessing Groups
|
||||
|
||||
Each group gets its own MCP endpoint:
|
||||
|
||||
<Tabs>
|
||||
<Tab title="HTTP MCP">
|
||||
```
|
||||
http://localhost:3000/mcp/{group-name}
|
||||
```
|
||||
|
||||
Examples:
|
||||
- `http://localhost:3000/mcp/web-tools`
|
||||
- `http://localhost:3000/mcp/data-processing`
|
||||
- `http://localhost:3000/mcp/communication`
|
||||
|
||||
</Tab>
|
||||
|
||||
<Tab title="SSE (Legacy)">
|
||||
```
|
||||
http://localhost:3000/sse/{group-name}
|
||||
```
|
||||
|
||||
Examples:
|
||||
- `http://localhost:3000/sse/web-tools`
|
||||
- `http://localhost:3000/sse/data-processing`
|
||||
- `http://localhost:3000/sse/communication`
|
||||
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
### Group Tool Discovery
|
||||
|
||||
When connecting to a group endpoint, AI clients will only see tools from servers within that group:
|
||||
|
||||
```bash
|
||||
# List tools in web-tools group
|
||||
curl -X POST http://localhost:3000/mcp/web-tools \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"jsonrpc": "2.0",
|
||||
"id": 1,
|
||||
"method": "tools/list",
|
||||
"params": {}
|
||||
}'
|
||||
```
|
||||
|
||||
Response will only include tools from `fetch` and `playwright` servers.
|
||||
|
||||
## Dynamic Group Management
|
||||
|
||||
### Adding Servers to Groups
|
||||
|
||||
<Tabs>
|
||||
<Tab title="Dashboard">
|
||||
1. Navigate to the group in the dashboard
|
||||
2. Click "Manage Servers"
|
||||
3. Select additional servers to add
|
||||
4. Click "Save Changes"
|
||||
</Tab>
|
||||
|
||||
<Tab title="API">
|
||||
```bash
|
||||
curl -X POST http://localhost:3000/api/groups/web-tools/servers \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer YOUR_JWT_TOKEN" \
|
||||
-d '{"serverId": "new-server"}'
|
||||
```
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
### Removing Servers from Groups
|
||||
|
||||
<Tabs>
|
||||
<Tab title="Dashboard">
|
||||
1. Navigate to the group in the dashboard
|
||||
2. Click "Manage Servers"
|
||||
3. Unselect servers to remove
|
||||
4. Click "Save Changes"
|
||||
</Tab>
|
||||
|
||||
<Tab title="API">
|
||||
```bash
|
||||
curl -X DELETE http://localhost:3000/api/groups/web-tools/servers/server-name \
|
||||
-H "Authorization: Bearer YOUR_JWT_TOKEN"
|
||||
```
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
### Batch Server Updates
|
||||
|
||||
Update multiple servers at once:
|
||||
|
||||
```bash
|
||||
curl -X PUT http://localhost:3000/api/groups/web-tools/servers \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer YOUR_JWT_TOKEN" \
|
||||
-d '{
|
||||
"servers": ["fetch", "playwright", "selenium"]
|
||||
}'
|
||||
```
|
||||
|
||||
## Group Monitoring
|
||||
|
||||
### Group Status
|
||||
|
||||
Monitor group health and activity:
|
||||
|
||||
```bash
|
||||
# Get group status
|
||||
curl http://localhost:3000/api/groups/web-tools/status \
|
||||
-H "Authorization: Bearer YOUR_JWT_TOKEN"
|
||||
```
|
||||
|
||||
Response includes:
|
||||
|
||||
- Number of active servers
|
||||
- Tool count
|
||||
- Active connections
|
||||
- Recent activity
|
||||
|
||||
### Group Analytics
|
||||
|
||||
Track group usage:
|
||||
|
||||
```bash
|
||||
# Get group analytics
|
||||
curl http://localhost:3000/api/groups/web-tools/analytics \
|
||||
-H "Authorization: Bearer YOUR_JWT_TOKEN"
|
||||
```
|
||||
|
||||
Metrics include:
|
||||
|
||||
- Request count by tool
|
||||
- Response times
|
||||
- Error rates
|
||||
- User activity
|
||||
|
||||
## Advanced Group Features
|
||||
|
||||
### Nested Groups
|
||||
|
||||
Create hierarchical group structures:
|
||||
|
||||
```json
|
||||
{
|
||||
"groups": {
|
||||
"development": {
|
||||
"displayName": "Development Tools",
|
||||
"subGroups": ["frontend-dev", "backend-dev", "devops"]
|
||||
},
|
||||
"frontend-dev": {
|
||||
"displayName": "Frontend Development",
|
||||
"servers": ["playwright", "webpack-server"],
|
||||
"parent": "development"
|
||||
},
|
||||
"backend-dev": {
|
||||
"displayName": "Backend Development",
|
||||
"servers": ["database", "api-server"],
|
||||
"parent": "development"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Group Templates
|
||||
|
||||
Use templates for quick group creation:
|
||||
|
||||
```json
|
||||
{
|
||||
"groupTemplates": {
|
||||
"web-project": {
|
||||
"description": "Standard web project toolset",
|
||||
"servers": ["fetch", "playwright", "filesystem"],
|
||||
"accessLevel": "public"
|
||||
},
|
||||
"data-science": {
|
||||
"description": "Data science and ML tools",
|
||||
"servers": ["python-tools", "jupyter", "vector-db"],
|
||||
"accessLevel": "private"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Apply template:
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:3000/api/groups/from-template \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer YOUR_JWT_TOKEN" \
|
||||
-d '{
|
||||
"name": "my-web-project",
|
||||
"template": "web-project",
|
||||
"displayName": "My Web Project Tools"
|
||||
}'
|
||||
```
|
||||
|
||||
### Group Policies
|
||||
|
||||
Define policies for group behavior:
|
||||
|
||||
```json
|
||||
{
|
||||
"groupPolicies": {
|
||||
"web-tools": {
|
||||
"maxConcurrentConnections": 10,
|
||||
"requestTimeout": 30000,
|
||||
"rateLimiting": {
|
||||
"requestsPerMinute": 100,
|
||||
"burstLimit": 20
|
||||
},
|
||||
"allowedOrigins": ["localhost", "myapp.com"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### Group Organization
|
||||
|
||||
<Tip>
|
||||
**Organize by Use Case**: Group servers based on what users want to accomplish, not just technical
|
||||
similarity.
|
||||
</Tip>
|
||||
|
||||
<Tip>
|
||||
**Keep Groups Focused**: Avoid creating groups with too many diverse tools. Smaller, focused
|
||||
groups are more useful.
|
||||
</Tip>
|
||||
|
||||
<Tip>
|
||||
**Use Descriptive Names**: Choose names that clearly indicate the group's purpose and contents.
|
||||
</Tip>
|
||||
|
||||
### Security Considerations
|
||||
|
||||
<Warning>
|
||||
**Principle of Least Privilege**: Only give users access to groups they actually need.
|
||||
</Warning>
|
||||
|
||||
<Warning>
|
||||
**Sensitive Tool Isolation**: Keep sensitive tools in restricted groups with proper access
|
||||
controls.
|
||||
</Warning>
|
||||
|
||||
<Warning>
|
||||
**Regular Access Reviews**: Periodically review group memberships and remove unnecessary access.
|
||||
</Warning>
|
||||
|
||||
### Performance Optimization
|
||||
|
||||
<Info>
|
||||
**Balance Group Size**: Very large groups may have slower tool discovery. Consider splitting into
|
||||
smaller groups.
|
||||
</Info>
|
||||
|
||||
<Info>
|
||||
**Monitor Usage**: Use analytics to identify which groups are heavily used and optimize
|
||||
accordingly.
|
||||
</Info>
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
<AccordionGroup>
|
||||
<Accordion title="Group Not Accessible">
|
||||
**Check:**
|
||||
- User has proper permissions
|
||||
- Group exists and is active
|
||||
- Servers in group are running
|
||||
- Network connectivity
|
||||
|
||||
**Solutions:**
|
||||
1. Verify user group membership
|
||||
2. Check group configuration
|
||||
3. Test individual server connections
|
||||
4. Review access logs
|
||||
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Tools Missing from Group">
|
||||
**Possible causes:**
|
||||
- Server not properly added to group
|
||||
- Server is not running
|
||||
- Tool discovery failed
|
||||
- Caching issues
|
||||
|
||||
**Debug steps:**
|
||||
1. Verify server is in group configuration
|
||||
2. Check server status
|
||||
3. Force refresh tool discovery
|
||||
4. Clear group cache
|
||||
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Group Performance Issues">
|
||||
**Common issues:**
|
||||
- Too many servers in group
|
||||
- Slow server responses
|
||||
- Network latency
|
||||
- Resource constraints
|
||||
|
||||
**Optimizations:**
|
||||
1. Split large groups
|
||||
2. Monitor server performance
|
||||
3. Implement request caching
|
||||
4. Use connection pooling
|
||||
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
|
||||
## Next Steps
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="Smart Routing" icon="route" href="/features/smart-routing">
|
||||
AI-powered tool discovery across groups
|
||||
</Card>
|
||||
<Card title="Authentication" icon="shield" href="/features/authentication">
|
||||
User management and access control
|
||||
</Card>
|
||||
<Card title="API Reference" icon="code" href="/api-reference/groups">
|
||||
Complete group management API
|
||||
</Card>
|
||||
<Card title="Configuration" icon="cog" href="/configuration/mcp-settings">
|
||||
Advanced group configuration options
|
||||
</Card>
|
||||
</CardGroup>
|
||||
526
docs/features/monitoring.mdx
Normal file
@@ -0,0 +1,526 @@
|
||||
---
|
||||
title: 'Monitoring & Logging'
|
||||
description: 'Monitor your MCP servers and analyze system logs with MCPHub'
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
MCPHub provides comprehensive monitoring and logging capabilities to help you track server performance, debug issues, and maintain system health.
|
||||
|
||||
## Real-time Monitoring
|
||||
|
||||
### Server Status Dashboard
|
||||
|
||||
The MCPHub dashboard provides real-time monitoring of all registered MCP servers:
|
||||
|
||||
- **Server Health**: Online/offline status with automatic health checks
|
||||
- **Response Times**: Average, min, max response times per server
|
||||
- **Request Volume**: Requests per second/minute/hour
|
||||
- **Error Rates**: Success/failure ratios and error trends
|
||||
- **Resource Usage**: Memory and CPU utilization (when available)
|
||||
|
||||
### Health Check Configuration
|
||||
|
||||
Configure health checks for your MCP servers:
|
||||
|
||||
```json
|
||||
{
|
||||
"healthCheck": {
|
||||
"enabled": true,
|
||||
"interval": 30000,
|
||||
"timeout": 5000,
|
||||
"retries": 3,
|
||||
"endpoints": {
|
||||
"ping": "/health",
|
||||
"tools": "/tools/list"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Monitoring API
|
||||
|
||||
Get monitoring data programmatically:
|
||||
|
||||
```bash
|
||||
# Get server health status
|
||||
curl http://localhost:3000/api/monitoring/health
|
||||
|
||||
# Get performance metrics
|
||||
curl http://localhost:3000/api/monitoring/metrics?server=my-server&range=1h
|
||||
|
||||
# Get system overview
|
||||
curl http://localhost:3000/api/monitoring/overview
|
||||
```
|
||||
|
||||
## Logging System
|
||||
|
||||
### Log Levels
|
||||
|
||||
MCPHub supports standard log levels:
|
||||
|
||||
- **ERROR**: Critical errors requiring immediate attention
|
||||
- **WARN**: Warning conditions that should be monitored
|
||||
- **INFO**: General operational messages
|
||||
- **DEBUG**: Detailed debugging information
|
||||
- **TRACE**: Very detailed trace information
|
||||
|
||||
### Log Configuration
|
||||
|
||||
Configure logging in your environment:
|
||||
|
||||
```bash
|
||||
# Set log level
|
||||
LOG_LEVEL=info
|
||||
|
||||
# Enable structured logging
|
||||
LOG_FORMAT=json
|
||||
|
||||
# Log file location
|
||||
LOG_FILE=/var/log/mcphub/app.log
|
||||
|
||||
# Enable request logging
|
||||
ENABLE_REQUEST_LOGS=true
|
||||
```
|
||||
|
||||
### Structured Logging
|
||||
|
||||
MCPHub uses structured logging for better analysis:
|
||||
|
||||
```json
|
||||
{
|
||||
"timestamp": "2024-01-20T10:30:00Z",
|
||||
"level": "info",
|
||||
"message": "MCP server request completed",
|
||||
"server": "github-mcp",
|
||||
"tool": "search_repositories",
|
||||
"duration": 245,
|
||||
"status": "success",
|
||||
"requestId": "req_123456",
|
||||
"userId": "user_789"
|
||||
}
|
||||
```
|
||||
|
||||
## Log Management
|
||||
|
||||
### Log Storage Options
|
||||
|
||||
#### File-based Logging
|
||||
|
||||
```yaml
|
||||
# docker-compose.yml
|
||||
services:
|
||||
mcphub:
|
||||
volumes:
|
||||
- ./logs:/app/logs
|
||||
environment:
|
||||
- LOG_FILE=/app/logs/mcphub.log
|
||||
- LOG_ROTATION=daily
|
||||
- LOG_MAX_SIZE=100MB
|
||||
- LOG_MAX_FILES=7
|
||||
```
|
||||
|
||||
#### Database Logging
|
||||
|
||||
```json
|
||||
{
|
||||
"logging": {
|
||||
"database": {
|
||||
"enabled": true,
|
||||
"table": "logs",
|
||||
"retention": "30d",
|
||||
"indexes": ["timestamp", "level", "server"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### External Log Services
|
||||
|
||||
```bash
|
||||
# Syslog integration
|
||||
SYSLOG_ENABLED=true
|
||||
SYSLOG_HOST=localhost
|
||||
SYSLOG_PORT=514
|
||||
SYSLOG_FACILITY=local0
|
||||
|
||||
# ELK Stack integration
|
||||
ELASTICSEARCH_URL=http://localhost:9200
|
||||
ELASTICSEARCH_INDEX=mcphub-logs
|
||||
```
|
||||
|
||||
### Log Rotation
|
||||
|
||||
Automatic log rotation configuration:
|
||||
|
||||
```json
|
||||
{
|
||||
"logRotation": {
|
||||
"enabled": true,
|
||||
"maxSize": "100MB",
|
||||
"maxFiles": 10,
|
||||
"compress": true,
|
||||
"interval": "daily"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Metrics Collection
|
||||
|
||||
### System Metrics
|
||||
|
||||
MCPHub collects various system metrics:
|
||||
|
||||
```javascript
|
||||
// Example metrics collected
|
||||
{
|
||||
"timestamp": "2024-01-20T10:30:00Z",
|
||||
"metrics": {
|
||||
"requests": {
|
||||
"total": 1547,
|
||||
"success": 1523,
|
||||
"errors": 24,
|
||||
"rate": 12.5
|
||||
},
|
||||
"servers": {
|
||||
"online": 8,
|
||||
"offline": 2,
|
||||
"total": 10
|
||||
},
|
||||
"performance": {
|
||||
"avgResponseTime": 156,
|
||||
"p95ResponseTime": 324,
|
||||
"p99ResponseTime": 567
|
||||
},
|
||||
"system": {
|
||||
"memoryUsage": "245MB",
|
||||
"cpuUsage": "15%",
|
||||
"uptime": "72h 35m"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Custom Metrics
|
||||
|
||||
Add custom metrics for your use case:
|
||||
|
||||
```javascript
|
||||
// Custom metric example
|
||||
const customMetric = {
|
||||
name: 'tool_usage',
|
||||
type: 'counter',
|
||||
tags: {
|
||||
server: 'github-mcp',
|
||||
tool: 'search_repositories',
|
||||
result: 'success',
|
||||
},
|
||||
value: 1,
|
||||
};
|
||||
|
||||
// Send to metrics endpoint
|
||||
await fetch('/api/monitoring/metrics', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(customMetric),
|
||||
});
|
||||
```
|
||||
|
||||
## Alerting
|
||||
|
||||
### Alert Configuration
|
||||
|
||||
Set up alerts for critical conditions:
|
||||
|
||||
```json
|
||||
{
|
||||
"alerts": {
|
||||
"serverDown": {
|
||||
"condition": "server.status == 'offline'",
|
||||
"duration": "5m",
|
||||
"severity": "critical",
|
||||
"channels": ["email", "slack"]
|
||||
},
|
||||
"highErrorRate": {
|
||||
"condition": "errors.rate > 0.1",
|
||||
"duration": "2m",
|
||||
"severity": "warning",
|
||||
"channels": ["slack"]
|
||||
},
|
||||
"slowResponse": {
|
||||
"condition": "response.p95 > 1000",
|
||||
"duration": "5m",
|
||||
"severity": "warning",
|
||||
"channels": ["email"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Notification Channels
|
||||
|
||||
#### Email Notifications
|
||||
|
||||
```bash
|
||||
# Email configuration
|
||||
SMTP_HOST=smtp.gmail.com
|
||||
SMTP_PORT=587
|
||||
SMTP_USER=alerts@yourcompany.com
|
||||
SMTP_PASS=your-app-password
|
||||
ALERT_EMAIL_TO=admin@yourcompany.com
|
||||
```
|
||||
|
||||
#### Slack Integration
|
||||
|
||||
```bash
|
||||
# Slack webhook
|
||||
SLACK_WEBHOOK_URL=https://hooks.slack.com/services/...
|
||||
SLACK_CHANNEL=#mcphub-alerts
|
||||
```
|
||||
|
||||
#### Webhook Notifications
|
||||
|
||||
```json
|
||||
{
|
||||
"webhooks": [
|
||||
{
|
||||
"url": "https://your-service.com/webhooks/mcphub",
|
||||
"events": ["server.down", "error.rate.high"],
|
||||
"headers": {
|
||||
"Authorization": "Bearer your-token"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Log Analysis
|
||||
|
||||
### Query Logs
|
||||
|
||||
Use the logs API to query and analyze logs:
|
||||
|
||||
```bash
|
||||
# Get recent errors
|
||||
curl "http://localhost:3000/api/logs?level=error&since=1h"
|
||||
|
||||
# Search logs by server
|
||||
curl "http://localhost:3000/api/logs?server=github-mcp&limit=100"
|
||||
|
||||
# Get logs for specific request
|
||||
curl "http://localhost:3000/api/logs?requestId=req_123456"
|
||||
|
||||
# Filter by time range
|
||||
curl "http://localhost:3000/api/logs?from=2024-01-20T00:00:00Z&to=2024-01-20T23:59:59Z"
|
||||
```
|
||||
|
||||
### Log Aggregation
|
||||
|
||||
Aggregate logs for insights:
|
||||
|
||||
```bash
|
||||
# Error summary by server
|
||||
curl "http://localhost:3000/api/logs/aggregate?groupBy=server&level=error&since=24h"
|
||||
|
||||
# Request volume over time
|
||||
curl "http://localhost:3000/api/logs/aggregate?groupBy=hour&type=request&since=7d"
|
||||
```
|
||||
|
||||
## Performance Monitoring
|
||||
|
||||
### Response Time Tracking
|
||||
|
||||
Monitor MCP server response times:
|
||||
|
||||
```javascript
|
||||
// Response time metrics
|
||||
{
|
||||
"server": "github-mcp",
|
||||
"tool": "search_repositories",
|
||||
"metrics": {
|
||||
"calls": 156,
|
||||
"avgTime": 234,
|
||||
"minTime": 89,
|
||||
"maxTime": 1205,
|
||||
"p50": 201,
|
||||
"p95": 567,
|
||||
"p99": 892
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Error Rate Monitoring
|
||||
|
||||
Track error rates and patterns:
|
||||
|
||||
```bash
|
||||
# Get error rates by server
|
||||
curl "http://localhost:3000/api/monitoring/errors?groupBy=server&since=1h"
|
||||
|
||||
# Get error details
|
||||
curl "http://localhost:3000/api/monitoring/errors?server=github-mcp&details=true"
|
||||
```
|
||||
|
||||
## Integration with External Tools
|
||||
|
||||
### Prometheus Integration
|
||||
|
||||
Export metrics to Prometheus:
|
||||
|
||||
```yaml
|
||||
# prometheus.yml
|
||||
scrape_configs:
|
||||
- job_name: 'mcphub'
|
||||
static_configs:
|
||||
- targets: ['localhost:3000']
|
||||
metrics_path: '/api/monitoring/prometheus'
|
||||
scrape_interval: 30s
|
||||
```
|
||||
|
||||
### Grafana Dashboards
|
||||
|
||||
Import MCPHub Grafana dashboard:
|
||||
|
||||
```json
|
||||
{
|
||||
"dashboard": {
|
||||
"title": "MCPHub Monitoring",
|
||||
"panels": [
|
||||
{
|
||||
"title": "Server Status",
|
||||
"type": "stat",
|
||||
"targets": [
|
||||
{
|
||||
"expr": "mcphub_servers_online",
|
||||
"legendFormat": "Online"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Request Rate",
|
||||
"type": "graph",
|
||||
"targets": [
|
||||
{
|
||||
"expr": "rate(mcphub_requests_total[5m])",
|
||||
"legendFormat": "Requests/sec"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### ELK Stack Integration
|
||||
|
||||
Configure Logstash for log processing:
|
||||
|
||||
```ruby
|
||||
# logstash.conf
|
||||
input {
|
||||
beats {
|
||||
port => 5044
|
||||
}
|
||||
}
|
||||
|
||||
filter {
|
||||
if [fields][service] == "mcphub" {
|
||||
json {
|
||||
source => "message"
|
||||
}
|
||||
|
||||
date {
|
||||
match => [ "timestamp", "ISO8601" ]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
output {
|
||||
elasticsearch {
|
||||
hosts => ["localhost:9200"]
|
||||
index => "mcphub-logs-%{+YYYY.MM.dd}"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Monitoring Issues
|
||||
|
||||
**Missing Metrics**
|
||||
|
||||
```bash
|
||||
# Check metrics endpoint
|
||||
curl http://localhost:3000/api/monitoring/health
|
||||
|
||||
# Verify configuration
|
||||
grep -r "monitoring" /path/to/config/
|
||||
```
|
||||
|
||||
**Log File Issues**
|
||||
|
||||
```bash
|
||||
# Check log file permissions
|
||||
ls -la /var/log/mcphub/
|
||||
|
||||
# Verify disk space
|
||||
df -h /var/log/
|
||||
|
||||
# Check log rotation
|
||||
logrotate -d /etc/logrotate.d/mcphub
|
||||
```
|
||||
|
||||
**Performance Issues**
|
||||
|
||||
```bash
|
||||
# Monitor system resources
|
||||
top -p $(pgrep -f mcphub)
|
||||
|
||||
# Check database connections
|
||||
curl http://localhost:3000/api/monitoring/database
|
||||
|
||||
# Analyze slow queries
|
||||
curl http://localhost:3000/api/monitoring/slow-queries
|
||||
```
|
||||
|
||||
### Debug Mode
|
||||
|
||||
Enable debug logging for troubleshooting:
|
||||
|
||||
```bash
|
||||
# Enable debug mode
|
||||
DEBUG=mcphub:* npm start
|
||||
|
||||
# Or set environment variable
|
||||
export DEBUG=mcphub:monitoring,mcphub:logging
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### Log Management
|
||||
|
||||
- Use structured logging with consistent formats
|
||||
- Implement proper log levels and filtering
|
||||
- Set up log rotation and retention policies
|
||||
- Monitor log file sizes and disk usage
|
||||
|
||||
### Monitoring Setup
|
||||
|
||||
- Configure appropriate health check intervals
|
||||
- Set up alerts for critical conditions
|
||||
- Monitor both system and application metrics
|
||||
- Use dashboards for visual monitoring
|
||||
|
||||
### Performance Optimization
|
||||
|
||||
- Index log database tables appropriately
|
||||
- Use log sampling for high-volume scenarios
|
||||
- Implement proper caching for metrics
|
||||
- Regular cleanup of old logs and metrics
|
||||
|
||||
### Security Considerations
|
||||
|
||||
- Sanitize sensitive data in logs
|
||||
- Secure access to monitoring endpoints
|
||||
- Use authentication for external integrations
|
||||
- Encrypt log transmission when using external services
|
||||
509
docs/features/server-management.mdx
Normal file
@@ -0,0 +1,509 @@
|
||||
---
|
||||
title: 'Server Management'
|
||||
description: 'Centrally manage multiple MCP servers with hot-swappable configuration'
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
MCPHub's server management system allows you to centrally configure, monitor, and control multiple MCP (Model Context Protocol) servers from a single dashboard. All changes are applied in real-time without requiring server restarts.
|
||||
|
||||
## Adding MCP Servers
|
||||
|
||||
### Via Dashboard
|
||||
|
||||
1. **Access the Dashboard**: Navigate to `http://localhost:3000` and log in
|
||||
2. **Click "Add Server"**: Located in the servers section
|
||||
3. **Fill Server Details**:
|
||||
- **Name**: Unique identifier for the server
|
||||
- **Command**: Executable command (e.g., `npx`, `uvx`, `python`)
|
||||
- **Arguments**: Array of command arguments
|
||||
- **Environment Variables**: Key-value pairs for environment setup
|
||||
- **Working Directory**: Optional working directory for the command
|
||||
|
||||
### Via Configuration File
|
||||
|
||||
Edit your `mcp_settings.json` file:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"server-name": {
|
||||
"command": "command-to-run",
|
||||
"args": ["arg1", "arg2"],
|
||||
"env": {
|
||||
"API_KEY": "your-api-key",
|
||||
"CONFIG_VALUE": "some-value"
|
||||
},
|
||||
"cwd": "/optional/working/directory"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Via API
|
||||
|
||||
Use the REST API to add servers programmatically:
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:3000/api/servers \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer YOUR_JWT_TOKEN" \
|
||||
-d '{
|
||||
"name": "fetch-server",
|
||||
"command": "uvx",
|
||||
"args": ["mcp-server-fetch"],
|
||||
"env": {}
|
||||
}'
|
||||
```
|
||||
|
||||
## Popular MCP Server Examples
|
||||
|
||||
<AccordionGroup>
|
||||
<Accordion title="Web Fetch Server">
|
||||
Provides web scraping and HTTP request capabilities:
|
||||
|
||||
```json
|
||||
{
|
||||
"fetch": {
|
||||
"command": "uvx",
|
||||
"args": ["mcp-server-fetch"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Available Tools:**
|
||||
- `fetch`: Make HTTP requests
|
||||
- `fetch_html`: Scrape web pages
|
||||
- `fetch_json`: Get JSON data from APIs
|
||||
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Playwright Browser Automation">
|
||||
Browser automation for web interactions:
|
||||
|
||||
```json
|
||||
{
|
||||
"playwright": {
|
||||
"command": "npx",
|
||||
"args": ["@playwright/mcp@latest", "--headless"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Available Tools:**
|
||||
- `playwright_navigate`: Navigate to web pages
|
||||
- `playwright_screenshot`: Take screenshots
|
||||
- `playwright_click`: Click elements
|
||||
- `playwright_fill`: Fill forms
|
||||
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="File System Operations">
|
||||
File and directory management:
|
||||
|
||||
```json
|
||||
{
|
||||
"filesystem": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "@modelcontextprotocol/server-filesystem", "/allowed/path"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Available Tools:**
|
||||
- `read_file`: Read file contents
|
||||
- `write_file`: Write to files
|
||||
- `create_directory`: Create directories
|
||||
- `list_directory`: List directory contents
|
||||
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="SQLite Database">
|
||||
Database operations:
|
||||
|
||||
```json
|
||||
{
|
||||
"sqlite": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "@modelcontextprotocol/server-sqlite", "/path/to/database.db"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Available Tools:**
|
||||
- `execute_query`: Execute SQL queries
|
||||
- `describe_tables`: Get table schemas
|
||||
- `create_table`: Create new tables
|
||||
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Slack Integration">
|
||||
Slack workspace integration:
|
||||
|
||||
```json
|
||||
{
|
||||
"slack": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "@modelcontextprotocol/server-slack"],
|
||||
"env": {
|
||||
"SLACK_BOT_TOKEN": "xoxb-your-bot-token",
|
||||
"SLACK_TEAM_ID": "T1234567890"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Available Tools:**
|
||||
- `send_slack_message`: Send messages to channels
|
||||
- `list_slack_channels`: List available channels
|
||||
- `get_slack_thread`: Get thread messages
|
||||
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="GitHub Integration">
|
||||
GitHub repository operations:
|
||||
|
||||
```json
|
||||
{
|
||||
"github": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "@modelcontextprotocol/server-github"],
|
||||
"env": {
|
||||
"GITHUB_PERSONAL_ACCESS_TOKEN": "ghp_your_token"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Available Tools:**
|
||||
- `create_or_update_file`: Create/update repository files
|
||||
- `search_repositories`: Search GitHub repositories
|
||||
- `create_issue`: Create issues
|
||||
- `create_pull_request`: Create pull requests
|
||||
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Google Drive">
|
||||
Google Drive file operations:
|
||||
|
||||
```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"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Available Tools:**
|
||||
- `gdrive_search`: Search files and folders
|
||||
- `gdrive_read`: Read file contents
|
||||
- `gdrive_create`: Create new files
|
||||
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Amap Maps (China)">
|
||||
Chinese mapping and location services:
|
||||
|
||||
```json
|
||||
{
|
||||
"amap": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "@amap/amap-maps-mcp-server"],
|
||||
"env": {
|
||||
"AMAP_MAPS_API_KEY": "your-api-key"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Available Tools:**
|
||||
- `search_location`: Search for locations
|
||||
- `get_directions`: Get route directions
|
||||
- `reverse_geocode`: Convert coordinates to addresses
|
||||
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
|
||||
## Server Lifecycle Management
|
||||
|
||||
### Starting Servers
|
||||
|
||||
Servers are automatically started when:
|
||||
|
||||
- MCPHub boots up
|
||||
- A server is added via the dashboard or API
|
||||
- A server configuration is updated
|
||||
- A stopped server is manually restarted
|
||||
|
||||
### Stopping Servers
|
||||
|
||||
You can stop servers:
|
||||
|
||||
- **Via Dashboard**: Toggle the server status switch
|
||||
- **Via API**: Send a POST request to `/api/servers/{name}/toggle`
|
||||
- **Automatically**: Servers stop if they crash or encounter errors
|
||||
|
||||
### Restarting Servers
|
||||
|
||||
Servers are automatically restarted:
|
||||
|
||||
- When configuration changes are made
|
||||
- After environment variable updates
|
||||
- When manually triggered via dashboard or API
|
||||
|
||||
## Server Status Monitoring
|
||||
|
||||
### Status Indicators
|
||||
|
||||
Each server displays a status indicator:
|
||||
|
||||
- 🟢 **Running**: Server is active and responding
|
||||
- 🟡 **Starting**: Server is initializing
|
||||
- 🔴 **Stopped**: Server is not running
|
||||
- ⚠️ **Error**: Server encountered an error
|
||||
|
||||
### Real-time Logs
|
||||
|
||||
View server logs in real-time:
|
||||
|
||||
1. **Dashboard Logs**: Click on a server to view its logs
|
||||
2. **API Logs**: Access logs via `/api/logs` endpoint
|
||||
3. **Streaming Logs**: Subscribe to log streams via WebSocket
|
||||
|
||||
### Health Checks
|
||||
|
||||
MCPHub automatically performs health checks:
|
||||
|
||||
- **Initialization Check**: Verifies server starts successfully
|
||||
- **Tool Discovery**: Confirms available tools are detected
|
||||
- **Response Check**: Tests server responsiveness
|
||||
- **Resource Monitoring**: Tracks CPU and memory usage
|
||||
|
||||
## Configuration Management
|
||||
|
||||
### Environment Variables
|
||||
|
||||
Servers can use environment variables for configuration:
|
||||
|
||||
```json
|
||||
{
|
||||
"server-name": {
|
||||
"command": "python",
|
||||
"args": ["server.py"],
|
||||
"env": {
|
||||
"API_KEY": "${YOUR_API_KEY}",
|
||||
"DEBUG": "true",
|
||||
"MAX_CONNECTIONS": "10"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Environment Variable Expansion:**
|
||||
|
||||
- `${VAR_NAME}`: Expands to environment variable value
|
||||
- `${VAR_NAME:-default}`: Uses default if variable not set
|
||||
- `${VAR_NAME:+value}`: Uses value if variable is set
|
||||
|
||||
### Working Directory
|
||||
|
||||
Set the working directory for server execution:
|
||||
|
||||
```json
|
||||
{
|
||||
"server-name": {
|
||||
"command": "./local-script.sh",
|
||||
"args": [],
|
||||
"cwd": "/path/to/server/directory"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Command Variations
|
||||
|
||||
Different ways to specify server commands:
|
||||
|
||||
<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>
|
||||
|
||||
<Tab title="Direct Python">
|
||||
```json
|
||||
{
|
||||
"direct-python": {
|
||||
"command": "python",
|
||||
"args": ["-m", "module_name", "--arg", "value"]
|
||||
}
|
||||
}
|
||||
```
|
||||
</Tab>
|
||||
|
||||
<Tab title="Local Script">
|
||||
```json
|
||||
{
|
||||
"local-script": {
|
||||
"command": "./server.sh",
|
||||
"args": ["--port", "8080"],
|
||||
"cwd": "/path/to/script"
|
||||
}
|
||||
}
|
||||
```
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
## Advanced Features
|
||||
|
||||
### Hot Reloading
|
||||
|
||||
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
|
||||
|
||||
### Resource Limits
|
||||
|
||||
Control server resource usage:
|
||||
|
||||
```json
|
||||
{
|
||||
"resource-limited-server": {
|
||||
"command": "memory-intensive-server",
|
||||
"args": [],
|
||||
"limits": {
|
||||
"memory": "512MB",
|
||||
"cpu": "50%",
|
||||
"timeout": "30s"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Dependency Management
|
||||
|
||||
Handle server dependencies:
|
||||
|
||||
<AccordionGroup>
|
||||
<Accordion title="Auto-installation">
|
||||
MCPHub can automatically install missing packages:
|
||||
|
||||
```json
|
||||
{
|
||||
"auto-install-server": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "package-that-might-not-exist"],
|
||||
"autoInstall": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Pre-installation Scripts">
|
||||
Run setup scripts before starting servers:
|
||||
|
||||
```json
|
||||
{
|
||||
"setup-server": {
|
||||
"preStart": ["npm install", "pip install -r requirements.txt"],
|
||||
"command": "python",
|
||||
"args": ["server.py"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
<AccordionGroup>
|
||||
<Accordion title="Server Won't Start">
|
||||
**Check the following:**
|
||||
- Command is available in PATH
|
||||
- All required environment variables are set
|
||||
- Working directory exists and is accessible
|
||||
- Network ports are not blocked
|
||||
- Dependencies are installed
|
||||
|
||||
**Debug steps:**
|
||||
1. Check server logs in the dashboard
|
||||
2. Test command manually in terminal
|
||||
3. Verify environment variable expansion
|
||||
4. Check file permissions
|
||||
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Server Keeps Crashing">
|
||||
**Common causes:**
|
||||
- Invalid configuration parameters
|
||||
- Missing API keys or credentials
|
||||
- Resource limits exceeded
|
||||
- Dependency conflicts
|
||||
|
||||
**Solutions:**
|
||||
1. Review server logs for error messages
|
||||
2. Test with minimal configuration
|
||||
3. Verify all credentials and API keys
|
||||
4. Check system resource availability
|
||||
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Tools Not Appearing">
|
||||
**Possible issues:**
|
||||
- Server not fully initialized
|
||||
- Tool discovery timeout
|
||||
- Communication protocol mismatch
|
||||
- Server reporting errors
|
||||
|
||||
**Debug steps:**
|
||||
1. Wait for server initialization to complete
|
||||
2. Check server logs for tool registration messages
|
||||
3. Test direct communication with server
|
||||
4. Verify MCP protocol compatibility
|
||||
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
|
||||
## Next Steps
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="Group Management" icon="users" href="/features/group-management">
|
||||
Organize servers into logical groups
|
||||
</Card>
|
||||
<Card title="Smart Routing" icon="route" href="/features/smart-routing">
|
||||
Set up AI-powered tool discovery
|
||||
</Card>
|
||||
<Card title="API Reference" icon="code" href="/api-reference/servers">
|
||||
Server management API documentation
|
||||
</Card>
|
||||
<Card title="Configuration Guide" icon="cog" href="/configuration/mcp-settings">
|
||||
Detailed configuration options
|
||||
</Card>
|
||||
</CardGroup>
|
||||
720
docs/features/smart-routing.mdx
Normal file
@@ -0,0 +1,720 @@
|
||||
---
|
||||
title: 'Smart Routing'
|
||||
description: 'AI-powered tool discovery using vector semantic search'
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
Smart Routing is MCPHub's intelligent tool discovery system that uses vector semantic search to automatically find the most relevant tools for any given task. Instead of manually specifying which tools to use, AI clients can describe what they want to accomplish, and Smart Routing will identify and provide access to the most appropriate tools.
|
||||
|
||||
## How Smart Routing Works
|
||||
|
||||
### 1. Tool Indexing
|
||||
|
||||
When servers start up, Smart Routing automatically:
|
||||
|
||||
- Discovers all available tools from MCP servers
|
||||
- Extracts tool metadata (names, descriptions, parameters)
|
||||
- Converts tool information to vector embeddings
|
||||
- Stores embeddings in PostgreSQL with pgvector
|
||||
|
||||
### 2. Semantic Search
|
||||
|
||||
When a query is made:
|
||||
|
||||
- User queries are converted to vector embeddings
|
||||
- Similarity search finds matching tools using cosine similarity
|
||||
- Dynamic thresholds filter out irrelevant results
|
||||
- Results are ranked by relevance score
|
||||
|
||||
### 3. Intelligent Filtering
|
||||
|
||||
Smart Routing applies several filters:
|
||||
|
||||
- **Relevance Threshold**: Only returns tools above similarity threshold
|
||||
- **Context Awareness**: Considers conversation context
|
||||
- **Tool Availability**: Ensures tools are currently accessible
|
||||
- **Permission Filtering**: Respects user access permissions
|
||||
|
||||
### 4. Tool Execution
|
||||
|
||||
Found tools can be directly executed:
|
||||
|
||||
- Parameter validation ensures correct tool usage
|
||||
- Error handling provides helpful feedback
|
||||
- Response formatting maintains consistency
|
||||
- Logging tracks tool usage for analytics
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Smart Routing requires additional setup compared to basic MCPHub usage:
|
||||
|
||||
### Required Components
|
||||
|
||||
1. **PostgreSQL with pgvector**: Vector database for embeddings storage
|
||||
2. **Embedding Service**: OpenAI API or compatible service
|
||||
3. **Environment Configuration**: Proper configuration variables
|
||||
|
||||
### Quick Setup
|
||||
|
||||
<Tabs>
|
||||
<Tab title="Docker Compose">
|
||||
Use this `docker-compose.yml` for complete setup:
|
||||
|
||||
```yaml
|
||||
version: '3.8'
|
||||
services:
|
||||
mcphub:
|
||||
image: samanhappy/mcphub:latest
|
||||
ports:
|
||||
- "3000:3000"
|
||||
environment:
|
||||
- DATABASE_URL=postgresql://mcphub:password@postgres:5432/mcphub
|
||||
- OPENAI_API_KEY=your_openai_api_key
|
||||
- ENABLE_SMART_ROUTING=true
|
||||
depends_on:
|
||||
- postgres
|
||||
volumes:
|
||||
- ./mcp_settings.json:/app/mcp_settings.json
|
||||
|
||||
postgres:
|
||||
image: pgvector/pgvector:pg16
|
||||
environment:
|
||||
- POSTGRES_DB=mcphub
|
||||
- POSTGRES_USER=mcphub
|
||||
- POSTGRES_PASSWORD=password
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
ports:
|
||||
- "5432:5432"
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
```
|
||||
|
||||
Start with:
|
||||
```bash
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
</Tab>
|
||||
|
||||
<Tab title="Manual Setup">
|
||||
1. **Install PostgreSQL with pgvector**:
|
||||
```bash
|
||||
# Using Docker
|
||||
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
|
||||
```
|
||||
|
||||
2. **Set Environment Variables**:
|
||||
```bash
|
||||
export DATABASE_URL="postgresql://mcphub:your_password@localhost:5432/mcphub"
|
||||
export OPENAI_API_KEY="your_openai_api_key"
|
||||
export ENABLE_SMART_ROUTING="true"
|
||||
```
|
||||
|
||||
3. **Start MCPHub**:
|
||||
```bash
|
||||
mcphub
|
||||
```
|
||||
|
||||
</Tab>
|
||||
|
||||
<Tab title="Kubernetes">
|
||||
Deploy with these Kubernetes manifests:
|
||||
|
||||
```yaml
|
||||
# postgres-deployment.yaml
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: postgres
|
||||
spec:
|
||||
selector:
|
||||
matchLabels:
|
||||
app: postgres
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: postgres
|
||||
spec:
|
||||
containers:
|
||||
- name: postgres
|
||||
image: pgvector/pgvector:pg16
|
||||
env:
|
||||
- name: POSTGRES_DB
|
||||
value: mcphub
|
||||
- name: POSTGRES_USER
|
||||
value: mcphub
|
||||
- name: POSTGRES_PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: postgres-secret
|
||||
key: password
|
||||
ports:
|
||||
- containerPort: 5432
|
||||
---
|
||||
# mcphub-deployment.yaml
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: mcphub
|
||||
spec:
|
||||
selector:
|
||||
matchLabels:
|
||||
app: mcphub
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: mcphub
|
||||
spec:
|
||||
containers:
|
||||
- name: mcphub
|
||||
image: samanhappy/mcphub:latest
|
||||
env:
|
||||
- name: DATABASE_URL
|
||||
value: "postgresql://mcphub:password@postgres:5432/mcphub"
|
||||
- name: OPENAI_API_KEY
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: openai-secret
|
||||
key: api-key
|
||||
- name: ENABLE_SMART_ROUTING
|
||||
value: "true"
|
||||
ports:
|
||||
- containerPort: 3000
|
||||
```
|
||||
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
## Configuration
|
||||
|
||||
### Environment Variables
|
||||
|
||||
Configure Smart Routing with these environment variables:
|
||||
|
||||
```bash
|
||||
# Required
|
||||
DATABASE_URL=postgresql://user:password@host:5432/database
|
||||
OPENAI_API_KEY=your_openai_api_key
|
||||
|
||||
# Optional
|
||||
ENABLE_SMART_ROUTING=true
|
||||
EMBEDDING_MODEL=text-embedding-3-small
|
||||
SIMILARITY_THRESHOLD=0.7
|
||||
MAX_TOOLS_RETURNED=10
|
||||
EMBEDDING_BATCH_SIZE=100
|
||||
```
|
||||
|
||||
### Configuration Options
|
||||
|
||||
<AccordionGroup>
|
||||
<Accordion title="Database Configuration">
|
||||
```bash
|
||||
# Full PostgreSQL connection string
|
||||
DATABASE_URL=postgresql://username:password@host:port/database?schema=public
|
||||
|
||||
# SSL configuration for cloud databases
|
||||
DATABASE_URL=postgresql://user:pass@host:5432/db?sslmode=require
|
||||
|
||||
# Connection pool settings
|
||||
DATABASE_POOL_SIZE=20
|
||||
DATABASE_TIMEOUT=30000
|
||||
```
|
||||
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Embedding Service">
|
||||
```bash
|
||||
# OpenAI (default)
|
||||
OPENAI_API_KEY=sk-your-api-key
|
||||
EMBEDDING_MODEL=text-embedding-3-small
|
||||
|
||||
# Azure OpenAI
|
||||
AZURE_OPENAI_ENDPOINT=https://your-resource.openai.azure.com
|
||||
AZURE_OPENAI_API_KEY=your-api-key
|
||||
AZURE_OPENAI_DEPLOYMENT=your-embedding-deployment
|
||||
|
||||
# Custom embedding service
|
||||
EMBEDDING_SERVICE_URL=https://your-embedding-service.com
|
||||
EMBEDDING_SERVICE_API_KEY=your-api-key
|
||||
```
|
||||
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Search Parameters">
|
||||
```bash
|
||||
# Similarity threshold (0.0 to 1.0)
|
||||
SIMILARITY_THRESHOLD=0.7
|
||||
|
||||
# Maximum tools to return
|
||||
MAX_TOOLS_RETURNED=10
|
||||
|
||||
# Minimum query length for smart routing
|
||||
MIN_QUERY_LENGTH=5
|
||||
|
||||
# Cache TTL for embeddings (seconds)
|
||||
EMBEDDING_CACHE_TTL=3600
|
||||
```
|
||||
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
|
||||
## Using Smart Routing
|
||||
|
||||
### Smart Routing Endpoint
|
||||
|
||||
Access Smart Routing through the special `$smart` endpoint:
|
||||
|
||||
<Tabs>
|
||||
<Tab title="HTTP MCP">
|
||||
```
|
||||
http://localhost:3000/mcp/$smart
|
||||
```
|
||||
</Tab>
|
||||
|
||||
<Tab title="SSE (Legacy)">
|
||||
```
|
||||
http://localhost:3000/sse/$smart
|
||||
```
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
### Basic Usage
|
||||
|
||||
Connect your AI client to the Smart Routing endpoint and make natural language requests:
|
||||
|
||||
```bash
|
||||
# Example: Find tools for web scraping
|
||||
curl -X POST http://localhost:3000/mcp/$smart \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"jsonrpc": "2.0",
|
||||
"id": 1,
|
||||
"method": "tools/search",
|
||||
"params": {
|
||||
"query": "scrape website content and extract text"
|
||||
}
|
||||
}'
|
||||
```
|
||||
|
||||
Response:
|
||||
|
||||
```json
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"id": 1,
|
||||
"result": {
|
||||
"tools": [
|
||||
{
|
||||
"name": "fetch_html",
|
||||
"server": "fetch",
|
||||
"description": "Fetch and parse HTML content from a URL",
|
||||
"relevanceScore": 0.92,
|
||||
"parameters": { ... }
|
||||
},
|
||||
{
|
||||
"name": "playwright_navigate",
|
||||
"server": "playwright",
|
||||
"description": "Navigate to a web page and extract content",
|
||||
"relevanceScore": 0.87,
|
||||
"parameters": { ... }
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Advanced Queries
|
||||
|
||||
Smart Routing supports various query types:
|
||||
|
||||
<AccordionGroup>
|
||||
<Accordion title="Task-Based Queries">
|
||||
```bash
|
||||
# What you want to accomplish
|
||||
curl -X POST http://localhost:3000/mcp/$smart \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"jsonrpc": "2.0",
|
||||
"id": 1,
|
||||
"method": "tools/search",
|
||||
"params": {
|
||||
"query": "send a message to a slack channel"
|
||||
}
|
||||
}'
|
||||
```
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Domain-Specific Queries">
|
||||
```bash
|
||||
# Specific domain or technology
|
||||
curl -X POST http://localhost:3000/mcp/$smart \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"jsonrpc": "2.0",
|
||||
"id": 1,
|
||||
"method": "tools/search",
|
||||
"params": {
|
||||
"query": "database operations SQL queries"
|
||||
}
|
||||
}'
|
||||
```
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Action-Oriented Queries">
|
||||
```bash
|
||||
# Specific actions
|
||||
curl -X POST http://localhost:3000/mcp/$smart \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"jsonrpc": "2.0",
|
||||
"id": 1,
|
||||
"method": "tools/search",
|
||||
"params": {
|
||||
"query": "create file upload to github repository"
|
||||
}
|
||||
}'
|
||||
```
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Context-Aware Queries">
|
||||
```bash
|
||||
# Include context for better results
|
||||
curl -X POST http://localhost:3000/mcp/$smart \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"jsonrpc": "2.0",
|
||||
"id": 1,
|
||||
"method": "tools/search",
|
||||
"params": {
|
||||
"query": "automated testing web application",
|
||||
"context": {
|
||||
"project": "e-commerce website",
|
||||
"technologies": ["React", "Node.js"],
|
||||
"environment": "staging"
|
||||
}
|
||||
}
|
||||
}'
|
||||
```
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
|
||||
### Tool Execution
|
||||
|
||||
Once Smart Routing finds relevant tools, you can execute them directly:
|
||||
|
||||
```bash
|
||||
# Execute a found tool
|
||||
curl -X POST http://localhost:3000/mcp/$smart \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"jsonrpc": "2.0",
|
||||
"id": 2,
|
||||
"method": "tools/call",
|
||||
"params": {
|
||||
"name": "fetch_html",
|
||||
"arguments": {
|
||||
"url": "https://example.com"
|
||||
}
|
||||
}
|
||||
}'
|
||||
```
|
||||
|
||||
## Performance Optimization
|
||||
|
||||
### Embedding Cache
|
||||
|
||||
Smart Routing caches embeddings to improve performance:
|
||||
|
||||
```bash
|
||||
# Configure cache settings
|
||||
EMBEDDING_CACHE_TTL=3600 # Cache for 1 hour
|
||||
EMBEDDING_CACHE_SIZE=10000 # Cache up to 10k embeddings
|
||||
EMBEDDING_CACHE_CLEANUP=300 # Cleanup every 5 minutes
|
||||
```
|
||||
|
||||
### Batch Processing
|
||||
|
||||
Tools are indexed in batches for efficiency:
|
||||
|
||||
```bash
|
||||
# Batch size for embedding generation
|
||||
EMBEDDING_BATCH_SIZE=100
|
||||
|
||||
# Concurrent embedding requests
|
||||
EMBEDDING_CONCURRENCY=5
|
||||
|
||||
# Index update frequency
|
||||
INDEX_UPDATE_INTERVAL=3600 # Re-index every hour
|
||||
```
|
||||
|
||||
### Database Optimization
|
||||
|
||||
Optimize PostgreSQL for vector operations:
|
||||
|
||||
```sql
|
||||
-- Create indexes for better performance
|
||||
CREATE INDEX ON tool_embeddings USING hnsw (embedding vector_cosine_ops);
|
||||
|
||||
-- Adjust PostgreSQL settings
|
||||
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';
|
||||
```
|
||||
|
||||
## Monitoring and Analytics
|
||||
|
||||
### Smart Routing Metrics
|
||||
|
||||
Monitor Smart Routing performance:
|
||||
|
||||
```bash
|
||||
# Get Smart Routing statistics
|
||||
curl http://localhost:3000/api/smart-routing/stats \
|
||||
-H "Authorization: Bearer YOUR_JWT_TOKEN"
|
||||
```
|
||||
|
||||
Response includes:
|
||||
|
||||
- Query count and frequency
|
||||
- Average response time
|
||||
- Embedding cache hit rate
|
||||
- Most popular tools
|
||||
- Query patterns
|
||||
|
||||
### Tool Usage Analytics
|
||||
|
||||
Track which tools are found and used:
|
||||
|
||||
```bash
|
||||
# Get tool usage analytics
|
||||
curl http://localhost:3000/api/smart-routing/analytics \
|
||||
-H "Authorization: Bearer YOUR_JWT_TOKEN"
|
||||
```
|
||||
|
||||
Metrics include:
|
||||
|
||||
- Tool discovery rates
|
||||
- Execution success rates
|
||||
- User satisfaction scores
|
||||
- Query-to-execution conversion
|
||||
|
||||
### Performance Monitoring
|
||||
|
||||
Monitor system performance:
|
||||
|
||||
```bash
|
||||
# Database performance
|
||||
curl http://localhost:3000/api/smart-routing/db-stats \
|
||||
-H "Authorization: Bearer YOUR_JWT_TOKEN"
|
||||
|
||||
# Embedding service status
|
||||
curl http://localhost:3000/api/smart-routing/embedding-stats \
|
||||
-H "Authorization: Bearer YOUR_JWT_TOKEN"
|
||||
```
|
||||
|
||||
## Advanced Features
|
||||
|
||||
### Custom Embeddings
|
||||
|
||||
Use custom embedding models:
|
||||
|
||||
```bash
|
||||
# Hugging Face models
|
||||
EMBEDDING_SERVICE=huggingface
|
||||
HUGGINGFACE_MODEL=sentence-transformers/all-MiniLM-L6-v2
|
||||
HUGGINGFACE_API_KEY=your_api_key
|
||||
|
||||
# Local embedding service
|
||||
EMBEDDING_SERVICE=local
|
||||
EMBEDDING_SERVICE_URL=http://localhost:8080/embeddings
|
||||
```
|
||||
|
||||
### Query Enhancement
|
||||
|
||||
Enhance queries for better results:
|
||||
|
||||
```json
|
||||
{
|
||||
"queryEnhancement": {
|
||||
"enabled": true,
|
||||
"expandAcronyms": true,
|
||||
"addSynonyms": true,
|
||||
"contextualExpansion": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Result Filtering
|
||||
|
||||
Filter results based on criteria:
|
||||
|
||||
```json
|
||||
{
|
||||
"resultFiltering": {
|
||||
"minRelevanceScore": 0.7,
|
||||
"maxResults": 10,
|
||||
"preferredServers": ["fetch", "playwright"],
|
||||
"excludeServers": ["deprecated-server"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Feedback Learning
|
||||
|
||||
Improve results based on user feedback:
|
||||
|
||||
```bash
|
||||
# Provide feedback on search results
|
||||
curl -X POST http://localhost:3000/api/smart-routing/feedback \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer YOUR_JWT_TOKEN" \
|
||||
-d '{
|
||||
"queryId": "search-123",
|
||||
"toolName": "fetch_html",
|
||||
"rating": 5,
|
||||
"successful": true,
|
||||
"comments": "Perfect tool for the task"
|
||||
}'
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
<AccordionGroup>
|
||||
<Accordion title="Database Connection Issues">
|
||||
**Symptoms:**
|
||||
- Smart Routing not available
|
||||
- Database connection errors
|
||||
- Embedding storage failures
|
||||
|
||||
**Solutions:**
|
||||
1. Verify PostgreSQL is running
|
||||
2. Check DATABASE_URL format
|
||||
3. Ensure pgvector extension is installed
|
||||
4. Test connection manually:
|
||||
```bash
|
||||
psql $DATABASE_URL -c "SELECT 1;"
|
||||
```
|
||||
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Embedding Service Problems">
|
||||
**Symptoms:**
|
||||
- Tool indexing failures
|
||||
- Query processing errors
|
||||
- API rate limit errors
|
||||
|
||||
**Solutions:**
|
||||
1. Verify API key validity
|
||||
2. Check network connectivity
|
||||
3. Monitor rate limits
|
||||
4. Test embedding service:
|
||||
```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"}'
|
||||
```
|
||||
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Poor Search Results">
|
||||
**Symptoms:**
|
||||
- Irrelevant tools returned
|
||||
- Low relevance scores
|
||||
- Missing expected tools
|
||||
|
||||
**Solutions:**
|
||||
1. Adjust similarity threshold
|
||||
2. Re-index tools with better descriptions
|
||||
3. Use more specific queries
|
||||
4. Check tool metadata quality
|
||||
```bash
|
||||
# Re-index all tools
|
||||
curl -X POST http://localhost:3000/api/smart-routing/reindex \
|
||||
-H "Authorization: Bearer YOUR_JWT_TOKEN"
|
||||
```
|
||||
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Performance Issues">
|
||||
**Symptoms:**
|
||||
- Slow query responses
|
||||
- High database load
|
||||
- Memory usage spikes
|
||||
|
||||
**Solutions:**
|
||||
1. Optimize database configuration
|
||||
2. Increase cache sizes
|
||||
3. Reduce batch sizes
|
||||
4. Monitor system resources
|
||||
```bash
|
||||
# Check system performance
|
||||
curl http://localhost:3000/api/smart-routing/performance \
|
||||
-H "Authorization: Bearer YOUR_JWT_TOKEN"
|
||||
```
|
||||
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
|
||||
## Best Practices
|
||||
|
||||
### Query Writing
|
||||
|
||||
<Tip>
|
||||
**Be Descriptive**: Use specific, descriptive language in queries for better tool matching.
|
||||
</Tip>
|
||||
|
||||
<Tip>
|
||||
**Include Context**: Provide relevant context about your task or domain for more accurate results.
|
||||
</Tip>
|
||||
|
||||
<Tip>**Use Natural Language**: Write queries as you would describe the task to a human.</Tip>
|
||||
|
||||
### Tool Descriptions
|
||||
|
||||
<Warning>
|
||||
**Quality Metadata**: Ensure MCP servers provide high-quality tool descriptions and metadata.
|
||||
</Warning>
|
||||
|
||||
<Warning>**Regular Updates**: Keep tool descriptions current as functionality evolves.</Warning>
|
||||
|
||||
<Warning>
|
||||
**Consistent Naming**: Use consistent naming conventions across tools and servers.
|
||||
</Warning>
|
||||
|
||||
### System Maintenance
|
||||
|
||||
<Info>**Regular Re-indexing**: Periodically re-index tools to ensure embedding quality.</Info>
|
||||
|
||||
<Info>**Monitor Performance**: Track query patterns and optimize based on usage.</Info>
|
||||
|
||||
<Info>
|
||||
**Update Models**: Consider updating to newer embedding models as they become available.
|
||||
</Info>
|
||||
|
||||
## Next Steps
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="Authentication" icon="shield" href="/features/authentication">
|
||||
User management and access control
|
||||
</Card>
|
||||
<Card title="Monitoring" icon="chart-line" href="/features/monitoring">
|
||||
System monitoring and analytics
|
||||
</Card>
|
||||
<Card title="API Reference" icon="code" href="/api-reference/smart-routing">
|
||||
Complete Smart Routing API documentation
|
||||
</Card>
|
||||
<Card title="Configuration" icon="cog" href="/configuration/environment-variables">
|
||||
Advanced configuration options
|
||||
</Card>
|
||||
</CardGroup>
|
||||
128
docs/index.mdx
@@ -1,71 +1,95 @@
|
||||
---
|
||||
title: Introduction
|
||||
description: "Welcome to the home of your new documentation"
|
||||
title: MCPHub Documentation
|
||||
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" />
|
||||
|
||||
## Setting up
|
||||
# Welcome to MCPHub
|
||||
|
||||
The first step to world-class documentation is setting up your editing environments.
|
||||
MCPHub makes it easy to manage and scale multiple MCP (Model Context Protocol) servers by organizing them into flexible Streamable HTTP (SSE) endpoints—supporting access to all servers, individual servers, or logical server groups.
|
||||
|
||||
## Key Features
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card
|
||||
title="Edit Your Docs"
|
||||
icon="pen-to-square"
|
||||
href="https://mintlify.com/docs/quickstart"
|
||||
>
|
||||
Get your docs set up locally for easy development
|
||||
<Card title="Unified Management" icon="server" href="/features/server-management">
|
||||
Centrally manage multiple MCP servers with hot-swappable configuration
|
||||
</Card>
|
||||
<Card
|
||||
title="Preview Changes"
|
||||
icon="image"
|
||||
href="https://mintlify.com/docs/development"
|
||||
>
|
||||
Preview your changes before you push to make sure they're perfect
|
||||
<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="Real-time Monitoring" icon="chart-line" href="/features/monitoring">
|
||||
Monitor server status and performance from a unified dashboard
|
||||
</Card>
|
||||
</CardGroup>
|
||||
|
||||
## Make it yours
|
||||
## Quick Start
|
||||
|
||||
Update your docs to your brand and add valuable content for the best user conversion.
|
||||
Get MCPHub running in minutes with Docker:
|
||||
|
||||
```bash
|
||||
docker run -p 3000:3000 samanhappy/mcphub
|
||||
```
|
||||
|
||||
Or with custom configuration:
|
||||
|
||||
```bash
|
||||
docker run -p 3000:3000 -v $(pwd)/mcp_settings.json:/app/mcp_settings.json samanhappy/mcphub
|
||||
```
|
||||
|
||||
Access the dashboard at `http://localhost:3000` with default credentials:
|
||||
|
||||
- Username: `admin`
|
||||
- Password: `admin123`
|
||||
|
||||
## Core Concepts
|
||||
|
||||
### MCP Endpoints
|
||||
|
||||
MCPHub provides multiple ways to access your MCP servers:
|
||||
|
||||
- **Unified Access**: `http://localhost:3000/mcp` - Access all servers
|
||||
- **Group Access**: `http://localhost:3000/mcp/{group}` - Access specific groups
|
||||
- **Server Access**: `http://localhost:3000/mcp/{server}` - Access individual servers
|
||||
- **Smart Routing**: `http://localhost:3000/mcp/$smart` - AI-powered tool discovery
|
||||
|
||||
### Protocol Support
|
||||
|
||||
- **HTTP MCP**: Modern streamable HTTP interface (recommended)
|
||||
- **SSE**: Server-Sent Events for legacy compatibility
|
||||
- **stdio**: Native MCP protocol for server communication
|
||||
|
||||
## Getting Started
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card
|
||||
title="Customize Style"
|
||||
icon="palette"
|
||||
href="https://mintlify.com/docs/settings/global"
|
||||
>
|
||||
Customize your docs to your company's colors and brands
|
||||
<Card title="Quick Start Guide" icon="rocket" href="/quickstart">
|
||||
Get MCPHub running in 5 minutes
|
||||
</Card>
|
||||
<Card
|
||||
title="Reference APIs"
|
||||
icon="code"
|
||||
href="https://mintlify.com/docs/api-playground/openapi"
|
||||
>
|
||||
Automatically generate endpoints from an OpenAPI spec
|
||||
<Card title="Installation Guide" icon="download" href="/installation">
|
||||
Detailed installation instructions for all platforms
|
||||
</Card>
|
||||
<Card
|
||||
title="Add Components"
|
||||
icon="screwdriver-wrench"
|
||||
href="https://mintlify.com/docs/content/components/accordions"
|
||||
>
|
||||
Build interactive features and designs to guide your users
|
||||
<Card title="Configuration" icon="cog" href="/configuration/mcp-settings">
|
||||
Learn how to configure your MCP servers
|
||||
</Card>
|
||||
<Card
|
||||
title="Get Inspiration"
|
||||
icon="stars"
|
||||
href="https://mintlify.com/customers"
|
||||
>
|
||||
Check out our showcase of our favorite documentation
|
||||
<Card title="API Reference" icon="code" href="/api-reference/introduction">
|
||||
Complete API documentation
|
||||
</Card>
|
||||
</CardGroup>
|
||||
|
||||
## Community & Support
|
||||
|
||||
<CardGroup cols={3}>
|
||||
<Card title="GitHub" icon="github" href="https://github.com/samanhappy/mcphub">
|
||||
Source code and issue tracking
|
||||
</Card>
|
||||
<Card title="Discord" icon="discord" href="https://discord.gg/qMKNsn5Q">
|
||||
Join our community discussions
|
||||
</Card>
|
||||
<Card title="Sponsor" icon="heart" href="https://ko-fi.com/samanhappy">
|
||||
Support the project development
|
||||
</Card>
|
||||
</CardGroup>
|
||||
|
||||
575
docs/installation.mdx
Normal file
@@ -0,0 +1,575 @@
|
||||
---
|
||||
title: 'Installation Guide'
|
||||
description: 'Detailed installation instructions for all platforms'
|
||||
---
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Before installing MCPHub, ensure you have the following prerequisites:
|
||||
|
||||
- **Node.js** 18+ (for local development)
|
||||
- **Docker** (recommended for production)
|
||||
- **pnpm** (for local development)
|
||||
|
||||
Optional for Smart Routing:
|
||||
|
||||
- **PostgreSQL** with pgvector extension
|
||||
- **OpenAI API Key** or compatible embedding service
|
||||
|
||||
## Installation Methods
|
||||
|
||||
<Tabs>
|
||||
<Tab title="Docker (Recommended)">
|
||||
### Docker Installation
|
||||
|
||||
Docker is the recommended way to deploy MCPHub in production.
|
||||
|
||||
#### 1. Basic Installation
|
||||
|
||||
```bash
|
||||
# Pull the latest image
|
||||
docker pull samanhappy/mcphub:latest
|
||||
|
||||
# Run with default settings
|
||||
docker run -d \
|
||||
--name mcphub \
|
||||
-p 3000:3000 \
|
||||
samanhappy/mcphub:latest
|
||||
```
|
||||
|
||||
#### 2. With Custom Configuration
|
||||
|
||||
```bash
|
||||
# Create your configuration file
|
||||
cat > mcp_settings.json << 'EOF'
|
||||
{
|
||||
"mcpServers": {
|
||||
"fetch": {
|
||||
"command": "uvx",
|
||||
"args": ["mcp-server-fetch"]
|
||||
},
|
||||
"playwright": {
|
||||
"command": "npx",
|
||||
"args": ["@playwright/mcp@latest", "--headless"]
|
||||
}
|
||||
}
|
||||
}
|
||||
EOF
|
||||
|
||||
# Run with mounted config
|
||||
docker run -d \
|
||||
--name mcphub \
|
||||
-p 3000:3000 \
|
||||
-v $(pwd)/mcp_settings.json:/app/mcp_settings.json \
|
||||
samanhappy/mcphub:latest
|
||||
```
|
||||
|
||||
#### 3. With Environment Variables
|
||||
|
||||
```bash
|
||||
docker run -d \
|
||||
--name mcphub \
|
||||
-p 3000:3000 \
|
||||
-e PORT=3000 \
|
||||
-e BASE_PATH="" \
|
||||
-e REQUEST_TIMEOUT=60000 \
|
||||
samanhappy/mcphub:latest
|
||||
```
|
||||
|
||||
#### 4. Docker Compose
|
||||
|
||||
Create a `docker-compose.yml` file:
|
||||
|
||||
```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
|
||||
|
||||
# Optional: PostgreSQL for Smart Routing
|
||||
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:
|
||||
```
|
||||
|
||||
Run with:
|
||||
```bash
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
</Tab>
|
||||
|
||||
<Tab title="npm Package">
|
||||
### npm Package Installation
|
||||
|
||||
Install MCPHub as a global npm package:
|
||||
|
||||
#### 1. Global Installation
|
||||
|
||||
```bash
|
||||
# Install globally
|
||||
npm install -g @samanhappy/mcphub
|
||||
|
||||
# Or with yarn
|
||||
yarn global add @samanhappy/mcphub
|
||||
|
||||
# Or with pnpm
|
||||
pnpm add -g @samanhappy/mcphub
|
||||
```
|
||||
|
||||
#### 2. Running MCPHub
|
||||
|
||||
```bash
|
||||
# Run with default settings
|
||||
mcphub
|
||||
|
||||
# Run with custom port
|
||||
PORT=8080 mcphub
|
||||
|
||||
# Run with custom config path
|
||||
MCP_SETTINGS_PATH=/path/to/mcp_settings.json mcphub
|
||||
```
|
||||
|
||||
#### 3. Local Installation
|
||||
|
||||
You can also install MCPHub locally in a project:
|
||||
|
||||
```bash
|
||||
# Create a new directory
|
||||
mkdir my-mcphub
|
||||
cd my-mcphub
|
||||
|
||||
# Initialize package.json
|
||||
npm init -y
|
||||
|
||||
# Install MCPHub locally
|
||||
npm install @samanhappy/mcphub
|
||||
|
||||
# Create a start script
|
||||
echo '#!/bin/bash\nnpx mcphub' > start.sh
|
||||
chmod +x start.sh
|
||||
|
||||
# Run MCPHub
|
||||
./start.sh
|
||||
```
|
||||
|
||||
</Tab>
|
||||
|
||||
<Tab title="Local Development">
|
||||
### Local Development Setup
|
||||
|
||||
For development, customization, or contribution:
|
||||
|
||||
#### 1. Clone Repository
|
||||
|
||||
```bash
|
||||
# Clone the repository
|
||||
git clone https://github.com/samanhappy/mcphub.git
|
||||
cd mcphub
|
||||
```
|
||||
|
||||
#### 2. Install Dependencies
|
||||
|
||||
```bash
|
||||
# Install dependencies with pnpm (recommended)
|
||||
pnpm install
|
||||
|
||||
# Or with npm
|
||||
npm install
|
||||
|
||||
# Or with yarn
|
||||
yarn install
|
||||
```
|
||||
|
||||
#### 3. Development Mode
|
||||
|
||||
```bash
|
||||
# Start both backend and frontend in development mode
|
||||
pnpm dev
|
||||
|
||||
# This will start:
|
||||
# - Backend on http://localhost:3001
|
||||
# - Frontend on http://localhost:5173
|
||||
# - Frontend proxies API calls to backend
|
||||
```
|
||||
|
||||
#### 4. Build for Production
|
||||
|
||||
```bash
|
||||
# Build both backend and frontend
|
||||
pnpm build
|
||||
|
||||
# Start production server
|
||||
pnpm start
|
||||
```
|
||||
|
||||
#### 5. Development Scripts
|
||||
|
||||
```bash
|
||||
# Backend only (for API development)
|
||||
pnpm backend:dev
|
||||
|
||||
# Frontend only (when backend is running separately)
|
||||
pnpm frontend:dev
|
||||
|
||||
# Run tests
|
||||
pnpm test
|
||||
|
||||
# Lint code
|
||||
pnpm lint
|
||||
|
||||
# Format code
|
||||
pnpm format
|
||||
```
|
||||
|
||||
<Note>
|
||||
On Windows, you may need to run backend and frontend separately:
|
||||
```bash
|
||||
# Terminal 1: Backend
|
||||
pnpm backend:dev
|
||||
|
||||
# Terminal 2: Frontend
|
||||
pnpm frontend:dev
|
||||
```
|
||||
</Note>
|
||||
|
||||
</Tab>
|
||||
|
||||
<Tab title="Kubernetes">
|
||||
### Kubernetes Deployment
|
||||
|
||||
Deploy MCPHub on Kubernetes with these manifests:
|
||||
|
||||
#### 1. ConfigMap for Settings
|
||||
|
||||
```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. Deployment
|
||||
|
||||
```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. Service
|
||||
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: mcphub-service
|
||||
spec:
|
||||
selector:
|
||||
app: mcphub
|
||||
ports:
|
||||
- port: 80
|
||||
targetPort: 3000
|
||||
type: ClusterIP
|
||||
```
|
||||
|
||||
#### 4. Ingress (Optional)
|
||||
|
||||
```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
|
||||
```
|
||||
|
||||
Deploy with:
|
||||
```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>
|
||||
|
||||
## Smart Routing Setup (Optional)
|
||||
|
||||
Smart Routing provides AI-powered tool discovery using vector semantic search.
|
||||
|
||||
### Prerequisites
|
||||
|
||||
1. **PostgreSQL with pgvector**
|
||||
2. **OpenAI API Key** (or compatible embedding service)
|
||||
|
||||
### Database Setup
|
||||
|
||||
<Tabs>
|
||||
<Tab title="Docker PostgreSQL">
|
||||
```bash
|
||||
# Run PostgreSQL with pgvector
|
||||
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="Existing PostgreSQL">
|
||||
If you have an existing PostgreSQL instance:
|
||||
|
||||
```sql
|
||||
-- Connect to your PostgreSQL instance
|
||||
-- Create database
|
||||
CREATE DATABASE mcphub;
|
||||
|
||||
-- Connect to the mcphub database
|
||||
\c mcphub;
|
||||
|
||||
-- Enable pgvector extension
|
||||
CREATE EXTENSION IF NOT EXISTS vector;
|
||||
```
|
||||
|
||||
</Tab>
|
||||
|
||||
<Tab title="Cloud PostgreSQL">
|
||||
For cloud providers (AWS RDS, Google Cloud SQL, etc.):
|
||||
|
||||
1. Enable the pgvector extension in your cloud provider's console
|
||||
2. Create a database named `mcphub`
|
||||
3. Note down the connection details
|
||||
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
### Environment Configuration
|
||||
|
||||
Set the following environment variables:
|
||||
|
||||
```bash
|
||||
# Database connection
|
||||
DATABASE_URL=postgresql://mcphub:your_password@localhost:5432/mcphub
|
||||
|
||||
# OpenAI API for embeddings
|
||||
OPENAI_API_KEY=your_openai_api_key
|
||||
|
||||
# Optional: Custom embedding model
|
||||
EMBEDDING_MODEL=text-embedding-3-small
|
||||
|
||||
# Optional: Enable smart routing
|
||||
ENABLE_SMART_ROUTING=true
|
||||
```
|
||||
|
||||
## Verification
|
||||
|
||||
After installation, verify MCPHub is working:
|
||||
|
||||
### 1. Health Check
|
||||
|
||||
```bash
|
||||
curl http://localhost:3000/api/health
|
||||
```
|
||||
|
||||
Expected response:
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "ok",
|
||||
"version": "x.x.x",
|
||||
"uptime": 123
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Dashboard Access
|
||||
|
||||
Open your browser and navigate to:
|
||||
|
||||
```
|
||||
http://localhost:3000
|
||||
```
|
||||
|
||||
### 3. API Test
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:3000/mcp \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"jsonrpc": "2.0",
|
||||
"id": 1,
|
||||
"method": "tools/list",
|
||||
"params": {}
|
||||
}'
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
<AccordionGroup>
|
||||
<Accordion title="Docker Issues">
|
||||
**Port already in use:**
|
||||
```bash
|
||||
# Check what's using port 3000
|
||||
lsof -i :3000
|
||||
|
||||
# Use a different port
|
||||
docker run -p 8080:3000 samanhappy/mcphub
|
||||
```
|
||||
|
||||
**Container won't start:**
|
||||
```bash
|
||||
# Check container logs
|
||||
docker logs mcphub
|
||||
|
||||
# Run interactively for debugging
|
||||
docker run -it --rm samanhappy/mcphub /bin/bash
|
||||
```
|
||||
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="npm Installation Issues">
|
||||
**Permission errors:**
|
||||
```bash
|
||||
# Use npx instead of global install
|
||||
npx @samanhappy/mcphub
|
||||
|
||||
# Or fix npm permissions
|
||||
npm config set prefix ~/.npm-global
|
||||
export PATH=~/.npm-global/bin:$PATH
|
||||
```
|
||||
|
||||
**Node version issues:**
|
||||
```bash
|
||||
# Check Node version
|
||||
node --version
|
||||
|
||||
# Install Node 18+ using nvm
|
||||
nvm install 18
|
||||
nvm use 18
|
||||
```
|
||||
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Network Issues">
|
||||
**Can't access dashboard:**
|
||||
- Check if MCPHub is running: `ps aux | grep mcphub`
|
||||
- Verify port binding: `netstat -tlnp | grep 3000`
|
||||
- Check firewall settings
|
||||
- Try accessing via `127.0.0.1:3000` instead of `localhost:3000`
|
||||
|
||||
**AI clients can't connect:**
|
||||
- Ensure the endpoint URL is correct
|
||||
- Check if MCPHub is behind a proxy
|
||||
- Verify network policies in Kubernetes/Docker environments
|
||||
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Smart Routing Issues">
|
||||
**Database connection failed:**
|
||||
```bash
|
||||
# Test database connection
|
||||
psql $DATABASE_URL -c "SELECT 1;"
|
||||
|
||||
# Check if pgvector is installed
|
||||
psql $DATABASE_URL -c "CREATE EXTENSION IF NOT EXISTS vector;"
|
||||
```
|
||||
|
||||
**Embedding service errors:**
|
||||
- Verify OpenAI API key is valid
|
||||
- Check internet connectivity
|
||||
- Monitor rate limits
|
||||
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
|
||||
## Next Steps
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="Configuration" icon="cog" href="/configuration/mcp-settings">
|
||||
Configure your MCP servers and settings
|
||||
</Card>
|
||||
<Card title="Quick Start" icon="rocket" href="/quickstart">
|
||||
Get up and running in 5 minutes
|
||||
</Card>
|
||||
<Card title="Server Management" icon="server" href="/features/server-management">
|
||||
Learn how to manage your MCP servers
|
||||
</Card>
|
||||
<Card title="API Reference" icon="code" href="/api-reference/introduction">
|
||||
Explore the complete API documentation
|
||||
</Card>
|
||||
</CardGroup>
|
||||
149
docs/openapi-schema-support.md
Normal file
@@ -0,0 +1,149 @@
|
||||
# OpenAPI Schema Support in MCPHub
|
||||
|
||||
MCPHub now supports both OpenAPI specification URLs and complete JSON schemas for OpenAPI server configuration. This allows you to either reference an external OpenAPI specification file or embed the complete schema directly in your configuration.
|
||||
|
||||
## Configuration Options
|
||||
|
||||
### 1. Using OpenAPI Specification URL (Traditional)
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "openapi",
|
||||
"openapi": {
|
||||
"url": "https://api.example.com/openapi.json",
|
||||
"version": "3.1.0",
|
||||
"security": {
|
||||
"type": "apiKey",
|
||||
"apiKey": {
|
||||
"name": "X-API-Key",
|
||||
"in": "header",
|
||||
"value": "your-api-key"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Using Complete JSON Schema (New)
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "openapi",
|
||||
"openapi": {
|
||||
"schema": {
|
||||
"openapi": "3.1.0",
|
||||
"info": {
|
||||
"title": "My API",
|
||||
"version": "1.0.0"
|
||||
},
|
||||
"servers": [
|
||||
{
|
||||
"url": "https://api.example.com"
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"/users": {
|
||||
"get": {
|
||||
"operationId": "getUsers",
|
||||
"summary": "Get all users",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "List of users"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"version": "3.1.0",
|
||||
"security": {
|
||||
"type": "apiKey",
|
||||
"apiKey": {
|
||||
"name": "X-API-Key",
|
||||
"in": "header",
|
||||
"value": "your-api-key"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Benefits of JSON Schema Support
|
||||
|
||||
1. **Offline Development**: No need for external URLs during development
|
||||
2. **Version Control**: Schema changes can be tracked in your configuration
|
||||
3. **Security**: No external dependencies or network calls required
|
||||
4. **Customization**: Full control over the API specification
|
||||
5. **Testing**: Easy to create test configurations with mock schemas
|
||||
|
||||
## Frontend Form Support
|
||||
|
||||
The web interface now includes:
|
||||
|
||||
- **Input Mode Selection**: Choose between "Specification URL" or "JSON Schema"
|
||||
- **URL Input**: Traditional URL input field for external specifications
|
||||
- **Schema Editor**: Large text area with syntax highlighting for JSON schema input
|
||||
- **Validation**: Client-side JSON validation before submission
|
||||
- **Help Text**: Contextual help for schema format
|
||||
|
||||
## API Validation
|
||||
|
||||
The backend validates that:
|
||||
|
||||
- At least one of `url` or `schema` is provided for OpenAPI servers
|
||||
- JSON schemas are properly formatted when provided
|
||||
- Security configurations are valid for both input modes
|
||||
- All required OpenAPI fields are present
|
||||
|
||||
## Migration Guide
|
||||
|
||||
### From URL to Schema
|
||||
|
||||
If you want to convert an existing URL-based configuration to schema-based:
|
||||
|
||||
1. Download your OpenAPI specification from the URL
|
||||
2. Copy the JSON content
|
||||
3. Update your configuration to use the `schema` field instead of `url`
|
||||
4. Paste the JSON content as the value of the `schema` field
|
||||
|
||||
### Maintaining Both
|
||||
|
||||
You can include both `url` and `schema` in your configuration. The system will prioritize the `schema` field if both are present.
|
||||
|
||||
## Examples
|
||||
|
||||
See the `examples/openapi-schema-config.json` file for complete configuration examples showing both URL and schema-based configurations.
|
||||
|
||||
## Technical Implementation
|
||||
|
||||
- **Backend**: OpenAPI client supports both SwaggerParser.dereference() with URLs and direct schema objects
|
||||
- **Frontend**: Dynamic form rendering based on selected input mode
|
||||
- **Validation**: Enhanced validation logic in server controllers
|
||||
- **Type Safety**: Updated TypeScript interfaces for both input modes
|
||||
|
||||
## Security Considerations
|
||||
|
||||
When using JSON schemas:
|
||||
|
||||
- Ensure schemas are properly validated before use
|
||||
- Be aware that large schemas increase configuration file size
|
||||
- Consider using URL-based approach for frequently changing APIs
|
||||
- Store sensitive information (like API keys) in environment variables, not in schemas
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
1. **Invalid JSON**: Ensure your schema is valid JSON format
|
||||
2. **Missing Required Fields**: OpenAPI schemas must include `openapi`, `info`, and `paths` fields
|
||||
3. **Schema Size**: Very large schemas may impact performance
|
||||
4. **Server Configuration**: Ensure the `servers` field in your schema points to the correct endpoints
|
||||
|
||||
### Validation Errors
|
||||
|
||||
The system provides detailed error messages for:
|
||||
|
||||
- Malformed JSON in schema field
|
||||
- Missing required OpenAPI fields
|
||||
- Invalid security configurations
|
||||
- Network issues with URL-based configurations
|
||||
172
docs/openapi-support.md
Normal file
@@ -0,0 +1,172 @@
|
||||
# OpenAPI Support in MCPHub
|
||||
|
||||
MCPHub now supports OpenAPI 3.1.1 servers as a new server type, allowing you to integrate REST APIs directly into your MCP workflow.
|
||||
|
||||
## Features
|
||||
|
||||
- ✅ **Full OpenAPI 3.1.1 Support**: Load and parse OpenAPI specifications
|
||||
- ✅ **Multiple Security Types**: None, API Key, HTTP Authentication, OAuth 2.0, OpenID Connect
|
||||
- ✅ **Dynamic Tool Generation**: Automatically creates MCP tools from OpenAPI operations
|
||||
- ✅ **Type Safety**: Full TypeScript support with proper type definitions
|
||||
- ✅ **Frontend Integration**: Easy-to-use forms for configuring OpenAPI servers
|
||||
- ✅ **Internationalization**: Support for English and Chinese languages
|
||||
|
||||
## Configuration
|
||||
|
||||
### Basic Configuration
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "openapi",
|
||||
"openapi": {
|
||||
"url": "https://api.example.com/v1/openapi.json",
|
||||
"version": "3.1.0",
|
||||
"security": {
|
||||
"type": "none"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### With API Key Authentication
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "openapi",
|
||||
"openapi": {
|
||||
"url": "https://api.example.com/v1/openapi.json",
|
||||
"version": "3.1.0",
|
||||
"security": {
|
||||
"type": "apiKey",
|
||||
"apiKey": {
|
||||
"name": "X-API-Key",
|
||||
"in": "header",
|
||||
"value": "your-api-key-here"
|
||||
}
|
||||
}
|
||||
},
|
||||
"headers": {
|
||||
"Accept": "application/json"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### With HTTP Bearer Authentication
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "openapi",
|
||||
"openapi": {
|
||||
"url": "https://api.example.com/v1/openapi.json",
|
||||
"version": "3.1.0",
|
||||
"security": {
|
||||
"type": "http",
|
||||
"http": {
|
||||
"scheme": "bearer",
|
||||
"credentials": "your-bearer-token-here"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Supported Security Types
|
||||
|
||||
1. **None**: No authentication required
|
||||
2. **API Key**: API key in header or query parameter
|
||||
3. **HTTP**: Basic, Bearer, or Digest authentication
|
||||
4. **OAuth 2.0**: OAuth 2.0 access tokens
|
||||
5. **OpenID Connect**: OpenID Connect ID tokens
|
||||
|
||||
## How It Works
|
||||
|
||||
1. **Specification Loading**: The OpenAPI client fetches and parses the OpenAPI specification
|
||||
2. **Tool Generation**: Each operation in the spec becomes an MCP tool
|
||||
3. **Request Handling**: Tools handle parameter validation and API calls
|
||||
4. **Response Processing**: API responses are returned as tool results
|
||||
|
||||
## Frontend Usage
|
||||
|
||||
1. Navigate to the Servers page
|
||||
2. Click "Add Server"
|
||||
3. Select "OpenAPI" as the server type
|
||||
4. Enter the OpenAPI specification URL
|
||||
5. Configure security settings if needed
|
||||
6. Add any additional headers
|
||||
7. Save the configuration
|
||||
|
||||
## Testing
|
||||
|
||||
You can test the OpenAPI integration using the provided test scripts:
|
||||
|
||||
```bash
|
||||
# Test OpenAPI client directly
|
||||
npx tsx test-openapi.ts
|
||||
|
||||
# Test full integration
|
||||
npx tsx test-integration.ts
|
||||
```
|
||||
|
||||
## Example: Swagger Petstore
|
||||
|
||||
The Swagger Petstore API is a perfect example for testing:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "openapi",
|
||||
"openapi": {
|
||||
"url": "https://petstore3.swagger.io/api/v3/openapi.json",
|
||||
"version": "3.1.0",
|
||||
"security": {
|
||||
"type": "none"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This will create tools like:
|
||||
|
||||
- `addPet`: Add a new pet to the store
|
||||
- `findPetsByStatus`: Find pets by status
|
||||
- `getPetById`: Find pet by ID
|
||||
- And many more...
|
||||
|
||||
## Error Handling
|
||||
|
||||
The OpenAPI client includes comprehensive error handling:
|
||||
|
||||
- Network errors are properly caught and reported
|
||||
- Invalid specifications are rejected with clear error messages
|
||||
- API errors include response status and body information
|
||||
- Type validation ensures proper parameter handling
|
||||
|
||||
## Limitations
|
||||
|
||||
- Only supports OpenAPI 3.x specifications (3.0.0 and above)
|
||||
- Complex authentication flows (like OAuth 2.0 authorization code flow) require manual token management
|
||||
- Large specifications may take time to parse initially
|
||||
- Some advanced OpenAPI features may not be fully supported
|
||||
|
||||
## Contributing
|
||||
|
||||
To add new features or fix bugs in the OpenAPI integration:
|
||||
|
||||
1. Backend types: `src/types/index.ts`
|
||||
2. OpenAPI client: `src/clients/openapi.ts`
|
||||
3. Service integration: `src/services/mcpService.ts`
|
||||
4. Frontend forms: `frontend/src/components/ServerForm.tsx`
|
||||
5. Internationalization: `frontend/src/locales/`
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**Q: My OpenAPI server won't connect**
|
||||
A: Check that the specification URL is accessible and returns valid JSON/YAML
|
||||
|
||||
**Q: Tools aren't showing up**
|
||||
A: Verify that your OpenAPI specification includes valid operations with required fields
|
||||
|
||||
**Q: Authentication isn't working**
|
||||
A: Double-check your security configuration matches the API's requirements
|
||||
|
||||
**Q: Getting CORS errors**
|
||||
A: The API server needs to allow CORS requests from your MCPHub domain
|
||||
@@ -1,97 +1,228 @@
|
||||
---
|
||||
title: 'Quickstart'
|
||||
description: 'Start building awesome documentation in under 5 minutes'
|
||||
title: 'Quick Start Guide'
|
||||
description: 'Get MCPHub running in 5 minutes'
|
||||
---
|
||||
|
||||
## Setup your development
|
||||
## Installation
|
||||
|
||||
Learn how to update your docs locally and deploy them to the public.
|
||||
<Tabs>
|
||||
<Tab title="Docker (Recommended)">
|
||||
The fastest way to get started with MCPHub is using Docker:
|
||||
|
||||
### Edit and preview
|
||||
```bash
|
||||
# Run with default configuration
|
||||
docker run -p 3000:3000 samanhappy/mcphub
|
||||
```
|
||||
|
||||
Or mount your custom configuration:
|
||||
|
||||
```bash
|
||||
# Run with custom MCP settings
|
||||
docker run -p 3000:3000 \
|
||||
-v $(pwd)/mcp_settings.json:/app/mcp_settings.json \
|
||||
samanhappy/mcphub
|
||||
```
|
||||
|
||||
</Tab>
|
||||
<Tab title="Local Development">
|
||||
For development or customization:
|
||||
|
||||
```bash
|
||||
# Clone the repository
|
||||
git clone https://github.com/samanhappy/mcphub.git
|
||||
cd mcphub
|
||||
|
||||
# Install dependencies
|
||||
pnpm install
|
||||
|
||||
# Start development servers
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
This starts both backend (port 3001) and frontend (port 5173) in development mode.
|
||||
|
||||
</Tab>
|
||||
<Tab title="npm Package">
|
||||
Install MCPHub as a global package:
|
||||
|
||||
```bash
|
||||
# Install globally
|
||||
npm install -g @samanhappy/mcphub
|
||||
|
||||
# Run MCPHub
|
||||
mcphub
|
||||
```
|
||||
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
## Initial Setup
|
||||
|
||||
### 1. Access the Dashboard
|
||||
|
||||
Open your browser and navigate to:
|
||||
|
||||
```
|
||||
http://localhost:3000
|
||||
```
|
||||
|
||||
### 2. Login
|
||||
|
||||
Use the default credentials:
|
||||
|
||||
- **Username**: `admin`
|
||||
- **Password**: `admin123`
|
||||
|
||||
<Warning>Change these default credentials immediately after first login for security.</Warning>
|
||||
|
||||
### 3. Configure Your First MCP Server
|
||||
|
||||
1. Click **"Add Server"** in the dashboard
|
||||
2. Enter server details:
|
||||
- **Name**: A unique identifier (e.g., `fetch`)
|
||||
- **Command**: The executable command (`uvx`)
|
||||
- **Args**: Command arguments (`["mcp-server-fetch"]`)
|
||||
- **Environment**: Any required environment variables
|
||||
|
||||
Example configuration for a fetch server:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "fetch",
|
||||
"command": "uvx",
|
||||
"args": ["mcp-server-fetch"],
|
||||
"env": {}
|
||||
}
|
||||
```
|
||||
|
||||
## Basic Usage
|
||||
|
||||
### Connecting AI Clients
|
||||
|
||||
Once your servers are configured, connect your AI clients using MCPHub endpoints:
|
||||
|
||||
<Tabs>
|
||||
<Tab title="All Servers">
|
||||
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}
|
||||
```
|
||||
</Tab>
|
||||
<Tab title="Individual Server">
|
||||
Access a single server: ``` http://localhost:3000/mcp/{server - name}
|
||||
```
|
||||
</Tab>
|
||||
<Tab title="Smart Routing">
|
||||
Use AI-powered tool discovery: ``` http://localhost:3000/mcp/$smart ```
|
||||
<Info>Smart routing requires PostgreSQL with pgvector and an OpenAI API key.</Info>
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
### Example: Adding Popular MCP Servers
|
||||
|
||||
Here are some popular MCP servers you can add:
|
||||
|
||||
<AccordionGroup>
|
||||
<Accordion icon="github" title="Clone your docs locally">
|
||||
During the onboarding process, we created a repository on your Github with
|
||||
your docs content. You can find this repository on our
|
||||
[dashboard](https://dashboard.mintlify.com). To clone the repository
|
||||
locally, follow these
|
||||
[instructions](https://docs.github.com/en/repositories/creating-and-managing-repositories/cloning-a-repository)
|
||||
in your terminal.
|
||||
<Accordion title="Web Fetch Server">
|
||||
```json
|
||||
{
|
||||
"name": "fetch",
|
||||
"command": "uvx",
|
||||
"args": ["mcp-server-fetch"]
|
||||
}
|
||||
```
|
||||
</Accordion>
|
||||
<Accordion icon="rectangle-terminal" title="Preview changes">
|
||||
Previewing helps you make sure your changes look as intended. We built a
|
||||
command line interface to render these changes locally.
|
||||
1. Install the
|
||||
[Mintlify CLI](https://www.npmjs.com/package/mintlify) to preview the
|
||||
documentation changes locally with this command: ``` npm i -g mintlify ```
|
||||
2. Run the following command at the root of your documentation (where
|
||||
`docs.json` is): ``` mintlify dev ```
|
||||
<Note>
|
||||
If you’re currently using the legacy ```mint.json``` configuration file, please update the Mintlify CLI:
|
||||
|
||||
|
||||
```npm i -g mintlify@latest```
|
||||
And run the new upgrade command in your docs repository:
|
||||
|
||||
```mintlify upgrade```
|
||||
You should now be using the new ```docs.json``` configuration file. Feel free to delete the ```mint.json``` file from your repository.
|
||||
</Note>
|
||||
|
||||
<Accordion title="Playwright Browser Automation">
|
||||
```json
|
||||
{
|
||||
"name": "playwright",
|
||||
"command": "npx",
|
||||
"args": ["@playwright/mcp@latest", "--headless"]
|
||||
}
|
||||
```
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Amap Maps (with API key)">
|
||||
```json
|
||||
{
|
||||
"name": "amap",
|
||||
"command": "npx",
|
||||
"args": ["-y", "@amap/amap-maps-mcp-server"],
|
||||
"env": {
|
||||
"AMAP_MAPS_API_KEY": "your-api-key-here"
|
||||
}
|
||||
}
|
||||
```
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Slack Integration">
|
||||
```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>
|
||||
|
||||
### Deploy your changes
|
||||
## Verification
|
||||
|
||||
<AccordionGroup>
|
||||
Test your setup by making a simple request:
|
||||
|
||||
<Accordion icon="message-bot" title="Install our Github app">
|
||||
Our Github app automatically deploys your changes to your docs site, so you
|
||||
don't need to manage deployments yourself. You can find the link to install on
|
||||
your [dashboard](https://dashboard.mintlify.com). Once the bot has been
|
||||
successfully installed, there should be a check mark next to the commit hash
|
||||
of the repo.
|
||||
</Accordion>
|
||||
<Accordion icon="rocket" title="Push your changes">
|
||||
[Commit and push your changes to
|
||||
Git](https://docs.github.com/en/get-started/using-git/pushing-commits-to-a-remote-repository#about-git-push)
|
||||
for your changes to update in your docs site. If you push and don't see that
|
||||
the Github app successfully deployed your changes, you can also manually
|
||||
update your docs through our [dashboard](https://dashboard.mintlify.com).
|
||||
</Accordion>
|
||||
```bash
|
||||
curl -X POST http://localhost:3000/mcp \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"jsonrpc": "2.0",
|
||||
"id": 1,
|
||||
"method": "tools/list",
|
||||
"params": {}
|
||||
}'
|
||||
```
|
||||
|
||||
</AccordionGroup>
|
||||
You should receive a list of available tools from your configured MCP servers.
|
||||
|
||||
## Update your docs
|
||||
|
||||
Add content directly in your files with MDX syntax and React components. You can use any of our components, or even build your own.
|
||||
|
||||
<CardGroup>
|
||||
|
||||
<Card title="Add Content With MDX" icon="file" href="/essentials/markdown">
|
||||
Add content to your docs with MDX syntax.
|
||||
</Card>
|
||||
|
||||
<Card
|
||||
title="Add Code Blocks"
|
||||
icon="square-code"
|
||||
href="/essentials/code"
|
||||
>
|
||||
Add code directly to your docs with syntax highlighting.
|
||||
</Card>
|
||||
|
||||
<Card
|
||||
title="Add Images"
|
||||
icon="image"
|
||||
href="/essentials/images"
|
||||
>
|
||||
Add images to your docs to make them more engaging.
|
||||
</Card>
|
||||
|
||||
<Card
|
||||
title="Add Custom Components"
|
||||
icon="puzzle-piece"
|
||||
href="/essentials/reusable-snippets"
|
||||
>
|
||||
Add templates to your docs to make them more reusable.
|
||||
</Card>
|
||||
## Next Steps
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="Server Management" icon="server" href="/features/server-management">
|
||||
Learn advanced server configuration and management
|
||||
</Card>
|
||||
<Card title="Group Management" icon="users" href="/features/group-management">
|
||||
Organize servers into logical groups
|
||||
</Card>
|
||||
<Card title="Smart Routing" icon="route" href="/features/smart-routing">
|
||||
Set up AI-powered tool discovery
|
||||
</Card>
|
||||
<Card title="API Reference" icon="code" href="/api-reference/introduction">
|
||||
Explore the complete API documentation
|
||||
</Card>
|
||||
</CardGroup>
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
<AccordionGroup>
|
||||
<Accordion title="Server won't start">
|
||||
- Check if the MCP server command is accessible in your PATH - Verify environment variables are
|
||||
correctly set - Check MCPHub logs for detailed error messages
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Can't connect from AI client">
|
||||
- Ensure MCPHub is running on the correct port - Check firewall settings - Verify the endpoint
|
||||
URL format
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Authentication issues">
|
||||
- Verify credentials are correct - Check if JWT token is valid - Try clearing browser cache and
|
||||
cookies
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
|
||||
Need more help? Join our [Discord community](https://discord.gg/qMKNsn5Q) for support!
|
||||
|
||||
172
docs/testing-framework.md
Normal file
@@ -0,0 +1,172 @@
|
||||
# 测试框架和自动化测试实现报告
|
||||
|
||||
## 概述
|
||||
|
||||
本项目已成功引入现代化的测试框架和自动化测试流程。实现了基于Jest的测试环境,支持TypeScript、ES模块,并包含完整的CI/CD配置。
|
||||
|
||||
## 已实现的功能
|
||||
|
||||
### 1. 测试框架配置
|
||||
|
||||
- **Jest配置**: 使用`jest.config.cjs`配置文件,支持ES模块和TypeScript
|
||||
- **覆盖率报告**: 配置了代码覆盖率收集和报告
|
||||
- **测试环境**: 支持Node.js环境的单元测试和集成测试
|
||||
- **模块映射**: 配置了路径别名支持
|
||||
|
||||
### 2. 测试工具和辅助函数
|
||||
|
||||
创建了完善的测试工具库 (`tests/utils/testHelpers.ts`):
|
||||
|
||||
- **认证工具**: JWT token生成和管理
|
||||
- **HTTP测试**: Supertest集成用于API测试
|
||||
- **数据生成**: 测试数据工厂函数
|
||||
- **响应断言**: 自定义API响应验证器
|
||||
- **环境管理**: 测试环境变量配置
|
||||
|
||||
### 3. 测试用例实现
|
||||
|
||||
已实现的测试场景:
|
||||
|
||||
#### 基础配置测试 (`tests/basic.test.ts`)
|
||||
- Jest配置验证
|
||||
- 异步操作支持测试
|
||||
- 自定义匹配器验证
|
||||
|
||||
#### 认证逻辑测试 (`tests/auth.logic.test.ts`)
|
||||
- 用户登录逻辑
|
||||
- 密码验证
|
||||
- JWT生成和验证
|
||||
- 错误处理场景
|
||||
- 用户数据验证
|
||||
|
||||
#### 路径工具测试 (`tests/utils/pathLogic.test.ts`)
|
||||
- 配置文件路径解析
|
||||
- 环境变量处理
|
||||
- 文件系统操作
|
||||
- 错误处理和边界条件
|
||||
- 跨平台路径处理
|
||||
|
||||
### 4. CI/CD配置
|
||||
|
||||
GitHub Actions配置 (`.github/workflows/ci.yml`):
|
||||
|
||||
- **多Node.js版本支持**: 18.x和20.x
|
||||
- **自动化测试流程**:
|
||||
- 代码检查 (ESLint)
|
||||
- 类型检查 (TypeScript)
|
||||
- 单元测试执行
|
||||
- 覆盖率报告
|
||||
- **构建验证**: 应用构建和产物验证
|
||||
- **集成测试**: 包含数据库环境的集成测试
|
||||
|
||||
### 5. 测试脚本
|
||||
|
||||
在`package.json`中添加的测试命令:
|
||||
|
||||
```json
|
||||
{
|
||||
"test": "jest",
|
||||
"test:watch": "jest --watch",
|
||||
"test:coverage": "jest --coverage",
|
||||
"test:verbose": "jest --verbose",
|
||||
"test:ci": "jest --ci --coverage --watchAll=false"
|
||||
}
|
||||
```
|
||||
|
||||
## 测试结果
|
||||
|
||||
当前测试统计:
|
||||
- **测试套件**: 3个
|
||||
- **测试用例**: 19个
|
||||
- **通过率**: 100%
|
||||
- **执行时间**: ~15秒
|
||||
|
||||
### 测试覆盖的功能模块
|
||||
|
||||
1. **认证系统**: 用户登录、JWT处理、密码验证
|
||||
2. **配置管理**: 文件路径解析、环境变量处理
|
||||
3. **基础设施**: Jest配置、测试工具验证
|
||||
|
||||
## 技术特点
|
||||
|
||||
### 现代化特性
|
||||
|
||||
- **ES模块支持**: 完全支持ES2022模块语法
|
||||
- **TypeScript集成**: 类型安全的测试编写
|
||||
- **异步测试**: Promise和async/await支持
|
||||
- **模拟系统**: Jest mock功能的深度使用
|
||||
- **参数化测试**: 数据驱动的测试用例
|
||||
|
||||
### 最佳实践
|
||||
|
||||
- **测试隔离**: 每个测试用例独立运行
|
||||
- **Mock管理**: 统一的mock清理和重置
|
||||
- **错误处理**: 完整的错误场景测试
|
||||
- **边界测试**: 输入验证和边界条件覆盖
|
||||
- **文档化**: 清晰的测试用例命名和描述
|
||||
|
||||
## 后续扩展计划
|
||||
|
||||
### 短期目标
|
||||
|
||||
1. **API测试**: 为REST API端点添加集成测试
|
||||
2. **数据库测试**: 添加数据模型和存储层测试
|
||||
3. **中间件测试**: 认证和权限中间件测试
|
||||
4. **服务层测试**: 核心业务逻辑测试
|
||||
|
||||
### 中期目标
|
||||
|
||||
1. **端到端测试**: 使用Playwright或Cypress
|
||||
2. **性能测试**: API响应时间和负载测试
|
||||
3. **安全测试**: 输入验证和安全漏洞测试
|
||||
4. **契约测试**: API契约验证
|
||||
|
||||
### 长期目标
|
||||
|
||||
1. **测试数据管理**: 测试数据库和fixture管理
|
||||
2. **视觉回归测试**: UI组件的视觉测试
|
||||
3. **监控集成**: 生产环境测试监控
|
||||
4. **自动化测试报告**: 详细的测试报告和趋势分析
|
||||
|
||||
## 开发指南
|
||||
|
||||
### 添加新测试用例
|
||||
|
||||
1. 在`tests/`目录下创建对应的测试文件
|
||||
2. 使用`testHelpers.ts`中的工具函数
|
||||
3. 遵循命名约定: `*.test.ts`或`*.spec.ts`
|
||||
4. 确保测试用例具有清晰的描述和断言
|
||||
|
||||
### 运行测试
|
||||
|
||||
```bash
|
||||
# 运行所有测试
|
||||
pnpm test
|
||||
|
||||
# 监听模式
|
||||
pnpm test:watch
|
||||
|
||||
# 生成覆盖率报告
|
||||
pnpm test:coverage
|
||||
|
||||
# CI模式运行
|
||||
pnpm test:ci
|
||||
```
|
||||
|
||||
### Mock最佳实践
|
||||
|
||||
- 在`beforeEach`中清理所有mock
|
||||
- 使用具体的mock实现而不是空函数
|
||||
- 验证mock被正确调用
|
||||
- 保持mock的一致性和可维护性
|
||||
|
||||
## 结论
|
||||
|
||||
本项目已成功建立了完整的现代化测试框架,具备以下优势:
|
||||
|
||||
1. **高度可扩展**: 易于添加新的测试用例和测试类型
|
||||
2. **开发友好**: 丰富的工具函数和清晰的结构
|
||||
3. **CI/CD就绪**: 完整的自动化流水线配置
|
||||
4. **质量保证**: 代码覆盖率和持续测试验证
|
||||
|
||||
这个测试框架为项目的持续发展和质量保证提供了坚实的基础,支持敏捷开发和持续集成的最佳实践。
|
||||
572
docs/zh/api-reference/endpoint/create.mdx
Normal file
@@ -0,0 +1,572 @@
|
||||
---
|
||||
title: '创建资源'
|
||||
description: '创建新的 MCP 服务器、用户和组'
|
||||
---
|
||||
|
||||
## 创建服务器
|
||||
|
||||
### 端点
|
||||
|
||||
```http
|
||||
POST /api/servers
|
||||
```
|
||||
|
||||
### 请求
|
||||
|
||||
#### 请求头
|
||||
|
||||
```http
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer YOUR_JWT_TOKEN
|
||||
```
|
||||
|
||||
#### 请求体
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "文件系统服务器",
|
||||
"command": "npx",
|
||||
"args": ["-y", "@modelcontextprotocol/server-filesystem", "/allowed/path"],
|
||||
"env": {
|
||||
"NODE_ENV": "production",
|
||||
"DEBUG": "mcp:*",
|
||||
"MAX_FILES": "1000"
|
||||
},
|
||||
"cwd": "/app/workspace",
|
||||
"timeout": 30000,
|
||||
"retries": 3,
|
||||
"enabled": true,
|
||||
"description": "提供文件系统访问的 MCP 服务器",
|
||||
"tags": ["filesystem", "production"],
|
||||
"healthCheck": {
|
||||
"enabled": true,
|
||||
"interval": 30000,
|
||||
"timeout": 5000,
|
||||
"retries": 3,
|
||||
"endpoint": "/health"
|
||||
},
|
||||
"resources": {
|
||||
"memory": {
|
||||
"limit": "512MB",
|
||||
"warning": "400MB"
|
||||
},
|
||||
"cpu": {
|
||||
"limit": "50%"
|
||||
}
|
||||
},
|
||||
"logging": {
|
||||
"level": "info",
|
||||
"file": "/var/log/mcphub/server.log",
|
||||
"maxSize": "100MB",
|
||||
"maxFiles": 5
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 必填字段
|
||||
|
||||
- `name` (string): 服务器唯一名称
|
||||
- `command` (string): 执行命令
|
||||
- `args` (array): 命令参数数组
|
||||
|
||||
#### 可选字段
|
||||
|
||||
- `env` (object): 环境变量键值对
|
||||
- `cwd` (string): 工作目录
|
||||
- `timeout` (number): 超时时间(毫秒)
|
||||
- `retries` (number): 重试次数
|
||||
- `enabled` (boolean): 是否启用(默认 true)
|
||||
- `description` (string): 服务器描述
|
||||
- `tags` (array): 标签数组
|
||||
- `healthCheck` (object): 健康检查配置
|
||||
- `resources` (object): 资源限制配置
|
||||
- `logging` (object): 日志配置
|
||||
|
||||
### 响应
|
||||
|
||||
#### 成功响应 (201 Created)
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"id": "server-abc123",
|
||||
"name": "文件系统服务器",
|
||||
"status": "stopped",
|
||||
"command": "npx",
|
||||
"args": ["-y", "@modelcontextprotocol/server-filesystem", "/allowed/path"],
|
||||
"env": {
|
||||
"NODE_ENV": "production",
|
||||
"DEBUG": "mcp:*",
|
||||
"MAX_FILES": "1000"
|
||||
},
|
||||
"cwd": "/app/workspace",
|
||||
"timeout": 30000,
|
||||
"retries": 3,
|
||||
"enabled": true,
|
||||
"description": "提供文件系统访问的 MCP 服务器",
|
||||
"tags": ["filesystem", "production"],
|
||||
"healthCheck": {
|
||||
"enabled": true,
|
||||
"interval": 30000,
|
||||
"timeout": 5000,
|
||||
"retries": 3,
|
||||
"endpoint": "/health",
|
||||
"status": "unknown"
|
||||
},
|
||||
"resources": {
|
||||
"memory": {
|
||||
"limit": "512MB",
|
||||
"warning": "400MB",
|
||||
"current": "0MB"
|
||||
},
|
||||
"cpu": {
|
||||
"limit": "50%",
|
||||
"current": "0%"
|
||||
}
|
||||
},
|
||||
"logging": {
|
||||
"level": "info",
|
||||
"file": "/var/log/mcphub/server.log",
|
||||
"maxSize": "100MB",
|
||||
"maxFiles": 5,
|
||||
"currentSize": "0MB"
|
||||
},
|
||||
"createdAt": "2024-01-01T12:00:00Z",
|
||||
"updatedAt": "2024-01-01T12:00:00Z",
|
||||
"createdBy": "user123"
|
||||
},
|
||||
"message": "服务器创建成功"
|
||||
}
|
||||
```
|
||||
|
||||
#### 错误响应
|
||||
|
||||
**400 Bad Request - 参数错误**
|
||||
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"error": {
|
||||
"code": "VALIDATION_ERROR",
|
||||
"message": "请求数据验证失败",
|
||||
"details": [
|
||||
{
|
||||
"field": "name",
|
||||
"message": "服务器名称不能为空"
|
||||
},
|
||||
{
|
||||
"field": "command",
|
||||
"message": "执行命令不能为空"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**409 Conflict - 名称冲突**
|
||||
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"error": {
|
||||
"code": "RESOURCE_CONFLICT",
|
||||
"message": "服务器名称已存在",
|
||||
"details": {
|
||||
"field": "name",
|
||||
"value": "文件系统服务器",
|
||||
"conflictingResourceId": "server-xyz789"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 示例
|
||||
|
||||
#### cURL
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:3000/api/servers \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer YOUR_JWT_TOKEN" \
|
||||
-d '{
|
||||
"name": "文件系统服务器",
|
||||
"command": "npx",
|
||||
"args": ["-y", "@modelcontextprotocol/server-filesystem", "/data"],
|
||||
"env": {
|
||||
"NODE_ENV": "production"
|
||||
},
|
||||
"description": "生产环境文件系统服务器"
|
||||
}'
|
||||
```
|
||||
|
||||
#### JavaScript
|
||||
|
||||
```javascript
|
||||
const response = await fetch('/api/servers', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
name: '文件系统服务器',
|
||||
command: 'npx',
|
||||
args: ['-y', '@modelcontextprotocol/server-filesystem', '/data'],
|
||||
env: {
|
||||
NODE_ENV: 'production',
|
||||
},
|
||||
description: '生产环境文件系统服务器',
|
||||
}),
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
if (result.success) {
|
||||
console.log('服务器创建成功:', result.data);
|
||||
} else {
|
||||
console.error('创建失败:', result.error);
|
||||
}
|
||||
```
|
||||
|
||||
#### Python
|
||||
|
||||
```python
|
||||
import requests
|
||||
|
||||
response = requests.post(
|
||||
'http://localhost:3000/api/servers',
|
||||
headers={
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': f'Bearer {token}'
|
||||
},
|
||||
json={
|
||||
'name': '文件系统服务器',
|
||||
'command': 'npx',
|
||||
'args': ['-y', '@modelcontextprotocol/server-filesystem', '/data'],
|
||||
'env': {
|
||||
'NODE_ENV': 'production'
|
||||
},
|
||||
'description': '生产环境文件系统服务器'
|
||||
}
|
||||
)
|
||||
|
||||
if response.status_code == 201:
|
||||
result = response.json()
|
||||
print('服务器创建成功:', result['data'])
|
||||
else:
|
||||
error = response.json()
|
||||
print('创建失败:', error['error'])
|
||||
```
|
||||
|
||||
## 创建用户
|
||||
|
||||
### 端点
|
||||
|
||||
```http
|
||||
POST /api/users
|
||||
```
|
||||
|
||||
### 请求体
|
||||
|
||||
```json
|
||||
{
|
||||
"username": "newuser",
|
||||
"email": "user@example.com",
|
||||
"password": "SecurePassword123!",
|
||||
"role": "user",
|
||||
"groups": ["dev-team", "qa-team"],
|
||||
"profile": {
|
||||
"firstName": "张",
|
||||
"lastName": "三",
|
||||
"department": "开发部",
|
||||
"title": "软件工程师",
|
||||
"phone": "+86-138-0013-8000",
|
||||
"location": "北京"
|
||||
},
|
||||
"preferences": {
|
||||
"language": "zh-CN",
|
||||
"timezone": "Asia/Shanghai",
|
||||
"notifications": {
|
||||
"email": true,
|
||||
"slack": false,
|
||||
"browser": true
|
||||
}
|
||||
},
|
||||
"enabled": true
|
||||
}
|
||||
```
|
||||
|
||||
### 响应 (201 Created)
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"id": "user-abc123",
|
||||
"username": "newuser",
|
||||
"email": "user@example.com",
|
||||
"role": "user",
|
||||
"groups": [
|
||||
{
|
||||
"id": "dev-team",
|
||||
"name": "开发团队",
|
||||
"role": "member"
|
||||
}
|
||||
],
|
||||
"profile": {
|
||||
"firstName": "张",
|
||||
"lastName": "三",
|
||||
"fullName": "张三",
|
||||
"department": "开发部",
|
||||
"title": "软件工程师",
|
||||
"phone": "+86-138-0013-8000",
|
||||
"location": "北京",
|
||||
"avatar": null
|
||||
},
|
||||
"preferences": {
|
||||
"language": "zh-CN",
|
||||
"timezone": "Asia/Shanghai",
|
||||
"notifications": {
|
||||
"email": true,
|
||||
"slack": false,
|
||||
"browser": true
|
||||
}
|
||||
},
|
||||
"enabled": true,
|
||||
"lastLoginAt": null,
|
||||
"createdAt": "2024-01-01T12:00:00Z",
|
||||
"updatedAt": "2024-01-01T12:00:00Z"
|
||||
},
|
||||
"message": "用户创建成功"
|
||||
}
|
||||
```
|
||||
|
||||
## 创建组
|
||||
|
||||
### 端点
|
||||
|
||||
```http
|
||||
POST /api/groups
|
||||
```
|
||||
|
||||
### 请求体
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "dev-team",
|
||||
"displayName": "开发团队",
|
||||
"description": "前端和后端开发人员",
|
||||
"parentGroup": null,
|
||||
"permissions": {
|
||||
"servers": {
|
||||
"create": false,
|
||||
"read": true,
|
||||
"update": true,
|
||||
"delete": false,
|
||||
"execute": true
|
||||
},
|
||||
"tools": {
|
||||
"filesystem": {
|
||||
"read": true,
|
||||
"write": true,
|
||||
"paths": ["/app/data", "/tmp"]
|
||||
},
|
||||
"web-search": {
|
||||
"enabled": true,
|
||||
"maxQueries": 100
|
||||
}
|
||||
},
|
||||
"monitoring": {
|
||||
"viewLogs": true,
|
||||
"viewMetrics": true,
|
||||
"exportData": false
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"autoAssign": false,
|
||||
"maxMembers": 50,
|
||||
"requireApproval": true,
|
||||
"sessionTimeout": "8h"
|
||||
},
|
||||
"quotas": {
|
||||
"requests": {
|
||||
"daily": 1000,
|
||||
"monthly": 30000
|
||||
},
|
||||
"storage": {
|
||||
"maxSize": "10GB"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 响应 (201 Created)
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"id": "group-abc123",
|
||||
"name": "dev-team",
|
||||
"displayName": "开发团队",
|
||||
"description": "前端和后端开发人员",
|
||||
"parentGroup": null,
|
||||
"permissions": {
|
||||
"servers": {
|
||||
"create": false,
|
||||
"read": true,
|
||||
"update": true,
|
||||
"delete": false,
|
||||
"execute": true
|
||||
},
|
||||
"tools": {
|
||||
"filesystem": {
|
||||
"read": true,
|
||||
"write": true,
|
||||
"paths": ["/app/data", "/tmp"]
|
||||
},
|
||||
"web-search": {
|
||||
"enabled": true,
|
||||
"maxQueries": 100
|
||||
}
|
||||
},
|
||||
"monitoring": {
|
||||
"viewLogs": true,
|
||||
"viewMetrics": true,
|
||||
"exportData": false
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"autoAssign": false,
|
||||
"maxMembers": 50,
|
||||
"requireApproval": true,
|
||||
"sessionTimeout": "8h"
|
||||
},
|
||||
"quotas": {
|
||||
"requests": {
|
||||
"daily": 1000,
|
||||
"monthly": 30000
|
||||
},
|
||||
"storage": {
|
||||
"maxSize": "10GB"
|
||||
}
|
||||
},
|
||||
"memberCount": 0,
|
||||
"serverCount": 0,
|
||||
"createdAt": "2024-01-01T12:00:00Z",
|
||||
"updatedAt": "2024-01-01T12:00:00Z",
|
||||
"createdBy": "admin"
|
||||
},
|
||||
"message": "组创建成功"
|
||||
}
|
||||
```
|
||||
|
||||
## 批量创建
|
||||
|
||||
### 批量创建服务器
|
||||
|
||||
```http
|
||||
POST /api/servers/bulk
|
||||
```
|
||||
|
||||
#### 请求体
|
||||
|
||||
```json
|
||||
{
|
||||
"servers": [
|
||||
{
|
||||
"name": "dev-server-1",
|
||||
"command": "python",
|
||||
"args": ["-m", "mcp_server"],
|
||||
"env": { "ENV": "development" }
|
||||
},
|
||||
{
|
||||
"name": "dev-server-2",
|
||||
"command": "node",
|
||||
"args": ["server.js"],
|
||||
"env": { "ENV": "development" }
|
||||
}
|
||||
],
|
||||
"options": {
|
||||
"skipExisting": true,
|
||||
"validateAll": true,
|
||||
"startAfterCreate": false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 响应 (201 Created)
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"created": [
|
||||
{
|
||||
"id": "server-1",
|
||||
"name": "dev-server-1",
|
||||
"status": "created"
|
||||
},
|
||||
{
|
||||
"id": "server-2",
|
||||
"name": "dev-server-2",
|
||||
"status": "created"
|
||||
}
|
||||
],
|
||||
"skipped": [],
|
||||
"failed": [],
|
||||
"summary": {
|
||||
"total": 2,
|
||||
"created": 2,
|
||||
"skipped": 0,
|
||||
"failed": 0
|
||||
}
|
||||
},
|
||||
"message": "批量创建完成,成功创建 2 个服务器"
|
||||
}
|
||||
```
|
||||
|
||||
## 验证
|
||||
|
||||
### 预验证创建请求
|
||||
|
||||
在实际创建资源之前验证请求:
|
||||
|
||||
```http
|
||||
POST /api/servers/validate
|
||||
```
|
||||
|
||||
#### 请求体
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "test-server",
|
||||
"command": "invalid-command",
|
||||
"args": []
|
||||
}
|
||||
```
|
||||
|
||||
#### 响应
|
||||
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"data": {
|
||||
"valid": false,
|
||||
"errors": [
|
||||
{
|
||||
"field": "command",
|
||||
"message": "命令 'invalid-command' 不存在或无法执行"
|
||||
}
|
||||
],
|
||||
"warnings": [
|
||||
{
|
||||
"field": "args",
|
||||
"message": "参数数组为空,服务器可能无法正常启动"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
有关更多 API 端点信息,请参阅 [获取资源](/zh/api-reference/endpoint/get)、[删除资源](/zh/api-reference/endpoint/delete) 和 [WebHooks](/zh/api-reference/endpoint/webhook) 文档。
|
||||
303
docs/zh/api-reference/endpoint/delete.mdx
Normal file
@@ -0,0 +1,303 @@
|
||||
---
|
||||
title: 删除资源 API
|
||||
description: 删除各种资源的 API 端点,包括服务器、组和配置等
|
||||
---
|
||||
|
||||
# 删除资源 API
|
||||
|
||||
本文档描述了用于删除各种资源的 API 端点。
|
||||
|
||||
## 删除 MCP 服务器
|
||||
|
||||
删除指定的 MCP 服务器配置。
|
||||
|
||||
### 端点
|
||||
|
||||
```http
|
||||
DELETE /api/servers/{id}
|
||||
```
|
||||
|
||||
### 参数
|
||||
|
||||
| 参数名 | 类型 | 位置 | 必需 | 描述 |
|
||||
| ------ | ------ | ---- | ---- | ------------------ |
|
||||
| id | string | path | 是 | 服务器的唯一标识符 |
|
||||
|
||||
### 请求示例
|
||||
|
||||
```bash
|
||||
curl -X DELETE \
|
||||
'https://api.mcphub.io/api/servers/mcp-server-123' \
|
||||
-H 'Authorization: Bearer YOUR_API_TOKEN' \
|
||||
-H 'Content-Type: application/json'
|
||||
```
|
||||
|
||||
### 响应
|
||||
|
||||
#### 成功响应 (204 No Content)
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "服务器已成功删除",
|
||||
"data": {
|
||||
"id": "mcp-server-123",
|
||||
"deletedAt": "2024-01-15T10:30:00Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 错误响应
|
||||
|
||||
**404 Not Found**
|
||||
|
||||
```json
|
||||
{
|
||||
"error": {
|
||||
"code": "SERVER_NOT_FOUND",
|
||||
"message": "指定的服务器不存在",
|
||||
"details": {
|
||||
"serverId": "mcp-server-123"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**409 Conflict**
|
||||
|
||||
```json
|
||||
{
|
||||
"error": {
|
||||
"code": "SERVER_IN_USE",
|
||||
"message": "服务器正在使用中,无法删除",
|
||||
"details": {
|
||||
"activeConnections": 5,
|
||||
"associatedGroups": ["group-1", "group-2"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 删除服务器组
|
||||
|
||||
删除指定的服务器组。
|
||||
|
||||
### 端点
|
||||
|
||||
```http
|
||||
DELETE /api/groups/{id}
|
||||
```
|
||||
|
||||
### 参数
|
||||
|
||||
| 参数名 | 类型 | 位置 | 必需 | 描述 |
|
||||
| ------ | ------- | ----- | ---- | ------------------------------ |
|
||||
| id | string | path | 是 | 组的唯一标识符 |
|
||||
| force | boolean | query | 否 | 是否强制删除(包含服务器的组) |
|
||||
|
||||
### 请求示例
|
||||
|
||||
```bash
|
||||
curl -X DELETE \
|
||||
'https://api.mcphub.io/api/groups/production-group?force=true' \
|
||||
-H 'Authorization: Bearer YOUR_API_TOKEN' \
|
||||
-H 'Content-Type: application/json'
|
||||
```
|
||||
|
||||
### 响应
|
||||
|
||||
#### 成功响应 (204 No Content)
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "服务器组已成功删除",
|
||||
"data": {
|
||||
"id": "production-group",
|
||||
"deletedServers": ["server-1", "server-2"],
|
||||
"deletedAt": "2024-01-15T10:30:00Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 删除配置项
|
||||
|
||||
删除指定的配置项。
|
||||
|
||||
### 端点
|
||||
|
||||
```http
|
||||
DELETE /api/config/{key}
|
||||
```
|
||||
|
||||
### 参数
|
||||
|
||||
| 参数名 | 类型 | 位置 | 必需 | 描述 |
|
||||
| ------ | ------ | ---- | ---- | -------- |
|
||||
| key | string | path | 是 | 配置键名 |
|
||||
|
||||
### 请求示例
|
||||
|
||||
```bash
|
||||
curl -X DELETE \
|
||||
'https://api.mcphub.io/api/config/custom-setting' \
|
||||
-H 'Authorization: Bearer YOUR_API_TOKEN'
|
||||
```
|
||||
|
||||
### 响应
|
||||
|
||||
#### 成功响应 (200 OK)
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "配置项已删除",
|
||||
"data": {
|
||||
"key": "custom-setting",
|
||||
"previousValue": "old-value",
|
||||
"deletedAt": "2024-01-15T10:30:00Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 批量删除
|
||||
|
||||
### 批量删除服务器
|
||||
|
||||
删除多个 MCP 服务器。
|
||||
|
||||
#### 端点
|
||||
|
||||
```http
|
||||
DELETE /api/servers/batch
|
||||
```
|
||||
|
||||
#### 请求体
|
||||
|
||||
```json
|
||||
{
|
||||
"serverIds": ["server-1", "server-2", "server-3"],
|
||||
"force": false
|
||||
}
|
||||
```
|
||||
|
||||
#### 响应
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "批量删除完成",
|
||||
"data": {
|
||||
"deleted": ["server-1", "server-3"],
|
||||
"failed": [
|
||||
{
|
||||
"id": "server-2",
|
||||
"reason": "服务器正在使用中"
|
||||
}
|
||||
],
|
||||
"summary": {
|
||||
"total": 3,
|
||||
"deleted": 2,
|
||||
"failed": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 软删除 vs 硬删除
|
||||
|
||||
### 软删除
|
||||
|
||||
默认情况下,MCPHub 使用软删除机制:
|
||||
|
||||
- 资源被标记为已删除但保留在数据库中
|
||||
- 可以通过恢复 API 恢复删除的资源
|
||||
- 删除的资源在列表 API 中默认不显示
|
||||
|
||||
### 硬删除
|
||||
|
||||
使用 `permanent=true` 参数执行硬删除:
|
||||
|
||||
```bash
|
||||
curl -X DELETE \
|
||||
'https://api.mcphub.io/api/servers/mcp-server-123?permanent=true' \
|
||||
-H 'Authorization: Bearer YOUR_API_TOKEN'
|
||||
```
|
||||
|
||||
<Warning>硬删除操作不可逆,请谨慎使用。</Warning>
|
||||
|
||||
## 权限要求
|
||||
|
||||
| 操作 | 所需权限 |
|
||||
| ---------- | ------------------------ |
|
||||
| 删除服务器 | `servers:delete` |
|
||||
| 删除组 | `groups:delete` |
|
||||
| 删除配置 | `config:delete` |
|
||||
| 硬删除 | `admin:permanent_delete` |
|
||||
|
||||
## 错误代码
|
||||
|
||||
| 错误代码 | HTTP 状态码 | 描述 |
|
||||
| -------------------------- | ----------- | ---------------- |
|
||||
| `RESOURCE_NOT_FOUND` | 404 | 资源不存在 |
|
||||
| `RESOURCE_IN_USE` | 409 | 资源正在使用中 |
|
||||
| `INSUFFICIENT_PERMISSIONS` | 403 | 权限不足 |
|
||||
| `VALIDATION_ERROR` | 400 | 请求参数验证失败 |
|
||||
| `INTERNAL_ERROR` | 500 | 服务器内部错误 |
|
||||
|
||||
## 最佳实践
|
||||
|
||||
### 1. 删除前检查
|
||||
|
||||
在删除资源前,建议先检查资源的使用情况:
|
||||
|
||||
```bash
|
||||
# 检查服务器使用情况
|
||||
curl -X GET \
|
||||
'https://api.mcphub.io/api/servers/mcp-server-123/usage' \
|
||||
-H 'Authorization: Bearer YOUR_API_TOKEN'
|
||||
```
|
||||
|
||||
### 2. 备份重要数据
|
||||
|
||||
对于重要资源,建议在删除前进行备份:
|
||||
|
||||
```bash
|
||||
# 导出服务器配置
|
||||
curl -X GET \
|
||||
'https://api.mcphub.io/api/servers/mcp-server-123/export' \
|
||||
-H 'Authorization: Bearer YOUR_API_TOKEN' \
|
||||
> server-backup.json
|
||||
```
|
||||
|
||||
### 3. 使用事务删除
|
||||
|
||||
对于复杂的删除操作,使用事务确保数据一致性:
|
||||
|
||||
```json
|
||||
{
|
||||
"transaction": true,
|
||||
"operations": [
|
||||
{
|
||||
"type": "delete",
|
||||
"resource": "server",
|
||||
"id": "server-1"
|
||||
},
|
||||
{
|
||||
"type": "delete",
|
||||
"resource": "group",
|
||||
"id": "group-1"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## 恢复删除的资源
|
||||
|
||||
软删除的资源可以通过恢复 API 恢复:
|
||||
|
||||
```bash
|
||||
curl -X POST \
|
||||
'https://api.mcphub.io/api/servers/mcp-server-123/restore' \
|
||||
-H 'Authorization: Bearer YOUR_API_TOKEN'
|
||||
```
|
||||
607
docs/zh/api-reference/endpoint/get.mdx
Normal file
@@ -0,0 +1,607 @@
|
||||
---
|
||||
title: '获取资源'
|
||||
description: '查询和检索 MCP 服务器、用户和组信息'
|
||||
---
|
||||
|
||||
## 获取服务器列表
|
||||
|
||||
### 端点
|
||||
|
||||
```http
|
||||
GET /api/servers
|
||||
```
|
||||
|
||||
### 查询参数
|
||||
|
||||
| 参数 | 类型 | 描述 | 示例 |
|
||||
| ---------------- | ------- | ------------------------------- | ---------------------------- |
|
||||
| `page` | integer | 页码(从 1 开始) | `?page=2` |
|
||||
| `limit` | integer | 每页记录数(默认 20,最大 100) | `?limit=50` |
|
||||
| `sort` | string | 排序字段 | `?sort=name` |
|
||||
| `order` | string | 排序顺序(asc/desc) | `?order=desc` |
|
||||
| `status` | string | 过滤服务器状态 | `?status=running` |
|
||||
| `search` | string | 搜索服务器名称或描述 | `?search=python` |
|
||||
| `group` | string | 过滤所属组 | `?group=dev-team` |
|
||||
| `tags` | string | 过滤标签(逗号分隔) | `?tags=python,production` |
|
||||
| `enabled` | boolean | 过滤启用状态 | `?enabled=true` |
|
||||
| `created_after` | string | 创建时间起始 | `?created_after=2024-01-01` |
|
||||
| `created_before` | string | 创建时间结束 | `?created_before=2024-01-31` |
|
||||
|
||||
### 响应
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"items": [
|
||||
{
|
||||
"id": "server-abc123",
|
||||
"name": "文件系统服务器",
|
||||
"status": "running",
|
||||
"command": "npx",
|
||||
"args": ["-y", "@modelcontextprotocol/server-filesystem", "/data"],
|
||||
"env": {
|
||||
"NODE_ENV": "production"
|
||||
},
|
||||
"cwd": "/app",
|
||||
"pid": 12345,
|
||||
"uptime": 3600000,
|
||||
"enabled": true,
|
||||
"description": "提供文件系统访问的 MCP 服务器",
|
||||
"tags": ["filesystem", "production"],
|
||||
"health": {
|
||||
"status": "healthy",
|
||||
"lastCheck": "2024-01-01T12:00:00Z",
|
||||
"responseTime": "45ms"
|
||||
},
|
||||
"resources": {
|
||||
"memory": {
|
||||
"used": "128MB",
|
||||
"limit": "512MB",
|
||||
"percentage": 25
|
||||
},
|
||||
"cpu": {
|
||||
"used": "15%",
|
||||
"limit": "50%"
|
||||
}
|
||||
},
|
||||
"stats": {
|
||||
"totalRequests": 1523,
|
||||
"errorCount": 2,
|
||||
"avgResponseTime": "234ms"
|
||||
},
|
||||
"lastRestart": "2024-01-01T08:00:00Z",
|
||||
"createdAt": "2024-01-01T00:00:00Z",
|
||||
"updatedAt": "2024-01-01T12:00:00Z"
|
||||
}
|
||||
],
|
||||
"pagination": {
|
||||
"page": 1,
|
||||
"limit": 20,
|
||||
"total": 45,
|
||||
"pages": 3,
|
||||
"hasNext": true,
|
||||
"hasPrev": false
|
||||
},
|
||||
"filters": {
|
||||
"status": "running",
|
||||
"totalFiltered": 12
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 示例
|
||||
|
||||
```bash
|
||||
# 获取运行中的服务器,按名称排序
|
||||
curl -X GET "http://localhost:3000/api/servers?status=running&sort=name&order=asc" \
|
||||
-H "Authorization: Bearer $TOKEN"
|
||||
|
||||
# 搜索包含 "python" 的服务器
|
||||
curl -X GET "http://localhost:3000/api/servers?search=python&limit=10" \
|
||||
-H "Authorization: Bearer $TOKEN"
|
||||
|
||||
# 获取开发团队的服务器
|
||||
curl -X GET "http://localhost:3000/api/servers?group=dev-team" \
|
||||
-H "Authorization: Bearer $TOKEN"
|
||||
```
|
||||
|
||||
## 获取服务器详情
|
||||
|
||||
### 端点
|
||||
|
||||
```http
|
||||
GET /api/servers/{serverId}
|
||||
```
|
||||
|
||||
### 路径参数
|
||||
|
||||
- `serverId` (string): 服务器唯一标识符
|
||||
|
||||
### 查询参数
|
||||
|
||||
| 参数 | 类型 | 描述 |
|
||||
| --------------- | ------ | ----------------------------------------------- |
|
||||
| `include` | string | 包含额外信息(逗号分隔):`logs,metrics,events` |
|
||||
| `metrics_range` | string | 指标时间范围:`1h`, `24h`, `7d` |
|
||||
|
||||
### 响应
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"id": "server-abc123",
|
||||
"name": "文件系统服务器",
|
||||
"status": "running",
|
||||
"command": "npx",
|
||||
"args": ["-y", "@modelcontextprotocol/server-filesystem", "/data"],
|
||||
"env": {
|
||||
"NODE_ENV": "production",
|
||||
"DEBUG": "mcp:*"
|
||||
},
|
||||
"cwd": "/app",
|
||||
"pid": 12345,
|
||||
"uptime": 3600000,
|
||||
"enabled": true,
|
||||
"description": "提供文件系统访问的 MCP 服务器",
|
||||
"tags": ["filesystem", "production"],
|
||||
"healthCheck": {
|
||||
"enabled": true,
|
||||
"interval": 30000,
|
||||
"timeout": 5000,
|
||||
"retries": 3,
|
||||
"endpoint": "/health",
|
||||
"status": "healthy",
|
||||
"lastCheck": "2024-01-01T12:00:00Z",
|
||||
"responseTime": "45ms",
|
||||
"consecutiveFailures": 0
|
||||
},
|
||||
"resources": {
|
||||
"memory": {
|
||||
"used": "128MB",
|
||||
"limit": "512MB",
|
||||
"warning": "400MB",
|
||||
"percentage": 25
|
||||
},
|
||||
"cpu": {
|
||||
"used": "15%",
|
||||
"limit": "50%",
|
||||
"cores": 4
|
||||
},
|
||||
"network": {
|
||||
"bytesIn": "1.2GB",
|
||||
"bytesOut": "890MB"
|
||||
}
|
||||
},
|
||||
"stats": {
|
||||
"totalRequests": 1523,
|
||||
"successfulRequests": 1521,
|
||||
"errorCount": 2,
|
||||
"avgResponseTime": "234ms",
|
||||
"p95ResponseTime": "450ms",
|
||||
"requestsPerMinute": 25,
|
||||
"lastError": {
|
||||
"timestamp": "2024-01-01T11:30:00Z",
|
||||
"message": "Temporary connection timeout",
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"capabilities": [
|
||||
{
|
||||
"type": "tool",
|
||||
"name": "read_file",
|
||||
"description": "读取文件内容",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"path": { "type": "string" }
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "tool",
|
||||
"name": "write_file",
|
||||
"description": "写入文件内容",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"path": { "type": "string" },
|
||||
"content": { "type": "string" }
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"groups": [
|
||||
{
|
||||
"id": "dev-team",
|
||||
"name": "开发团队",
|
||||
"permissions": ["read", "write", "execute"]
|
||||
}
|
||||
],
|
||||
"events": [
|
||||
{
|
||||
"id": "event-123",
|
||||
"type": "started",
|
||||
"timestamp": "2024-01-01T08:00:00Z",
|
||||
"message": "服务器启动成功",
|
||||
"metadata": {
|
||||
"pid": 12345,
|
||||
"startupTime": "2.3s"
|
||||
}
|
||||
}
|
||||
],
|
||||
"lastRestart": "2024-01-01T08:00:00Z",
|
||||
"createdAt": "2024-01-01T00:00:00Z",
|
||||
"updatedAt": "2024-01-01T12:00:00Z",
|
||||
"createdBy": "admin"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 示例
|
||||
|
||||
```bash
|
||||
# 获取服务器基本信息
|
||||
curl -X GET "http://localhost:3000/api/servers/server-abc123" \
|
||||
-H "Authorization: Bearer $TOKEN"
|
||||
|
||||
# 获取服务器详情包含日志和指标
|
||||
curl -X GET "http://localhost:3000/api/servers/server-abc123?include=logs,metrics&metrics_range=24h" \
|
||||
-H "Authorization: Bearer $TOKEN"
|
||||
```
|
||||
|
||||
## 获取服务器状态
|
||||
|
||||
### 端点
|
||||
|
||||
```http
|
||||
GET /api/servers/{serverId}/status
|
||||
```
|
||||
|
||||
### 响应
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"serverId": "server-abc123",
|
||||
"status": "running",
|
||||
"health": "healthy",
|
||||
"pid": 12345,
|
||||
"uptime": 3600000,
|
||||
"startedAt": "2024-01-01T08:00:00Z",
|
||||
"lastHealthCheck": "2024-01-01T12:00:00Z",
|
||||
"resources": {
|
||||
"memory": {
|
||||
"rss": 134217728,
|
||||
"heapTotal": 67108864,
|
||||
"heapUsed": 45088768,
|
||||
"external": 8388608
|
||||
},
|
||||
"cpu": {
|
||||
"user": 1000000,
|
||||
"system": 500000,
|
||||
"percentage": 15.5
|
||||
}
|
||||
},
|
||||
"connections": {
|
||||
"active": 5,
|
||||
"total": 127
|
||||
},
|
||||
"performance": {
|
||||
"requestsPerSecond": 12.5,
|
||||
"avgResponseTime": "234ms",
|
||||
"errorRate": "0.1%"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 获取服务器日志
|
||||
|
||||
### 端点
|
||||
|
||||
```http
|
||||
GET /api/servers/{serverId}/logs
|
||||
```
|
||||
|
||||
### 查询参数
|
||||
|
||||
| 参数 | 类型 | 描述 |
|
||||
| -------- | ------- | ---------------------------------------------- |
|
||||
| `level` | string | 日志级别过滤:`error`, `warn`, `info`, `debug` |
|
||||
| `limit` | integer | 返回日志条数(默认 100,最大 1000) |
|
||||
| `since` | string | 开始时间(ISO 8601 格式) |
|
||||
| `until` | string | 结束时间(ISO 8601 格式) |
|
||||
| `follow` | boolean | 实时跟踪日志流 |
|
||||
| `search` | string | 搜索日志内容 |
|
||||
|
||||
### 响应
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"logs": [
|
||||
{
|
||||
"id": "log-123",
|
||||
"timestamp": "2024-01-01T12:00:00Z",
|
||||
"level": "info",
|
||||
"message": "处理请求: read_file",
|
||||
"source": "mcp-server",
|
||||
"metadata": {
|
||||
"requestId": "req-456",
|
||||
"userId": "user-789",
|
||||
"duration": "45ms"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "log-124",
|
||||
"timestamp": "2024-01-01T12:00:05Z",
|
||||
"level": "error",
|
||||
"message": "文件不存在: /nonexistent/file.txt",
|
||||
"source": "filesystem",
|
||||
"metadata": {
|
||||
"requestId": "req-457",
|
||||
"path": "/nonexistent/file.txt",
|
||||
"error": "ENOENT"
|
||||
}
|
||||
}
|
||||
],
|
||||
"pagination": {
|
||||
"limit": 100,
|
||||
"total": 1523,
|
||||
"hasMore": true,
|
||||
"nextCursor": "cursor-abc123"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 实时日志流
|
||||
|
||||
```bash
|
||||
# 实时跟踪日志
|
||||
curl -X GET "http://localhost:3000/api/servers/server-abc123/logs?follow=true" \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-H "Accept: text/event-stream"
|
||||
```
|
||||
|
||||
## 获取服务器指标
|
||||
|
||||
### 端点
|
||||
|
||||
```http
|
||||
GET /api/servers/{serverId}/metrics
|
||||
```
|
||||
|
||||
### 查询参数
|
||||
|
||||
| 参数 | 类型 | 描述 |
|
||||
| ------------- | ------ | ------------------------------------------- |
|
||||
| `timeRange` | string | 时间范围:`1h`, `24h`, `7d`, `30d` |
|
||||
| `granularity` | string | 数据粒度:`1m`, `5m`, `1h`, `1d` |
|
||||
| `metrics` | string | 指定指标(逗号分隔):`cpu,memory,requests` |
|
||||
|
||||
### 响应
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"timeRange": "1h",
|
||||
"granularity": "5m",
|
||||
"metrics": {
|
||||
"cpu": {
|
||||
"data": [
|
||||
{ "timestamp": "2024-01-01T11:00:00Z", "value": 12.5 },
|
||||
{ "timestamp": "2024-01-01T11:05:00Z", "value": 15.2 }
|
||||
],
|
||||
"summary": {
|
||||
"avg": 13.8,
|
||||
"min": 8.1,
|
||||
"max": 18.5,
|
||||
"current": 15.2
|
||||
}
|
||||
},
|
||||
"memory": {
|
||||
"data": [
|
||||
{ "timestamp": "2024-01-01T11:00:00Z", "value": 125 },
|
||||
{ "timestamp": "2024-01-01T11:05:00Z", "value": 128 }
|
||||
],
|
||||
"summary": {
|
||||
"avg": 126.5,
|
||||
"min": 120,
|
||||
"max": 135,
|
||||
"current": 128
|
||||
}
|
||||
},
|
||||
"requests": {
|
||||
"data": [
|
||||
{ "timestamp": "2024-01-01T11:00:00Z", "value": 45 },
|
||||
{ "timestamp": "2024-01-01T11:05:00Z", "value": 52 }
|
||||
],
|
||||
"summary": {
|
||||
"total": 2847,
|
||||
"avg": 48.5,
|
||||
"peak": 67
|
||||
}
|
||||
},
|
||||
"responseTime": {
|
||||
"data": [
|
||||
{ "timestamp": "2024-01-01T11:00:00Z", "avg": 230, "p95": 450 },
|
||||
{ "timestamp": "2024-01-01T11:05:00Z", "avg": 245, "p95": 480 }
|
||||
],
|
||||
"summary": {
|
||||
"avgResponseTime": "237ms",
|
||||
"p95ResponseTime": "465ms"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 获取用户列表
|
||||
|
||||
### 端点
|
||||
|
||||
```http
|
||||
GET /api/users
|
||||
```
|
||||
|
||||
### 查询参数
|
||||
|
||||
| 参数 | 类型 | 描述 |
|
||||
| ------------------ | ------- | ---------------- |
|
||||
| `role` | string | 过滤用户角色 |
|
||||
| `group` | string | 过滤所属组 |
|
||||
| `enabled` | boolean | 过滤启用状态 |
|
||||
| `search` | string | 搜索用户名或邮箱 |
|
||||
| `last_login_after` | string | 最后登录时间起始 |
|
||||
|
||||
### 响应
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"items": [
|
||||
{
|
||||
"id": "user-abc123",
|
||||
"username": "zhangsan",
|
||||
"email": "zhangsan@example.com",
|
||||
"role": "user",
|
||||
"enabled": true,
|
||||
"profile": {
|
||||
"firstName": "张",
|
||||
"lastName": "三",
|
||||
"fullName": "张三",
|
||||
"department": "开发部",
|
||||
"title": "软件工程师"
|
||||
},
|
||||
"groups": [
|
||||
{
|
||||
"id": "dev-team",
|
||||
"name": "开发团队",
|
||||
"role": "member"
|
||||
}
|
||||
],
|
||||
"stats": {
|
||||
"totalSessions": 45,
|
||||
"totalRequests": 1234,
|
||||
"lastRequestAt": "2024-01-01T11:30:00Z"
|
||||
},
|
||||
"lastLoginAt": "2024-01-01T08:00:00Z",
|
||||
"createdAt": "2023-12-01T00:00:00Z"
|
||||
}
|
||||
],
|
||||
"pagination": {
|
||||
"page": 1,
|
||||
"limit": 20,
|
||||
"total": 89,
|
||||
"pages": 5
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 获取组列表
|
||||
|
||||
### 端点
|
||||
|
||||
```http
|
||||
GET /api/groups
|
||||
```
|
||||
|
||||
### 响应
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"items": [
|
||||
{
|
||||
"id": "group-abc123",
|
||||
"name": "dev-team",
|
||||
"displayName": "开发团队",
|
||||
"description": "前端和后端开发人员",
|
||||
"memberCount": 12,
|
||||
"serverCount": 8,
|
||||
"parentGroup": null,
|
||||
"children": [],
|
||||
"permissions": {
|
||||
"servers": ["read", "write", "execute"],
|
||||
"tools": ["read", "execute"]
|
||||
},
|
||||
"quotas": {
|
||||
"requests": {
|
||||
"used": 750,
|
||||
"limit": 1000
|
||||
}
|
||||
},
|
||||
"createdAt": "2023-12-01T00:00:00Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 搜索
|
||||
|
||||
### 全局搜索
|
||||
|
||||
```http
|
||||
GET /api/search
|
||||
```
|
||||
|
||||
### 查询参数
|
||||
|
||||
| 参数 | 类型 | 描述 |
|
||||
| ------- | ------- | ---------------------------------------------- |
|
||||
| `q` | string | 搜索关键词 |
|
||||
| `type` | string | 资源类型:`servers`, `users`, `groups`, `logs` |
|
||||
| `limit` | integer | 每种类型的最大结果数 |
|
||||
|
||||
### 响应
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"query": "python",
|
||||
"results": {
|
||||
"servers": [
|
||||
{
|
||||
"id": "server-1",
|
||||
"name": "Python MCP Server",
|
||||
"type": "server",
|
||||
"relevance": 0.95
|
||||
}
|
||||
],
|
||||
"users": [],
|
||||
"groups": [
|
||||
{
|
||||
"id": "python-devs",
|
||||
"name": "Python 开发者",
|
||||
"type": "group",
|
||||
"relevance": 0.8
|
||||
}
|
||||
],
|
||||
"logs": [
|
||||
{
|
||||
"id": "log-123",
|
||||
"message": "Starting Python server...",
|
||||
"type": "log",
|
||||
"relevance": 0.7
|
||||
}
|
||||
]
|
||||
},
|
||||
"total": 3
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
有关更多信息,请参阅 [创建资源](/zh/api-reference/endpoint/create)、[删除资源](/zh/api-reference/endpoint/delete) 和 [WebHooks](/zh/api-reference/endpoint/webhook) 文档。
|
||||
615
docs/zh/api-reference/endpoint/webhook.mdx
Normal file
@@ -0,0 +1,615 @@
|
||||
---
|
||||
title: WebHooks API
|
||||
description: 配置和管理 WebHook 事件通知的完整指南
|
||||
---
|
||||
|
||||
# WebHooks API
|
||||
|
||||
WebHooks 允许 MCPHub 在特定事件发生时向您的应用程序发送实时通知。
|
||||
|
||||
## 概述
|
||||
|
||||
MCPHub WebHooks 系统支持以下功能:
|
||||
|
||||
- 实时事件通知
|
||||
- 自定义过滤器
|
||||
- 重试机制
|
||||
- 签名验证
|
||||
- 批量事件处理
|
||||
|
||||
## 支持的事件类型
|
||||
|
||||
| 事件类型 | 描述 |
|
||||
| ----------------------- | -------------- |
|
||||
| `server.created` | MCP 服务器创建 |
|
||||
| `server.updated` | MCP 服务器更新 |
|
||||
| `server.deleted` | MCP 服务器删除 |
|
||||
| `server.status_changed` | 服务器状态变更 |
|
||||
| `group.created` | 服务器组创建 |
|
||||
| `group.updated` | 服务器组更新 |
|
||||
| `group.deleted` | 服务器组删除 |
|
||||
| `user.login` | 用户登录 |
|
||||
| `user.logout` | 用户登出 |
|
||||
| `config.changed` | 配置变更 |
|
||||
| `system.error` | 系统错误 |
|
||||
|
||||
## 创建 WebHook
|
||||
|
||||
### 端点
|
||||
|
||||
```http
|
||||
POST /api/webhooks
|
||||
```
|
||||
|
||||
### 请求体
|
||||
|
||||
```json
|
||||
{
|
||||
"url": "https://your-app.com/webhook",
|
||||
"events": ["server.created", "server.status_changed"],
|
||||
"secret": "your-webhook-secret",
|
||||
"active": true,
|
||||
"config": {
|
||||
"contentType": "application/json",
|
||||
"insecureSsl": false,
|
||||
"retryCount": 3,
|
||||
"timeout": 30
|
||||
},
|
||||
"filters": {
|
||||
"serverGroups": ["production", "staging"],
|
||||
"serverTypes": ["ai-assistant", "data-processor"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 响应
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"id": "webhook-123",
|
||||
"url": "https://your-app.com/webhook",
|
||||
"events": ["server.created", "server.status_changed"],
|
||||
"active": true,
|
||||
"secret": "your-webhook-secret",
|
||||
"config": {
|
||||
"contentType": "application/json",
|
||||
"insecureSsl": false,
|
||||
"retryCount": 3,
|
||||
"timeout": 30
|
||||
},
|
||||
"filters": {
|
||||
"serverGroups": ["production", "staging"],
|
||||
"serverTypes": ["ai-assistant", "data-processor"]
|
||||
},
|
||||
"createdAt": "2024-01-15T10:30:00Z",
|
||||
"updatedAt": "2024-01-15T10:30:00Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 获取 WebHook 列表
|
||||
|
||||
### 端点
|
||||
|
||||
```http
|
||||
GET /api/webhooks
|
||||
```
|
||||
|
||||
### 查询参数
|
||||
|
||||
| 参数名 | 类型 | 描述 |
|
||||
| ------ | ------- | -------------------- |
|
||||
| page | integer | 页码(默认:1) |
|
||||
| limit | integer | 每页数量(默认:20) |
|
||||
| active | boolean | 过滤活跃状态 |
|
||||
| event | string | 过滤事件类型 |
|
||||
|
||||
### 请求示例
|
||||
|
||||
```bash
|
||||
curl -X GET \
|
||||
'https://api.mcphub.io/api/webhooks?active=true&limit=10' \
|
||||
-H 'Authorization: Bearer YOUR_API_TOKEN'
|
||||
```
|
||||
|
||||
### 响应
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"webhooks": [
|
||||
{
|
||||
"id": "webhook-123",
|
||||
"url": "https://your-app.com/webhook",
|
||||
"events": ["server.created", "server.status_changed"],
|
||||
"active": true,
|
||||
"lastDelivery": "2024-01-15T09:30:00Z",
|
||||
"deliveryCount": 145,
|
||||
"failureCount": 2,
|
||||
"createdAt": "2024-01-10T10:30:00Z"
|
||||
}
|
||||
],
|
||||
"pagination": {
|
||||
"page": 1,
|
||||
"limit": 10,
|
||||
"total": 25,
|
||||
"pages": 3
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 获取单个 WebHook
|
||||
|
||||
### 端点
|
||||
|
||||
```http
|
||||
GET /api/webhooks/{id}
|
||||
```
|
||||
|
||||
### 响应
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"id": "webhook-123",
|
||||
"url": "https://your-app.com/webhook",
|
||||
"events": ["server.created", "server.status_changed"],
|
||||
"active": true,
|
||||
"secret": "your-webhook-secret",
|
||||
"config": {
|
||||
"contentType": "application/json",
|
||||
"insecureSsl": false,
|
||||
"retryCount": 3,
|
||||
"timeout": 30
|
||||
},
|
||||
"filters": {
|
||||
"serverGroups": ["production", "staging"],
|
||||
"serverTypes": ["ai-assistant", "data-processor"]
|
||||
},
|
||||
"stats": {
|
||||
"totalDeliveries": 145,
|
||||
"successfulDeliveries": 143,
|
||||
"failedDeliveries": 2,
|
||||
"lastDelivery": "2024-01-15T09:30:00Z",
|
||||
"lastSuccess": "2024-01-15T09:30:00Z",
|
||||
"lastFailure": "2024-01-14T15:20:00Z"
|
||||
},
|
||||
"createdAt": "2024-01-10T10:30:00Z",
|
||||
"updatedAt": "2024-01-15T10:30:00Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 更新 WebHook
|
||||
|
||||
### 端点
|
||||
|
||||
```http
|
||||
PUT /api/webhooks/{id}
|
||||
```
|
||||
|
||||
### 请求体
|
||||
|
||||
```json
|
||||
{
|
||||
"url": "https://your-app.com/new-webhook",
|
||||
"events": ["server.created", "server.updated", "server.deleted"],
|
||||
"active": true,
|
||||
"config": {
|
||||
"retryCount": 5,
|
||||
"timeout": 45
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 删除 WebHook
|
||||
|
||||
### 端点
|
||||
|
||||
```http
|
||||
DELETE /api/webhooks/{id}
|
||||
```
|
||||
|
||||
### 响应
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "WebHook 已成功删除"
|
||||
}
|
||||
```
|
||||
|
||||
## WebHook 事件格式
|
||||
|
||||
### 基本结构
|
||||
|
||||
所有 WebHook 事件都遵循以下基本结构:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "event-123",
|
||||
"type": "server.created",
|
||||
"timestamp": "2024-01-15T10:30:00Z",
|
||||
"version": "1.0",
|
||||
"data": {
|
||||
// 事件特定数据
|
||||
},
|
||||
"metadata": {
|
||||
"source": "mcphub",
|
||||
"environment": "production",
|
||||
"triggeredBy": "user-456"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 服务器事件示例
|
||||
|
||||
#### server.created
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "event-123",
|
||||
"type": "server.created",
|
||||
"timestamp": "2024-01-15T10:30:00Z",
|
||||
"version": "1.0",
|
||||
"data": {
|
||||
"server": {
|
||||
"id": "mcp-server-123",
|
||||
"name": "AI Assistant Server",
|
||||
"type": "ai-assistant",
|
||||
"endpoint": "https://ai-assistant.example.com",
|
||||
"group": "production",
|
||||
"status": "active",
|
||||
"capabilities": ["chat", "completion"],
|
||||
"createdAt": "2024-01-15T10:30:00Z"
|
||||
}
|
||||
},
|
||||
"metadata": {
|
||||
"source": "mcphub",
|
||||
"environment": "production",
|
||||
"triggeredBy": "user-456"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### server.status_changed
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "event-124",
|
||||
"type": "server.status_changed",
|
||||
"timestamp": "2024-01-15T11:30:00Z",
|
||||
"version": "1.0",
|
||||
"data": {
|
||||
"server": {
|
||||
"id": "mcp-server-123",
|
||||
"name": "AI Assistant Server",
|
||||
"previousStatus": "active",
|
||||
"currentStatus": "inactive",
|
||||
"reason": "Health check failed",
|
||||
"lastHealthCheck": "2024-01-15T11:25:00Z"
|
||||
}
|
||||
},
|
||||
"metadata": {
|
||||
"source": "mcphub",
|
||||
"environment": "production",
|
||||
"triggeredBy": "system"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 签名验证
|
||||
|
||||
MCPHub 使用 HMAC-SHA256 签名来验证 WebHook 的真实性。
|
||||
|
||||
### 签名生成
|
||||
|
||||
签名在 `X-MCPHub-Signature-256` 头中发送:
|
||||
|
||||
```
|
||||
X-MCPHub-Signature-256: sha256=5757107ea39eca8e35d1e8...
|
||||
```
|
||||
|
||||
### 验证示例
|
||||
|
||||
#### Node.js
|
||||
|
||||
```javascript
|
||||
const crypto = require('crypto');
|
||||
|
||||
function verifySignature(payload, signature, secret) {
|
||||
const expectedSignature = crypto
|
||||
.createHmac('sha256', secret)
|
||||
.update(payload, 'utf8')
|
||||
.digest('hex');
|
||||
|
||||
const actualSignature = signature.replace('sha256=', '');
|
||||
|
||||
return crypto.timingSafeEqual(
|
||||
Buffer.from(expectedSignature, 'hex'),
|
||||
Buffer.from(actualSignature, 'hex'),
|
||||
);
|
||||
}
|
||||
|
||||
// Express.js 中间件示例
|
||||
app.use('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
|
||||
const signature = req.headers['x-mcphub-signature-256'];
|
||||
const payload = req.body;
|
||||
|
||||
if (!verifySignature(payload, signature, process.env.WEBHOOK_SECRET)) {
|
||||
return res.status(401).send('Unauthorized');
|
||||
}
|
||||
|
||||
// 处理 WebHook 事件
|
||||
const event = JSON.parse(payload);
|
||||
console.log('收到事件:', event.type);
|
||||
|
||||
res.status(200).send('OK');
|
||||
});
|
||||
```
|
||||
|
||||
#### Python
|
||||
|
||||
```python
|
||||
import hmac
|
||||
import hashlib
|
||||
|
||||
def verify_signature(payload, signature, secret):
|
||||
expected_signature = hmac.new(
|
||||
secret.encode('utf-8'),
|
||||
payload,
|
||||
hashlib.sha256
|
||||
).hexdigest()
|
||||
|
||||
actual_signature = signature.replace('sha256=', '')
|
||||
|
||||
return hmac.compare_digest(expected_signature, actual_signature)
|
||||
|
||||
# Flask 示例
|
||||
from flask import Flask, request, jsonify
|
||||
import json
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
@app.route('/webhook', methods=['POST'])
|
||||
def webhook():
|
||||
signature = request.headers.get('X-MCPHub-Signature-256')
|
||||
payload = request.get_data()
|
||||
|
||||
if not verify_signature(payload, signature, 'your-webhook-secret'):
|
||||
return jsonify({'error': 'Unauthorized'}), 401
|
||||
|
||||
event = json.loads(payload)
|
||||
print(f'收到事件: {event["type"]}')
|
||||
|
||||
return jsonify({'status': 'success'}), 200
|
||||
```
|
||||
|
||||
## 重试机制
|
||||
|
||||
MCPHub 对失败的 WebHook 交付实施指数退避重试:
|
||||
|
||||
- **重试次数**: 可配置(默认 3 次)
|
||||
- **重试间隔**: 2^n 秒(n 为重试次数)
|
||||
- **最大间隔**: 300 秒(5 分钟)
|
||||
- **超时设置**: 可配置(默认 30 秒)
|
||||
|
||||
### 重试时间表
|
||||
|
||||
| 尝试次数 | 延迟时间 |
|
||||
| -------- | -------- |
|
||||
| 1 | 立即 |
|
||||
| 2 | 2 秒 |
|
||||
| 3 | 4 秒 |
|
||||
| 4 | 8 秒 |
|
||||
| 5 | 16 秒 |
|
||||
|
||||
## 获取交付历史
|
||||
|
||||
### 端点
|
||||
|
||||
```http
|
||||
GET /api/webhooks/{id}/deliveries
|
||||
```
|
||||
|
||||
### 查询参数
|
||||
|
||||
| 参数名 | 类型 | 描述 |
|
||||
| ---------- | ------- | ------------------------------------ |
|
||||
| page | integer | 页码 |
|
||||
| limit | integer | 每页数量 |
|
||||
| status | string | 过滤状态(success, failed, pending) |
|
||||
| event_type | string | 过滤事件类型 |
|
||||
|
||||
### 响应
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"deliveries": [
|
||||
{
|
||||
"id": "delivery-123",
|
||||
"eventId": "event-123",
|
||||
"eventType": "server.created",
|
||||
"url": "https://your-app.com/webhook",
|
||||
"status": "success",
|
||||
"responseCode": 200,
|
||||
"responseTime": 145,
|
||||
"attempts": 1,
|
||||
"deliveredAt": "2024-01-15T10:30:15Z",
|
||||
"nextRetry": null
|
||||
},
|
||||
{
|
||||
"id": "delivery-124",
|
||||
"eventId": "event-124",
|
||||
"eventType": "server.status_changed",
|
||||
"url": "https://your-app.com/webhook",
|
||||
"status": "failed",
|
||||
"responseCode": 500,
|
||||
"responseTime": 30000,
|
||||
"attempts": 3,
|
||||
"error": "Connection timeout",
|
||||
"deliveredAt": null,
|
||||
"nextRetry": "2024-01-15T11:45:00Z"
|
||||
}
|
||||
],
|
||||
"pagination": {
|
||||
"page": 1,
|
||||
"limit": 20,
|
||||
"total": 145,
|
||||
"pages": 8
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 测试 WebHook
|
||||
|
||||
### 端点
|
||||
|
||||
```http
|
||||
POST /api/webhooks/{id}/test
|
||||
```
|
||||
|
||||
### 请求体
|
||||
|
||||
```json
|
||||
{
|
||||
"eventType": "server.created",
|
||||
"customData": {
|
||||
"test": true,
|
||||
"message": "这是一个测试事件"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 响应
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"deliveryId": "delivery-test-123",
|
||||
"status": "delivered",
|
||||
"responseCode": 200,
|
||||
"responseTime": 124,
|
||||
"sentAt": "2024-01-15T10:30:00Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 最佳实践
|
||||
|
||||
### 1. 幂等性处理
|
||||
|
||||
确保您的 WebHook 端点能够处理重复事件:
|
||||
|
||||
```javascript
|
||||
const processedEvents = new Set();
|
||||
|
||||
app.post('/webhook', (req, res) => {
|
||||
const event = req.body;
|
||||
|
||||
// 检查事件是否已处理
|
||||
if (processedEvents.has(event.id)) {
|
||||
return res.status(200).send('Already processed');
|
||||
}
|
||||
|
||||
// 处理事件
|
||||
processEvent(event);
|
||||
|
||||
// 记录已处理的事件
|
||||
processedEvents.add(event.id);
|
||||
|
||||
res.status(200).send('OK');
|
||||
});
|
||||
```
|
||||
|
||||
### 2. 异步处理
|
||||
|
||||
对于复杂的处理逻辑,使用异步处理避免阻塞:
|
||||
|
||||
```javascript
|
||||
app.post('/webhook', async (req, res) => {
|
||||
const event = req.body;
|
||||
|
||||
// 立即响应
|
||||
res.status(200).send('OK');
|
||||
|
||||
// 异步处理事件
|
||||
setImmediate(() => {
|
||||
processEventAsync(event);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### 3. 错误处理
|
||||
|
||||
实施适当的错误处理和日志记录:
|
||||
|
||||
```javascript
|
||||
app.post('/webhook', (req, res) => {
|
||||
try {
|
||||
const event = req.body;
|
||||
processEvent(event);
|
||||
res.status(200).send('OK');
|
||||
} catch (error) {
|
||||
console.error('WebHook 处理错误:', error);
|
||||
res.status(500).send('Internal Server Error');
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### 4. 监控和告警
|
||||
|
||||
监控 WebHook 的交付状态:
|
||||
|
||||
```bash
|
||||
# 检查失败的交付
|
||||
curl -X GET \
|
||||
'https://api.mcphub.io/api/webhooks/webhook-123/deliveries?status=failed' \
|
||||
-H 'Authorization: Bearer YOUR_API_TOKEN'
|
||||
```
|
||||
|
||||
## 故障排除
|
||||
|
||||
### 常见问题
|
||||
|
||||
1. **签名验证失败**
|
||||
|
||||
- 检查密钥是否正确
|
||||
- 确保使用原始请求体进行验证
|
||||
- 验证 HMAC 计算实现
|
||||
|
||||
2. **超时错误**
|
||||
|
||||
- 增加 WebHook 超时设置
|
||||
- 优化端点响应时间
|
||||
- 使用异步处理
|
||||
|
||||
3. **重复事件**
|
||||
- 实施幂等性检查
|
||||
- 使用事件 ID 去重
|
||||
- 记录处理状态
|
||||
|
||||
### 调试工具
|
||||
|
||||
使用 MCPHub 提供的调试工具:
|
||||
|
||||
```bash
|
||||
# 查看最近的交付日志
|
||||
curl -X GET \
|
||||
'https://api.mcphub.io/api/webhooks/webhook-123/deliveries?limit=5' \
|
||||
-H 'Authorization: Bearer YOUR_API_TOKEN'
|
||||
|
||||
# 重新发送失败的事件
|
||||
curl -X POST \
|
||||
'https://api.mcphub.io/api/webhooks/delivery-124/redeliver' \
|
||||
-H 'Authorization: Bearer YOUR_API_TOKEN'
|
||||
```
|
||||
717
docs/zh/api-reference/introduction.mdx
Normal file
@@ -0,0 +1,717 @@
|
||||
---
|
||||
title: 'API 参考'
|
||||
description: 'MCPHub REST API 完整参考文档'
|
||||
---
|
||||
|
||||
## 概述
|
||||
|
||||
MCPHub 提供全面的 REST API,用于管理 MCP 服务器、用户、组和监控。所有 API 端点都需要身份验证,并支持 JSON 格式的请求和响应。
|
||||
|
||||
## 基础信息
|
||||
|
||||
### 基础 URL
|
||||
|
||||
```
|
||||
https://your-mcphub-instance.com/api
|
||||
```
|
||||
|
||||
### 身份验证
|
||||
|
||||
所有 API 请求都需要身份验证。支持以下方法:
|
||||
|
||||
#### JWT 令牌认证
|
||||
|
||||
```bash
|
||||
curl -X GET https://api.mcphub.com/servers \
|
||||
-H "Authorization: Bearer YOUR_JWT_TOKEN"
|
||||
```
|
||||
|
||||
#### API 密钥认证
|
||||
|
||||
```bash
|
||||
curl -X GET https://api.mcphub.com/servers \
|
||||
-H "X-API-Key: YOUR_API_KEY"
|
||||
```
|
||||
|
||||
### 请求格式
|
||||
|
||||
- **Content-Type**: `application/json`
|
||||
- **Accept**: `application/json`
|
||||
- **User-Agent**: 建议包含您的应用程序名称和版本
|
||||
|
||||
### 响应格式
|
||||
|
||||
所有响应都采用 JSON 格式:
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
// 响应数据
|
||||
},
|
||||
"message": "操作成功",
|
||||
"timestamp": "2024-01-01T12:00:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
错误响应格式:
|
||||
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"error": {
|
||||
"code": "VALIDATION_ERROR",
|
||||
"message": "请求数据无效",
|
||||
"details": {
|
||||
"field": "name",
|
||||
"reason": "名称不能为空"
|
||||
}
|
||||
},
|
||||
"timestamp": "2024-01-01T12:00:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
## 状态码
|
||||
|
||||
| 状态码 | 说明 |
|
||||
| ------ | -------------------- |
|
||||
| 200 | 请求成功 |
|
||||
| 201 | 资源创建成功 |
|
||||
| 204 | 请求成功,无返回内容 |
|
||||
| 400 | 请求参数错误 |
|
||||
| 401 | 未授权访问 |
|
||||
| 403 | 权限不足 |
|
||||
| 404 | 资源不存在 |
|
||||
| 409 | 资源冲突 |
|
||||
| 422 | 请求数据验证失败 |
|
||||
| 429 | 请求频率超限 |
|
||||
| 500 | 服务器内部错误 |
|
||||
|
||||
## 分页
|
||||
|
||||
支持分页的端点使用以下参数:
|
||||
|
||||
- `page`: 页码(从 1 开始)
|
||||
- `limit`: 每页记录数(默认 20,最大 100)
|
||||
- `sort`: 排序字段
|
||||
- `order`: 排序顺序(`asc` 或 `desc`)
|
||||
|
||||
```bash
|
||||
curl -X GET "https://api.mcphub.com/servers?page=2&limit=50&sort=name&order=asc" \
|
||||
-H "Authorization: Bearer $TOKEN"
|
||||
```
|
||||
|
||||
分页响应格式:
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"items": [...],
|
||||
"pagination": {
|
||||
"page": 2,
|
||||
"limit": 50,
|
||||
"total": 234,
|
||||
"pages": 5,
|
||||
"hasNext": true,
|
||||
"hasPrev": true
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 过滤和搜索
|
||||
|
||||
支持过滤的端点可以使用以下参数:
|
||||
|
||||
- `search`: 全文搜索
|
||||
- `filter[field]`: 字段过滤
|
||||
- `status`: 状态过滤
|
||||
- `created_after`: 创建时间筛选
|
||||
- `created_before`: 创建时间筛选
|
||||
|
||||
```bash
|
||||
curl -X GET "https://api.mcphub.com/servers?search=python&filter[status]=running&created_after=2024-01-01" \
|
||||
-H "Authorization: Bearer $TOKEN"
|
||||
```
|
||||
|
||||
## API 端点
|
||||
|
||||
### 服务器管理
|
||||
|
||||
#### 获取服务器列表
|
||||
|
||||
```http
|
||||
GET /api/servers
|
||||
```
|
||||
|
||||
参数:
|
||||
|
||||
- `status` (可选): 过滤服务器状态 (`running`, `stopped`, `error`)
|
||||
- `group` (可选): 过滤所属组
|
||||
- `search` (可选): 搜索服务器名称或描述
|
||||
|
||||
示例响应:
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"items": [
|
||||
{
|
||||
"id": "server-1",
|
||||
"name": "文件系统服务器",
|
||||
"status": "running",
|
||||
"command": "npx",
|
||||
"args": ["-y", "@modelcontextprotocol/server-filesystem", "/data"],
|
||||
"env": {
|
||||
"NODE_ENV": "production"
|
||||
},
|
||||
"cwd": "/app",
|
||||
"pid": 12345,
|
||||
"uptime": 3600000,
|
||||
"lastRestart": "2024-01-01T12:00:00Z",
|
||||
"createdAt": "2024-01-01T10:00:00Z",
|
||||
"updatedAt": "2024-01-01T12:00:00Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 创建服务器
|
||||
|
||||
```http
|
||||
POST /api/servers
|
||||
```
|
||||
|
||||
请求体:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "新服务器",
|
||||
"command": "python",
|
||||
"args": ["-m", "mcp_server"],
|
||||
"env": {
|
||||
"API_KEY": "your-api-key",
|
||||
"LOG_LEVEL": "INFO"
|
||||
},
|
||||
"cwd": "/app/python-server",
|
||||
"enabled": true,
|
||||
"description": "Python MCP 服务器",
|
||||
"tags": ["python", "production"]
|
||||
}
|
||||
```
|
||||
|
||||
#### 获取服务器详情
|
||||
|
||||
```http
|
||||
GET /api/servers/{serverId}
|
||||
```
|
||||
|
||||
#### 更新服务器
|
||||
|
||||
```http
|
||||
PUT /api/servers/{serverId}
|
||||
```
|
||||
|
||||
#### 删除服务器
|
||||
|
||||
```http
|
||||
DELETE /api/servers/{serverId}
|
||||
```
|
||||
|
||||
#### 启动服务器
|
||||
|
||||
```http
|
||||
POST /api/servers/{serverId}/start
|
||||
```
|
||||
|
||||
#### 停止服务器
|
||||
|
||||
```http
|
||||
POST /api/servers/{serverId}/stop
|
||||
```
|
||||
|
||||
请求体(可选):
|
||||
|
||||
```json
|
||||
{
|
||||
"graceful": true,
|
||||
"timeout": 30000
|
||||
}
|
||||
```
|
||||
|
||||
#### 重启服务器
|
||||
|
||||
```http
|
||||
POST /api/servers/{serverId}/restart
|
||||
```
|
||||
|
||||
#### 获取服务器日志
|
||||
|
||||
```http
|
||||
GET /api/servers/{serverId}/logs
|
||||
```
|
||||
|
||||
参数:
|
||||
|
||||
- `level` (可选): 日志级别过滤
|
||||
- `limit` (可选): 返回日志条数
|
||||
- `since` (可选): 开始时间
|
||||
- `follow` (可选): 实时跟踪日志
|
||||
|
||||
### 用户管理
|
||||
|
||||
#### 获取用户列表
|
||||
|
||||
```http
|
||||
GET /api/users
|
||||
```
|
||||
|
||||
#### 创建用户
|
||||
|
||||
```http
|
||||
POST /api/users
|
||||
```
|
||||
|
||||
请求体:
|
||||
|
||||
```json
|
||||
{
|
||||
"username": "newuser",
|
||||
"email": "user@example.com",
|
||||
"password": "securepassword",
|
||||
"role": "user",
|
||||
"groups": ["dev-team"],
|
||||
"profile": {
|
||||
"firstName": "张",
|
||||
"lastName": "三",
|
||||
"department": "开发部"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 获取用户详情
|
||||
|
||||
```http
|
||||
GET /api/users/{userId}
|
||||
```
|
||||
|
||||
#### 更新用户
|
||||
|
||||
```http
|
||||
PUT /api/users/{userId}
|
||||
```
|
||||
|
||||
#### 删除用户
|
||||
|
||||
```http
|
||||
DELETE /api/users/{userId}
|
||||
```
|
||||
|
||||
### 组管理
|
||||
|
||||
#### 获取组列表
|
||||
|
||||
```http
|
||||
GET /api/groups
|
||||
```
|
||||
|
||||
#### 创建组
|
||||
|
||||
```http
|
||||
POST /api/groups
|
||||
```
|
||||
|
||||
请求体:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "dev-team",
|
||||
"displayName": "开发团队",
|
||||
"description": "前端和后端开发人员",
|
||||
"parentGroup": null,
|
||||
"permissions": {
|
||||
"servers": ["read", "write", "execute"],
|
||||
"tools": ["read", "execute"]
|
||||
},
|
||||
"settings": {
|
||||
"autoAssign": false,
|
||||
"maxMembers": 50,
|
||||
"requireApproval": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 添加用户到组
|
||||
|
||||
```http
|
||||
POST /api/groups/{groupId}/members
|
||||
```
|
||||
|
||||
请求体:
|
||||
|
||||
```json
|
||||
{
|
||||
"userId": "user123",
|
||||
"role": "member"
|
||||
}
|
||||
```
|
||||
|
||||
#### 从组中移除用户
|
||||
|
||||
```http
|
||||
DELETE /api/groups/{groupId}/members/{userId}
|
||||
```
|
||||
|
||||
#### 分配服务器到组
|
||||
|
||||
```http
|
||||
POST /api/groups/{groupId}/servers
|
||||
```
|
||||
|
||||
请求体:
|
||||
|
||||
```json
|
||||
{
|
||||
"serverId": "server-1",
|
||||
"permissions": ["read", "write", "execute"]
|
||||
}
|
||||
```
|
||||
|
||||
### 身份验证
|
||||
|
||||
#### 登录
|
||||
|
||||
```http
|
||||
POST /api/auth/login
|
||||
```
|
||||
|
||||
请求体:
|
||||
|
||||
```json
|
||||
{
|
||||
"username": "admin",
|
||||
"password": "password",
|
||||
"mfaCode": "123456"
|
||||
}
|
||||
```
|
||||
|
||||
响应:
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
|
||||
"refreshToken": "refresh_token_here",
|
||||
"expiresIn": 86400,
|
||||
"user": {
|
||||
"id": "user123",
|
||||
"username": "admin",
|
||||
"role": "admin",
|
||||
"permissions": ["*"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 刷新令牌
|
||||
|
||||
```http
|
||||
POST /api/auth/refresh
|
||||
```
|
||||
|
||||
#### 注销
|
||||
|
||||
```http
|
||||
POST /api/auth/logout
|
||||
```
|
||||
|
||||
#### 验证令牌
|
||||
|
||||
```http
|
||||
GET /api/auth/verify
|
||||
```
|
||||
|
||||
### 监控
|
||||
|
||||
#### 获取系统状态
|
||||
|
||||
```http
|
||||
GET /api/monitoring/status
|
||||
```
|
||||
|
||||
响应:
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"system": {
|
||||
"uptime": 86400,
|
||||
"version": "2.1.0",
|
||||
"nodeVersion": "18.17.0"
|
||||
},
|
||||
"servers": {
|
||||
"total": 12,
|
||||
"running": 10,
|
||||
"stopped": 1,
|
||||
"error": 1
|
||||
},
|
||||
"performance": {
|
||||
"requestsPerMinute": 85,
|
||||
"avgResponseTime": "245ms",
|
||||
"errorRate": "0.3%"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 获取性能指标
|
||||
|
||||
```http
|
||||
GET /api/monitoring/metrics
|
||||
```
|
||||
|
||||
参数:
|
||||
|
||||
- `timeRange`: 时间范围 (`1h`, `24h`, `7d`, `30d`)
|
||||
- `granularity`: 数据粒度 (`1m`, `5m`, `1h`, `1d`)
|
||||
- `metrics`: 指定指标名称(逗号分隔)
|
||||
|
||||
#### 获取日志
|
||||
|
||||
```http
|
||||
GET /api/monitoring/logs
|
||||
```
|
||||
|
||||
参数:
|
||||
|
||||
- `level`: 日志级别
|
||||
- `source`: 日志源
|
||||
- `limit`: 返回条数
|
||||
- `since`: 开始时间
|
||||
- `until`: 结束时间
|
||||
|
||||
### 配置管理
|
||||
|
||||
#### 获取系统配置
|
||||
|
||||
```http
|
||||
GET /api/config
|
||||
```
|
||||
|
||||
#### 更新系统配置
|
||||
|
||||
```http
|
||||
PUT /api/config
|
||||
```
|
||||
|
||||
请求体:
|
||||
|
||||
```json
|
||||
{
|
||||
"smtp": {
|
||||
"host": "smtp.example.com",
|
||||
"port": 587,
|
||||
"secure": false,
|
||||
"auth": {
|
||||
"user": "noreply@example.com",
|
||||
"pass": "password"
|
||||
}
|
||||
},
|
||||
"notifications": {
|
||||
"email": true,
|
||||
"slack": true,
|
||||
"webhook": "https://hooks.example.com/notifications"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## WebSocket API
|
||||
|
||||
MCPHub 支持 WebSocket 连接以获取实时更新。
|
||||
|
||||
### 连接
|
||||
|
||||
```javascript
|
||||
const ws = new WebSocket('wss://api.mcphub.com/ws');
|
||||
ws.onopen = function () {
|
||||
// 发送认证消息
|
||||
ws.send(
|
||||
JSON.stringify({
|
||||
type: 'auth',
|
||||
token: 'YOUR_JWT_TOKEN',
|
||||
}),
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
### 订阅事件
|
||||
|
||||
```javascript
|
||||
// 订阅服务器状态更新
|
||||
ws.send(
|
||||
JSON.stringify({
|
||||
type: 'subscribe',
|
||||
channel: 'server-status',
|
||||
filters: {
|
||||
serverId: 'server-1',
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
// 订阅系统监控
|
||||
ws.send(
|
||||
JSON.stringify({
|
||||
type: 'subscribe',
|
||||
channel: 'monitoring',
|
||||
metrics: ['cpu', 'memory', 'requests'],
|
||||
}),
|
||||
);
|
||||
```
|
||||
|
||||
### 事件类型
|
||||
|
||||
- `server-status`: 服务器状态变化
|
||||
- `server-logs`: 实时日志流
|
||||
- `monitoring`: 系统监控指标
|
||||
- `alerts`: 系统警报
|
||||
- `user-activity`: 用户活动事件
|
||||
|
||||
## 错误处理
|
||||
|
||||
### 错误代码
|
||||
|
||||
| 错误代码 | 描述 |
|
||||
| ----------------------- | -------------- |
|
||||
| `INVALID_REQUEST` | 请求格式无效 |
|
||||
| `AUTHENTICATION_FAILED` | 身份验证失败 |
|
||||
| `AUTHORIZATION_FAILED` | 权限不足 |
|
||||
| `RESOURCE_NOT_FOUND` | 资源不存在 |
|
||||
| `RESOURCE_CONFLICT` | 资源冲突 |
|
||||
| `VALIDATION_ERROR` | 数据验证失败 |
|
||||
| `RATE_LIMIT_EXCEEDED` | 请求频率超限 |
|
||||
| `SERVER_ERROR` | 服务器内部错误 |
|
||||
|
||||
### 错误处理示例
|
||||
|
||||
```javascript
|
||||
async function handleApiRequest() {
|
||||
try {
|
||||
const response = await fetch('/api/servers', {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (!data.success) {
|
||||
switch (data.error.code) {
|
||||
case 'AUTHENTICATION_FAILED':
|
||||
// 重新登录
|
||||
redirectToLogin();
|
||||
break;
|
||||
case 'RATE_LIMIT_EXCEEDED':
|
||||
// 延迟重试
|
||||
setTimeout(() => handleApiRequest(), 5000);
|
||||
break;
|
||||
default:
|
||||
// 显示错误消息
|
||||
showError(data.error.message);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 处理成功响应
|
||||
handleSuccessResponse(data.data);
|
||||
} catch (error) {
|
||||
// 处理网络错误
|
||||
console.error('网络请求失败:', error);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 速率限制
|
||||
|
||||
API 实施速率限制以防止滥用:
|
||||
|
||||
- **默认限制**: 每分钟 100 请求
|
||||
- **认证用户**: 每分钟 1000 请求
|
||||
- **管理员**: 每分钟 5000 请求
|
||||
|
||||
响应头包含速率限制信息:
|
||||
|
||||
```
|
||||
X-RateLimit-Limit: 1000
|
||||
X-RateLimit-Remaining: 999
|
||||
X-RateLimit-Reset: 1609459200
|
||||
```
|
||||
|
||||
## SDK 和客户端库
|
||||
|
||||
### JavaScript/Node.js
|
||||
|
||||
```bash
|
||||
npm install @mcphub/sdk
|
||||
```
|
||||
|
||||
```javascript
|
||||
import { MCPHubClient } from '@mcphub/sdk';
|
||||
|
||||
const client = new MCPHubClient({
|
||||
baseURL: 'https://api.mcphub.com',
|
||||
token: 'YOUR_JWT_TOKEN',
|
||||
});
|
||||
|
||||
// 获取服务器列表
|
||||
const servers = await client.servers.list();
|
||||
|
||||
// 创建服务器
|
||||
const newServer = await client.servers.create({
|
||||
name: '新服务器',
|
||||
command: 'python',
|
||||
args: ['-m', 'mcp_server'],
|
||||
});
|
||||
```
|
||||
|
||||
### Python
|
||||
|
||||
```bash
|
||||
pip install mcphub-sdk
|
||||
```
|
||||
|
||||
```python
|
||||
from mcphub_sdk import MCPHubClient
|
||||
|
||||
client = MCPHubClient(
|
||||
base_url='https://api.mcphub.com',
|
||||
token='YOUR_JWT_TOKEN'
|
||||
)
|
||||
|
||||
# 获取服务器列表
|
||||
servers = client.servers.list()
|
||||
|
||||
# 创建服务器
|
||||
new_server = client.servers.create(
|
||||
name='新服务器',
|
||||
command='python',
|
||||
args=['-m', 'mcp_server']
|
||||
)
|
||||
```
|
||||
|
||||
## 最佳实践
|
||||
|
||||
1. **使用 HTTPS**: 始终通过 HTTPS 访问 API
|
||||
2. **安全存储令牌**: 不要在客户端代码中硬编码令牌
|
||||
3. **处理错误**: 实施适当的错误处理和重试逻辑
|
||||
4. **遵守速率限制**: 监控速率限制并实施退避策略
|
||||
5. **使用分页**: 对于大数据集使用分页参数
|
||||
6. **缓存响应**: 适当缓存 API 响应以减少请求
|
||||
7. **版本控制**: 使用 API 版本号以确保兼容性
|
||||
|
||||
有关更多信息,请参阅我们的 [SDK 文档](https://docs.mcphub.com/sdk) 和 [示例代码](https://github.com/mcphub/examples)。
|
||||
539
docs/zh/configuration/docker-setup.mdx
Normal file
@@ -0,0 +1,539 @@
|
||||
---
|
||||
title: 'Docker 部署'
|
||||
description: '使用 Docker 和 Docker Compose 部署 MCPHub'
|
||||
---
|
||||
|
||||
# Docker 部署
|
||||
|
||||
本指南介绍使用 Docker 部署 MCPHub,包括开发和生产配置。
|
||||
|
||||
## Docker 快速开始
|
||||
|
||||
### 使用预构建镜像
|
||||
|
||||
```bash
|
||||
# 拉取最新镜像
|
||||
docker pull mcphub/mcphub:latest
|
||||
|
||||
# 使用默认配置运行
|
||||
docker run -d \
|
||||
--name mcphub \
|
||||
-p 3000:3000 \
|
||||
-v $(pwd)/mcp_settings.json:/app/mcp_settings.json \
|
||||
mcphub/mcphub:latest
|
||||
```
|
||||
|
||||
### 从源码构建
|
||||
|
||||
```bash
|
||||
# 克隆仓库
|
||||
git clone https://github.com/your-username/mcphub.git
|
||||
cd mcphub
|
||||
|
||||
# 构建 Docker 镜像
|
||||
docker build -t mcphub:local .
|
||||
|
||||
# 运行容器
|
||||
docker run -d \
|
||||
--name mcphub \
|
||||
-p 3000:3000 \
|
||||
-v $(pwd)/mcp_settings.json:/app/mcp_settings.json \
|
||||
mcphub:local
|
||||
```
|
||||
|
||||
## Docker Compose 设置
|
||||
|
||||
### 基本配置
|
||||
|
||||
创建 `docker-compose.yml` 文件:
|
||||
|
||||
```yaml
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
mcphub:
|
||||
image: mcphub/mcphub:latest
|
||||
# 本地开发时使用:
|
||||
# build: .
|
||||
container_name: mcphub
|
||||
ports:
|
||||
- '3000:3000'
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
- PORT=3000
|
||||
- JWT_SECRET=${JWT_SECRET:-your-jwt-secret}
|
||||
- DATABASE_URL=postgresql://mcphub:password@postgres:5432/mcphub
|
||||
volumes:
|
||||
- ./mcp_settings.json:/app/mcp_settings.json:ro
|
||||
- ./servers.json:/app/servers.json:ro
|
||||
- mcphub_data:/app/data
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- mcphub-network
|
||||
|
||||
postgres:
|
||||
image: postgres:15-alpine
|
||||
container_name: mcphub-postgres
|
||||
environment:
|
||||
- POSTGRES_DB=mcphub
|
||||
- POSTGRES_USER=mcphub
|
||||
- POSTGRES_PASSWORD=password
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
- ./scripts/init-db.sql:/docker-entrypoint-initdb.d/init-db.sql:ro
|
||||
ports:
|
||||
- '5432:5432'
|
||||
healthcheck:
|
||||
test: ['CMD-SHELL', 'pg_isready -U mcphub -d mcphub']
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- mcphub-network
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
mcphub_data:
|
||||
|
||||
networks:
|
||||
mcphub-network:
|
||||
driver: bridge
|
||||
```
|
||||
|
||||
### 生产配置(包含 Nginx)
|
||||
|
||||
```yaml
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
nginx:
|
||||
image: nginx:alpine
|
||||
container_name: mcphub-nginx
|
||||
ports:
|
||||
- '80:80'
|
||||
- '443:443'
|
||||
volumes:
|
||||
- ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
|
||||
- ./ssl:/etc/nginx/ssl:ro
|
||||
- nginx_logs:/var/log/nginx
|
||||
depends_on:
|
||||
- mcphub
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- mcphub-network
|
||||
|
||||
mcphub:
|
||||
image: mcphub/mcphub:latest
|
||||
container_name: mcphub-app
|
||||
expose:
|
||||
- '3000'
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
- PORT=3000
|
||||
- JWT_SECRET=${JWT_SECRET}
|
||||
- JWT_EXPIRES_IN=${JWT_EXPIRES_IN:-24h}
|
||||
- DATABASE_URL=postgresql://mcphub:${POSTGRES_PASSWORD}@postgres:5432/mcphub
|
||||
- OPENAI_API_KEY=${OPENAI_API_KEY}
|
||||
- REDIS_URL=redis://redis:6379
|
||||
volumes:
|
||||
- ./mcp_settings.json:/app/mcp_settings.json:ro
|
||||
- ./servers.json:/app/servers.json:ro
|
||||
- mcphub_data:/app/data
|
||||
- mcphub_logs:/app/logs
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
redis:
|
||||
condition: service_healthy
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- mcphub-network
|
||||
healthcheck:
|
||||
test: ['CMD', 'wget', '--quiet', '--tries=1', '--spider', 'http://localhost:3000/health']
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
|
||||
postgres:
|
||||
image: postgres:15-alpine
|
||||
container_name: mcphub-postgres
|
||||
environment:
|
||||
- POSTGRES_DB=mcphub
|
||||
- POSTGRES_USER=mcphub
|
||||
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
- ./backups:/backups
|
||||
healthcheck:
|
||||
test: ['CMD-SHELL', 'pg_isready -U mcphub -d mcphub']
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- mcphub-network
|
||||
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
container_name: mcphub-redis
|
||||
command: redis-server --appendonly yes --requirepass ${REDIS_PASSWORD}
|
||||
volumes:
|
||||
- redis_data:/data
|
||||
healthcheck:
|
||||
test: ['CMD', 'redis-cli', 'ping']
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- mcphub-network
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
redis_data:
|
||||
mcphub_data:
|
||||
mcphub_logs:
|
||||
nginx_logs:
|
||||
|
||||
networks:
|
||||
mcphub-network:
|
||||
driver: bridge
|
||||
```
|
||||
|
||||
### 环境变量
|
||||
|
||||
为 Docker Compose 创建 `.env` 文件:
|
||||
|
||||
```env
|
||||
# 应用程序
|
||||
NODE_ENV=production
|
||||
JWT_SECRET=your-super-secret-jwt-key-change-this
|
||||
JWT_EXPIRES_IN=24h
|
||||
|
||||
# 数据库
|
||||
POSTGRES_PASSWORD=your-secure-database-password
|
||||
|
||||
# Redis
|
||||
REDIS_PASSWORD=your-secure-redis-password
|
||||
|
||||
# 外部 API
|
||||
OPENAI_API_KEY=your-openai-api-key
|
||||
|
||||
# 可选:自定义端口
|
||||
# PORT=3000
|
||||
```
|
||||
|
||||
## 开发设置
|
||||
|
||||
### 开发 Docker Compose
|
||||
|
||||
创建 `docker-compose.dev.yml`:
|
||||
|
||||
```yaml
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
mcphub-dev:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile.dev
|
||||
container_name: mcphub-dev
|
||||
ports:
|
||||
- '3000:3000'
|
||||
- '5173:5173' # 前端开发服务器
|
||||
- '9229:9229' # 调试端口
|
||||
environment:
|
||||
- NODE_ENV=development
|
||||
- PORT=3000
|
||||
- DATABASE_URL=postgresql://mcphub:password@postgres:5432/mcphub
|
||||
volumes:
|
||||
- .:/app
|
||||
- /app/node_modules
|
||||
- /app/frontend/node_modules
|
||||
depends_on:
|
||||
- postgres
|
||||
command: pnpm dev
|
||||
networks:
|
||||
- mcphub-dev
|
||||
|
||||
postgres:
|
||||
image: postgres:15-alpine
|
||||
container_name: mcphub-postgres-dev
|
||||
environment:
|
||||
- POSTGRES_DB=mcphub
|
||||
- POSTGRES_USER=mcphub
|
||||
- POSTGRES_PASSWORD=password
|
||||
ports:
|
||||
- '5432:5432'
|
||||
volumes:
|
||||
- postgres_dev_data:/var/lib/postgresql/data
|
||||
networks:
|
||||
- mcphub-dev
|
||||
|
||||
volumes:
|
||||
postgres_dev_data:
|
||||
|
||||
networks:
|
||||
mcphub-dev:
|
||||
driver: bridge
|
||||
```
|
||||
|
||||
### 开发 Dockerfile
|
||||
|
||||
创建 `Dockerfile.dev`:
|
||||
|
||||
```dockerfile
|
||||
FROM node:20-alpine
|
||||
|
||||
# 安装 pnpm
|
||||
RUN npm install -g pnpm
|
||||
|
||||
# 设置工作目录
|
||||
WORKDIR /app
|
||||
|
||||
# 复制包文件
|
||||
COPY package.json pnpm-lock.yaml ./
|
||||
COPY frontend/package.json ./frontend/
|
||||
|
||||
# 安装依赖
|
||||
RUN pnpm install
|
||||
|
||||
# 复制源代码
|
||||
COPY . .
|
||||
|
||||
# 暴露端口
|
||||
EXPOSE 3000 5173 9229
|
||||
|
||||
# 启动开发服务器
|
||||
CMD ["pnpm", "dev"]
|
||||
```
|
||||
|
||||
## 运行应用程序
|
||||
|
||||
### 开发模式
|
||||
|
||||
```bash
|
||||
# 启动开发环境
|
||||
docker-compose -f docker-compose.dev.yml up -d
|
||||
|
||||
# 查看日志
|
||||
docker-compose -f docker-compose.dev.yml logs -f mcphub-dev
|
||||
|
||||
# 停止开发环境
|
||||
docker-compose -f docker-compose.dev.yml down
|
||||
```
|
||||
|
||||
### 生产模式
|
||||
|
||||
```bash
|
||||
# 启动生产环境
|
||||
docker-compose up -d
|
||||
|
||||
# 查看日志
|
||||
docker-compose logs -f mcphub
|
||||
|
||||
# 停止生产环境
|
||||
docker-compose down
|
||||
```
|
||||
|
||||
## 配置管理
|
||||
|
||||
### MCP 设置卷挂载
|
||||
|
||||
创建您的 `mcp_settings.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"fetch": {
|
||||
"command": "uvx",
|
||||
"args": ["mcp-server-fetch"]
|
||||
},
|
||||
"playwright": {
|
||||
"command": "npx",
|
||||
"args": ["@playwright/mcp@latest", "--headless"]
|
||||
},
|
||||
"amap": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "@amap/amap-maps-mcp-server"],
|
||||
"env": {
|
||||
"AMAP_MAPS_API_KEY": "your-api-key"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 密钥管理
|
||||
|
||||
对于生产环境,使用 Docker 密钥:
|
||||
|
||||
```yaml
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
mcphub:
|
||||
image: mcphub/mcphub:latest
|
||||
environment:
|
||||
- JWT_SECRET_FILE=/run/secrets/jwt_secret
|
||||
- DATABASE_PASSWORD_FILE=/run/secrets/db_password
|
||||
secrets:
|
||||
- jwt_secret
|
||||
- db_password
|
||||
|
||||
secrets:
|
||||
jwt_secret:
|
||||
file: ./secrets/jwt_secret.txt
|
||||
db_password:
|
||||
file: ./secrets/db_password.txt
|
||||
```
|
||||
|
||||
## 数据持久化
|
||||
|
||||
### 数据库备份
|
||||
|
||||
在 `docker-compose.yml` 中添加备份服务:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
backup:
|
||||
image: postgres:15-alpine
|
||||
container_name: mcphub-backup
|
||||
environment:
|
||||
- PGPASSWORD=${POSTGRES_PASSWORD}
|
||||
volumes:
|
||||
- ./backups:/backups
|
||||
- ./scripts/backup.sh:/backup.sh:ro
|
||||
command: /bin/sh -c "chmod +x /backup.sh && /backup.sh"
|
||||
depends_on:
|
||||
- postgres
|
||||
profiles:
|
||||
- backup
|
||||
networks:
|
||||
- mcphub-network
|
||||
```
|
||||
|
||||
创建 `scripts/backup.sh`:
|
||||
|
||||
```bash
|
||||
#!/bin/sh
|
||||
BACKUP_FILE="/backups/mcphub_$(date +%Y%m%d_%H%M%S).sql"
|
||||
pg_dump -h postgres -U mcphub -d mcphub > "$BACKUP_FILE"
|
||||
echo "备份已创建:$BACKUP_FILE"
|
||||
|
||||
# 只保留最近 7 天的备份
|
||||
find /backups -name "mcphub_*.sql" -mtime +7 -delete
|
||||
```
|
||||
|
||||
运行备份:
|
||||
|
||||
```bash
|
||||
docker-compose --profile backup run --rm backup
|
||||
```
|
||||
|
||||
## 监控和健康检查
|
||||
|
||||
### 健康检查端点
|
||||
|
||||
在您的应用程序中添加:
|
||||
|
||||
```javascript
|
||||
// 在您的 Express 应用中
|
||||
app.get('/health', (req, res) => {
|
||||
res.json({
|
||||
status: 'healthy',
|
||||
timestamp: new Date().toISOString(),
|
||||
uptime: process.uptime(),
|
||||
memory: process.memoryUsage(),
|
||||
version: process.env.npm_package_version,
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Docker 健康检查
|
||||
|
||||
```yaml
|
||||
services:
|
||||
mcphub:
|
||||
# ... 其他配置
|
||||
healthcheck:
|
||||
test: ['CMD', 'wget', '--quiet', '--tries=1', '--spider', 'http://localhost:3000/health']
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 60s
|
||||
```
|
||||
|
||||
### 使用 Watchtower 监控
|
||||
|
||||
添加自动更新:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
watchtower:
|
||||
image: containrrr/watchtower
|
||||
container_name: mcphub-watchtower
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
environment:
|
||||
- WATCHTOWER_CLEANUP=true
|
||||
- WATCHTOWER_POLL_INTERVAL=3600
|
||||
- WATCHTOWER_INCLUDE_STOPPED=true
|
||||
restart: unless-stopped
|
||||
```
|
||||
|
||||
## 故障排除
|
||||
|
||||
### 常见问题
|
||||
|
||||
**容器启动失败**:使用 `docker-compose logs mcphub` 检查日志
|
||||
|
||||
**数据库连接错误**:确保 PostgreSQL 健康且可访问
|
||||
|
||||
**端口冲突**:检查端口 3000/5432 是否已被占用
|
||||
|
||||
**卷挂载问题**:验证文件路径和权限
|
||||
|
||||
### 调试命令
|
||||
|
||||
```bash
|
||||
# 检查容器状态
|
||||
docker-compose ps
|
||||
|
||||
# 查看日志
|
||||
docker-compose logs -f [service_name]
|
||||
|
||||
# 在容器中执行命令
|
||||
docker-compose exec mcphub sh
|
||||
|
||||
# 检查数据库连接
|
||||
docker-compose exec postgres psql -U mcphub -d mcphub
|
||||
|
||||
# 重启特定服务
|
||||
docker-compose restart mcphub
|
||||
|
||||
# 重新构建并重启
|
||||
docker-compose up --build -d
|
||||
```
|
||||
|
||||
### 性能优化
|
||||
|
||||
```yaml
|
||||
services:
|
||||
mcphub:
|
||||
# ... 其他配置
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
memory: 512M
|
||||
cpus: '0.5'
|
||||
reservations:
|
||||
memory: 256M
|
||||
cpus: '0.25'
|
||||
```
|
||||
|
||||
此 Docker 设置为 MCPHub 提供了完整的容器化环境,包含开发和生产配置。
|
||||
389
docs/zh/configuration/environment-variables.mdx
Normal file
@@ -0,0 +1,389 @@
|
||||
---
|
||||
title: '环境变量配置'
|
||||
description: '使用环境变量配置 MCPHub'
|
||||
---
|
||||
|
||||
# 环境变量配置
|
||||
|
||||
MCPHub 使用环境变量进行配置。本指南涵盖所有可用变量及其用法。
|
||||
|
||||
## 核心应用设置
|
||||
|
||||
### 服务器配置
|
||||
|
||||
| 变量 | 默认值 | 描述 |
|
||||
| ----------- | ------------- | ----------------------------------------------- |
|
||||
| `PORT` | `3000` | HTTP 服务器端口号 |
|
||||
| `HOST` | `0.0.0.0` | 服务器绑定的主机地址 |
|
||||
| `NODE_ENV` | `development` | 应用环境(`development`、`production`、`test`) |
|
||||
| `LOG_LEVEL` | `info` | 日志级别(`error`、`warn`、`info`、`debug`) |
|
||||
|
||||
```env
|
||||
PORT=3000
|
||||
HOST=0.0.0.0
|
||||
NODE_ENV=production
|
||||
LOG_LEVEL=info
|
||||
```
|
||||
|
||||
### 数据库配置
|
||||
|
||||
| 变量 | 默认值 | 描述 |
|
||||
| -------------- | ----------- | --------------------- |
|
||||
| `DATABASE_URL` | - | PostgreSQL 连接字符串 |
|
||||
| `DB_HOST` | `localhost` | 数据库主机 |
|
||||
| `DB_PORT` | `5432` | 数据库端口 |
|
||||
| `DB_NAME` | `mcphub` | 数据库名称 |
|
||||
| `DB_USER` | `mcphub` | 数据库用户名 |
|
||||
| `DB_PASSWORD` | - | 数据库密码 |
|
||||
| `DB_SSL` | `false` | 启用数据库 SSL 连接 |
|
||||
| `DB_POOL_MIN` | `2` | 最小数据库连接池大小 |
|
||||
| `DB_POOL_MAX` | `10` | 最大数据库连接池大小 |
|
||||
|
||||
```env
|
||||
# 选项 1:完整连接字符串
|
||||
DATABASE_URL=postgresql://username:password@localhost:5432/mcphub
|
||||
|
||||
# 选项 2:单独组件
|
||||
DB_HOST=localhost
|
||||
DB_PORT=5432
|
||||
DB_NAME=mcphub
|
||||
DB_USER=mcphub
|
||||
DB_PASSWORD=your-password
|
||||
DB_SSL=false
|
||||
```
|
||||
|
||||
## 认证与安全
|
||||
|
||||
### JWT 配置
|
||||
|
||||
| 变量 | 默认值 | 描述 |
|
||||
| ------------------------ | ------- | ------------------------ |
|
||||
| `JWT_SECRET` | - | JWT 令牌签名密钥(必需) |
|
||||
| `JWT_EXPIRES_IN` | `24h` | JWT 令牌过期时间 |
|
||||
| `JWT_REFRESH_EXPIRES_IN` | `7d` | 刷新令牌过期时间 |
|
||||
| `JWT_ALGORITHM` | `HS256` | JWT 签名算法 |
|
||||
|
||||
```env
|
||||
JWT_SECRET=your-super-secret-key-change-this-in-production
|
||||
JWT_EXPIRES_IN=24h
|
||||
JWT_REFRESH_EXPIRES_IN=7d
|
||||
```
|
||||
|
||||
### 会话与安全
|
||||
|
||||
| 变量 | 默认值 | 描述 |
|
||||
| ------------------- | ------ | -------------------- |
|
||||
| `SESSION_SECRET` | - | 会话加密密钥 |
|
||||
| `BCRYPT_ROUNDS` | `12` | bcrypt 哈希轮数 |
|
||||
| `RATE_LIMIT_WINDOW` | `15` | 速率限制窗口(分钟) |
|
||||
| `RATE_LIMIT_MAX` | `100` | 每个窗口最大请求数 |
|
||||
| `CORS_ORIGIN` | `*` | 允许的 CORS 来源 |
|
||||
|
||||
```env
|
||||
SESSION_SECRET=your-session-secret
|
||||
BCRYPT_ROUNDS=12
|
||||
RATE_LIMIT_WINDOW=15
|
||||
RATE_LIMIT_MAX=100
|
||||
CORS_ORIGIN=https://your-domain.com,https://admin.your-domain.com
|
||||
```
|
||||
|
||||
## 外部服务
|
||||
|
||||
### OpenAI 配置
|
||||
|
||||
| 变量 | 默认值 | 描述 |
|
||||
| ------------------------ | ------------------------ | ------------------------------- |
|
||||
| `OPENAI_API_KEY` | - | OpenAI API 密钥(用于智能路由) |
|
||||
| `OPENAI_MODEL` | `gpt-3.5-turbo` | OpenAI 嵌入模型 |
|
||||
| `OPENAI_EMBEDDING_MODEL` | `text-embedding-ada-002` | 向量嵌入模型 |
|
||||
| `OPENAI_MAX_TOKENS` | `1000` | 每个请求最大令牌数 |
|
||||
| `OPENAI_TEMPERATURE` | `0.1` | AI 响应温度 |
|
||||
|
||||
```env
|
||||
OPENAI_API_KEY=sk-your-openai-api-key
|
||||
OPENAI_MODEL=gpt-3.5-turbo
|
||||
OPENAI_EMBEDDING_MODEL=text-embedding-ada-002
|
||||
OPENAI_MAX_TOKENS=1000
|
||||
OPENAI_TEMPERATURE=0.1
|
||||
```
|
||||
|
||||
### Redis 配置(可选)
|
||||
|
||||
| 变量 | 默认值 | 描述 |
|
||||
| ---------------- | ----------- | ---------------- |
|
||||
| `REDIS_URL` | - | Redis 连接字符串 |
|
||||
| `REDIS_HOST` | `localhost` | Redis 主机 |
|
||||
| `REDIS_PORT` | `6379` | Redis 端口 |
|
||||
| `REDIS_PASSWORD` | - | Redis 密码 |
|
||||
| `REDIS_DB` | `0` | Redis 数据库编号 |
|
||||
| `REDIS_PREFIX` | `mcphub:` | Redis 键前缀 |
|
||||
|
||||
```env
|
||||
# 选项 1:完整连接字符串
|
||||
REDIS_URL=redis://username:password@localhost:6379/0
|
||||
|
||||
# 选项 2:单独组件
|
||||
REDIS_HOST=localhost
|
||||
REDIS_PORT=6379
|
||||
REDIS_PASSWORD=your-redis-password
|
||||
REDIS_DB=0
|
||||
REDIS_PREFIX=mcphub:
|
||||
```
|
||||
|
||||
## MCP 服务器配置
|
||||
|
||||
### 默认设置
|
||||
|
||||
| 变量 | 默认值 | 描述 |
|
||||
| ------------------- | ------------------- | ---------------------------- |
|
||||
| `MCP_SETTINGS_FILE` | `mcp_settings.json` | MCP 设置文件路径 |
|
||||
| `MCP_SERVERS_FILE` | `servers.json` | 服务器配置文件路径 |
|
||||
| `MCP_TIMEOUT` | `30000` | MCP 操作默认超时(毫秒) |
|
||||
| `MCP_MAX_RETRIES` | `3` | 失败操作最大重试次数 |
|
||||
| `MCP_RESTART_DELAY` | `5000` | 重启失败服务器的延迟(毫秒) |
|
||||
|
||||
```env
|
||||
MCP_SETTINGS_FILE=./config/mcp_settings.json
|
||||
MCP_SERVERS_FILE=./config/servers.json
|
||||
MCP_TIMEOUT=30000
|
||||
MCP_MAX_RETRIES=3
|
||||
MCP_RESTART_DELAY=5000
|
||||
```
|
||||
|
||||
### 智能路由
|
||||
|
||||
| 变量 | 默认值 | 描述 |
|
||||
| --------------------------- | ------ | ---------------------- |
|
||||
| `SMART_ROUTING_ENABLED` | `true` | 启用 AI 驱动的智能路由 |
|
||||
| `SMART_ROUTING_THRESHOLD` | `0.7` | 路由相似度阈值 |
|
||||
| `SMART_ROUTING_MAX_RESULTS` | `5` | 返回的最大工具数 |
|
||||
| `VECTOR_CACHE_TTL` | `3600` | 向量缓存 TTL(秒) |
|
||||
|
||||
```env
|
||||
SMART_ROUTING_ENABLED=true
|
||||
SMART_ROUTING_THRESHOLD=0.7
|
||||
SMART_ROUTING_MAX_RESULTS=5
|
||||
VECTOR_CACHE_TTL=3600
|
||||
```
|
||||
|
||||
## 文件存储与上传
|
||||
|
||||
| 变量 | 默认值 | 描述 |
|
||||
| -------------------- | ---------------- | -------------------------------- |
|
||||
| `UPLOAD_DIR` | `./uploads` | 文件上传目录 |
|
||||
| `MAX_FILE_SIZE` | `10485760` | 最大文件大小(字节,10MB) |
|
||||
| `ALLOWED_FILE_TYPES` | `image/*,text/*` | 允许的 MIME 类型 |
|
||||
| `STORAGE_TYPE` | `local` | 存储类型(`local`、`s3`、`gcs`) |
|
||||
|
||||
```env
|
||||
UPLOAD_DIR=./data/uploads
|
||||
MAX_FILE_SIZE=10485760
|
||||
ALLOWED_FILE_TYPES=image/*,text/*,application/json
|
||||
STORAGE_TYPE=local
|
||||
```
|
||||
|
||||
### S3 存储(可选)
|
||||
|
||||
| 变量 | 默认值 | 描述 |
|
||||
| ---------------------- | ----------- | -------------- |
|
||||
| `S3_BUCKET` | - | S3 存储桶名称 |
|
||||
| `S3_REGION` | `us-east-1` | S3 区域 |
|
||||
| `S3_ACCESS_KEY_ID` | - | S3 访问密钥 |
|
||||
| `S3_SECRET_ACCESS_KEY` | - | S3 密钥 |
|
||||
| `S3_ENDPOINT` | - | 自定义 S3 端点 |
|
||||
|
||||
```env
|
||||
S3_BUCKET=mcphub-uploads
|
||||
S3_REGION=us-east-1
|
||||
S3_ACCESS_KEY_ID=your-access-key
|
||||
S3_SECRET_ACCESS_KEY=your-secret-key
|
||||
```
|
||||
|
||||
## 监控与日志
|
||||
|
||||
### 应用监控
|
||||
|
||||
| 变量 | 默认值 | 描述 |
|
||||
| ------------------------ | ------- | -------------------- |
|
||||
| `METRICS_ENABLED` | `true` | 启用指标收集 |
|
||||
| `METRICS_PORT` | `9090` | 指标端点端口 |
|
||||
| `HEALTH_CHECK_INTERVAL` | `30000` | 健康检查间隔(毫秒) |
|
||||
| `PERFORMANCE_MONITORING` | `false` | 启用性能监控 |
|
||||
|
||||
```env
|
||||
METRICS_ENABLED=true
|
||||
METRICS_PORT=9090
|
||||
HEALTH_CHECK_INTERVAL=30000
|
||||
PERFORMANCE_MONITORING=true
|
||||
```
|
||||
|
||||
### 日志配置
|
||||
|
||||
| 变量 | 默认值 | 描述 |
|
||||
| ------------------ | ------------ | -------------------------------- |
|
||||
| `LOG_FORMAT` | `json` | 日志格式(`json`、`text`) |
|
||||
| `LOG_FILE` | - | 日志文件路径(如果启用文件日志) |
|
||||
| `LOG_MAX_SIZE` | `10m` | 最大日志文件大小 |
|
||||
| `LOG_MAX_FILES` | `5` | 最大日志文件数 |
|
||||
| `LOG_DATE_PATTERN` | `YYYY-MM-DD` | 日志轮换日期模式 |
|
||||
|
||||
```env
|
||||
LOG_FORMAT=json
|
||||
LOG_FILE=./logs/mcphub.log
|
||||
LOG_MAX_SIZE=10m
|
||||
LOG_MAX_FILES=5
|
||||
LOG_DATE_PATTERN=YYYY-MM-DD
|
||||
```
|
||||
|
||||
## 开发与调试
|
||||
|
||||
| 变量 | 默认值 | 描述 |
|
||||
| ------------------------ | ------- | ------------------------------- |
|
||||
| `DEBUG` | - | 调试命名空间(例如 `mcphub:*`) |
|
||||
| `DEV_TOOLS_ENABLED` | `false` | 启用开发工具 |
|
||||
| `HOT_RELOAD` | `true` | 在开发中启用热重载 |
|
||||
| `MOCK_EXTERNAL_SERVICES` | `false` | 模拟外部 API 调用 |
|
||||
|
||||
```env
|
||||
DEBUG=mcphub:*
|
||||
DEV_TOOLS_ENABLED=true
|
||||
HOT_RELOAD=true
|
||||
MOCK_EXTERNAL_SERVICES=false
|
||||
```
|
||||
|
||||
## 生产优化
|
||||
|
||||
| 变量 | 默认值 | 描述 |
|
||||
| ------------------ | ------- | ---------------------- |
|
||||
| `CLUSTER_MODE` | `false` | 启用集群模式 |
|
||||
| `WORKER_PROCESSES` | `0` | 工作进程数(0 = 自动) |
|
||||
| `MEMORY_LIMIT` | - | 每个进程内存限制 |
|
||||
| `CPU_LIMIT` | - | 每个进程 CPU 限制 |
|
||||
| `GC_OPTIMIZE` | `false` | 启用垃圾回收优化 |
|
||||
|
||||
```env
|
||||
CLUSTER_MODE=true
|
||||
WORKER_PROCESSES=4
|
||||
MEMORY_LIMIT=512M
|
||||
GC_OPTIMIZE=true
|
||||
```
|
||||
|
||||
## 配置示例
|
||||
|
||||
### 开发环境
|
||||
|
||||
```env
|
||||
# .env.development
|
||||
NODE_ENV=development
|
||||
PORT=3000
|
||||
LOG_LEVEL=debug
|
||||
|
||||
# 数据库
|
||||
DATABASE_URL=postgresql://mcphub:password@localhost:5432/mcphub_dev
|
||||
|
||||
# 认证
|
||||
JWT_SECRET=dev-secret-key
|
||||
JWT_EXPIRES_IN=24h
|
||||
|
||||
# OpenAI(开发时可选)
|
||||
# OPENAI_API_KEY=your-dev-key
|
||||
|
||||
# 调试
|
||||
DEBUG=mcphub:*
|
||||
DEV_TOOLS_ENABLED=true
|
||||
HOT_RELOAD=true
|
||||
```
|
||||
|
||||
### 生产环境
|
||||
|
||||
```env
|
||||
# .env.production
|
||||
NODE_ENV=production
|
||||
PORT=3000
|
||||
LOG_LEVEL=info
|
||||
LOG_FORMAT=json
|
||||
|
||||
# 数据库
|
||||
DATABASE_URL=postgresql://mcphub:secure-password@db.example.com:5432/mcphub
|
||||
DB_SSL=true
|
||||
DB_POOL_MAX=20
|
||||
|
||||
# 安全
|
||||
JWT_SECRET=your-super-secure-production-secret
|
||||
SESSION_SECRET=your-session-secret
|
||||
BCRYPT_ROUNDS=14
|
||||
|
||||
# 外部服务
|
||||
OPENAI_API_KEY=your-production-openai-key
|
||||
REDIS_URL=redis://redis.example.com:6379
|
||||
|
||||
# 监控
|
||||
METRICS_ENABLED=true
|
||||
PERFORMANCE_MONITORING=true
|
||||
|
||||
# 优化
|
||||
CLUSTER_MODE=true
|
||||
GC_OPTIMIZE=true
|
||||
```
|
||||
|
||||
### Docker 环境
|
||||
|
||||
```env
|
||||
# .env.docker
|
||||
NODE_ENV=production
|
||||
HOST=0.0.0.0
|
||||
PORT=3000
|
||||
|
||||
# 使用 Docker 网络的服务名
|
||||
DATABASE_URL=postgresql://mcphub:password@postgres:5432/mcphub
|
||||
REDIS_URL=redis://redis:6379
|
||||
|
||||
# 安全
|
||||
JWT_SECRET_FILE=/run/secrets/jwt_secret
|
||||
DB_PASSWORD_FILE=/run/secrets/db_password
|
||||
|
||||
# 容器中的文件路径
|
||||
MCP_SETTINGS_FILE=/app/mcp_settings.json
|
||||
UPLOAD_DIR=/app/data/uploads
|
||||
LOG_FILE=/app/logs/mcphub.log
|
||||
```
|
||||
|
||||
## 环境变量加载
|
||||
|
||||
MCPHub 按以下顺序加载环境变量:
|
||||
|
||||
1. 系统环境变量
|
||||
2. `.env.local`(被 git 忽略)
|
||||
3. `.env.{NODE_ENV}`(例如 `.env.production`)
|
||||
4. `.env`
|
||||
|
||||
### 使用 dotenv-expand
|
||||
|
||||
MCPHub 支持变量扩展:
|
||||
|
||||
```env
|
||||
BASE_URL=https://api.example.com
|
||||
API_ENDPOINT=${BASE_URL}/v1
|
||||
DATABASE_URL=postgresql://${DB_USER}:${DB_PASSWORD}@${DB_HOST}:${DB_PORT}/${DB_NAME}
|
||||
```
|
||||
|
||||
## 安全最佳实践
|
||||
|
||||
1. **永远不要提交密钥**到版本控制
|
||||
2. **为生产使用强唯一密钥**
|
||||
3. **定期轮换密钥**
|
||||
4. **使用特定于环境的文件**
|
||||
5. **在启动时验证所有环境变量**
|
||||
6. **为容器部署使用 Docker 密钥**
|
||||
|
||||
## 验证
|
||||
|
||||
MCPHub 在启动时验证环境变量。无效配置将阻止应用程序启动并提供有用的错误消息。
|
||||
|
||||
生产环境必需变量:
|
||||
|
||||
- `JWT_SECRET`
|
||||
- `DATABASE_URL` 或单独的数据库组件
|
||||
- `OPENAI_API_KEY`(如果启用智能路由)
|
||||
|
||||
这个全面的环境配置确保 MCPHub 可以为任何部署场景正确配置。
|
||||
564
docs/zh/configuration/mcp-settings.mdx
Normal file
@@ -0,0 +1,564 @@
|
||||
---
|
||||
title: 'MCP 设置配置'
|
||||
description: '配置 MCPHub 的 MCP 服务器及其设置'
|
||||
---
|
||||
|
||||
# MCP 设置配置
|
||||
|
||||
本指南说明如何使用 `mcp_settings.json` 文件和相关配置在 MCPHub 中配置 MCP 服务器。
|
||||
|
||||
## 配置文件概述
|
||||
|
||||
MCPHub 使用几个配置文件:
|
||||
|
||||
- **`mcp_settings.json`**:主要的 MCP 服务器配置
|
||||
- **`servers.json`**:服务器元数据和分组
|
||||
- **`.env`**:环境变量和密钥
|
||||
|
||||
## 基本 MCP 设置结构
|
||||
|
||||
### mcp_settings.json
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"server-name": {
|
||||
"command": "command-to-run",
|
||||
"args": ["arg1", "arg2"],
|
||||
"env": {
|
||||
"ENV_VAR": "value"
|
||||
},
|
||||
"cwd": "/working/directory",
|
||||
"timeout": 30000,
|
||||
"restart": true
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 示例配置
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"fetch": {
|
||||
"command": "uvx",
|
||||
"args": ["mcp-server-fetch"],
|
||||
"env": {
|
||||
"USER_AGENT": "MCPHub/1.0"
|
||||
}
|
||||
},
|
||||
"playwright": {
|
||||
"command": "npx",
|
||||
"args": ["@playwright/mcp@latest", "--headless"],
|
||||
"timeout": 60000
|
||||
},
|
||||
"slack": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "@modelcontextprotocol/server-slack"],
|
||||
"env": {
|
||||
"SLACK_BOT_TOKEN": "${SLACK_BOT_TOKEN}",
|
||||
"SLACK_TEAM_ID": "${SLACK_TEAM_ID}"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 服务器配置选项
|
||||
|
||||
### 必需字段
|
||||
|
||||
| 字段 | 类型 | 描述 |
|
||||
| --------- | ------ | ---------------- |
|
||||
| `command` | string | 可执行命令或路径 |
|
||||
| `args` | array | 命令行参数 |
|
||||
|
||||
### 可选字段
|
||||
|
||||
| 字段 | 类型 | 默认值 | 描述 |
|
||||
| -------------- | ------- | --------------- | ------------------ |
|
||||
| `env` | object | `{}` | 环境变量 |
|
||||
| `cwd` | string | `process.cwd()` | 工作目录 |
|
||||
| `timeout` | number | `30000` | 启动超时(毫秒) |
|
||||
| `restart` | boolean | `true` | 失败时自动重启 |
|
||||
| `maxRestarts` | number | `5` | 最大重启次数 |
|
||||
| `restartDelay` | number | `5000` | 重启间延迟(毫秒) |
|
||||
| `stdio` | string | `pipe` | stdio 配置 |
|
||||
|
||||
## 常见 MCP 服务器示例
|
||||
|
||||
### Web 和 API 服务器
|
||||
|
||||
#### Fetch 服务器
|
||||
|
||||
```json
|
||||
{
|
||||
"fetch": {
|
||||
"command": "uvx",
|
||||
"args": ["mcp-server-fetch"],
|
||||
"env": {
|
||||
"USER_AGENT": "MCPHub/1.0",
|
||||
"MAX_REDIRECTS": "10"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 使用 Playwright 进行网页抓取
|
||||
|
||||
```json
|
||||
{
|
||||
"playwright": {
|
||||
"command": "npx",
|
||||
"args": ["@playwright/mcp@latest", "--headless"],
|
||||
"timeout": 60000,
|
||||
"env": {
|
||||
"PLAYWRIGHT_BROWSERS_PATH": "/tmp/browsers"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 文件和系统服务器
|
||||
|
||||
#### 文件系统服务器
|
||||
|
||||
```json
|
||||
{
|
||||
"filesystem": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "@modelcontextprotocol/server-filesystem", "/allowed/path"],
|
||||
"env": {
|
||||
"ALLOWED_OPERATIONS": "read,write,list"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### SQLite 服务器
|
||||
|
||||
```json
|
||||
{
|
||||
"sqlite": {
|
||||
"command": "uvx",
|
||||
"args": ["mcp-server-sqlite", "--db-path", "/path/to/database.db"],
|
||||
"env": {
|
||||
"SQLITE_READONLY": "false"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 通信服务器
|
||||
|
||||
#### Slack 服务器
|
||||
|
||||
```json
|
||||
{
|
||||
"slack": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "@modelcontextprotocol/server-slack"],
|
||||
"env": {
|
||||
"SLACK_BOT_TOKEN": "${SLACK_BOT_TOKEN}",
|
||||
"SLACK_TEAM_ID": "${SLACK_TEAM_ID}",
|
||||
"SLACK_APP_TOKEN": "${SLACK_APP_TOKEN}"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 邮件服务器
|
||||
|
||||
```json
|
||||
{
|
||||
"email": {
|
||||
"command": "python",
|
||||
"args": ["-m", "mcp_server_email"],
|
||||
"env": {
|
||||
"SMTP_HOST": "smtp.gmail.com",
|
||||
"SMTP_PORT": "587",
|
||||
"EMAIL_USER": "${EMAIL_USER}",
|
||||
"EMAIL_PASSWORD": "${EMAIL_PASSWORD}"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 开发和 API 服务器
|
||||
|
||||
#### GitHub 服务器
|
||||
|
||||
```json
|
||||
{
|
||||
"github": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "@modelcontextprotocol/server-github"],
|
||||
"env": {
|
||||
"GITHUB_PERSONAL_ACCESS_TOKEN": "${GITHUB_TOKEN}"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Google Drive 服务器
|
||||
|
||||
```json
|
||||
{
|
||||
"gdrive": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "@google/mcp-server-gdrive"],
|
||||
"env": {
|
||||
"GOOGLE_CLIENT_ID": "${GOOGLE_CLIENT_ID}",
|
||||
"GOOGLE_CLIENT_SECRET": "${GOOGLE_CLIENT_SECRET}",
|
||||
"GOOGLE_REFRESH_TOKEN": "${GOOGLE_REFRESH_TOKEN}"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 地图和位置服务
|
||||
|
||||
#### 高德地图服务器
|
||||
|
||||
```json
|
||||
{
|
||||
"amap": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "@amap/amap-maps-mcp-server"],
|
||||
"env": {
|
||||
"AMAP_MAPS_API_KEY": "${AMAP_API_KEY}",
|
||||
"AMAP_LANGUAGE": "zh-cn"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### OpenStreetMap 服务器
|
||||
|
||||
```json
|
||||
{
|
||||
"osm": {
|
||||
"command": "python",
|
||||
"args": ["-m", "mcp_server_osm"],
|
||||
"env": {
|
||||
"OSM_USER_AGENT": "MCPHub/1.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 高级配置
|
||||
|
||||
### 环境变量替换
|
||||
|
||||
MCPHub 支持使用 `${VAR_NAME}` 语法进行环境变量替换:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"api-server": {
|
||||
"command": "python",
|
||||
"args": ["-m", "api_server"],
|
||||
"env": {
|
||||
"API_KEY": "${API_KEY}",
|
||||
"API_URL": "${API_BASE_URL}/v1",
|
||||
"DEBUG": "${NODE_ENV:development}"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
可以使用 `${VAR_NAME:default}` 指定默认值:
|
||||
|
||||
```json
|
||||
{
|
||||
"timeout": "${MCP_TIMEOUT:30000}",
|
||||
"maxRestarts": "${MCP_MAX_RESTARTS:5}"
|
||||
}
|
||||
```
|
||||
|
||||
### 条件配置
|
||||
|
||||
根据环境使用不同配置:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"database": {
|
||||
"command": "python",
|
||||
"args": ["-m", "db_server"],
|
||||
"env": {
|
||||
"DB_URL": "${NODE_ENV:development == 'production' ? DATABASE_URL : DEV_DATABASE_URL}"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 自定义服务器脚本
|
||||
|
||||
#### 本地 Python 服务器
|
||||
|
||||
```json
|
||||
{
|
||||
"custom-python": {
|
||||
"command": "python",
|
||||
"args": ["./servers/custom_server.py"],
|
||||
"cwd": "/app/custom-servers",
|
||||
"env": {
|
||||
"PYTHONPATH": "/app/custom-servers",
|
||||
"CONFIG_FILE": "./config.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 本地 Node.js 服务器
|
||||
|
||||
```json
|
||||
{
|
||||
"custom-node": {
|
||||
"command": "node",
|
||||
"args": ["./servers/custom-server.js"],
|
||||
"cwd": "/app/custom-servers",
|
||||
"env": {
|
||||
"NODE_ENV": "production"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 服务器元数据配置
|
||||
|
||||
### servers.json
|
||||
|
||||
使用服务器元数据补充 `mcp_settings.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"servers": {
|
||||
"fetch": {
|
||||
"name": "Fetch 服务器",
|
||||
"description": "用于网络请求的 HTTP 客户端",
|
||||
"category": "web",
|
||||
"tags": ["http", "api", "web"],
|
||||
"version": "1.0.0",
|
||||
"author": "MCPHub 团队",
|
||||
"documentation": "https://docs.mcphub.com/servers/fetch",
|
||||
"enabled": true
|
||||
},
|
||||
"playwright": {
|
||||
"name": "Playwright 浏览器",
|
||||
"description": "网页自动化和抓取",
|
||||
"category": "automation",
|
||||
"tags": ["browser", "scraping", "automation"],
|
||||
"version": "2.0.0",
|
||||
"enabled": true
|
||||
}
|
||||
},
|
||||
"groups": {
|
||||
"web-tools": {
|
||||
"name": "网页工具",
|
||||
"description": "用于网页交互的工具",
|
||||
"servers": ["fetch", "playwright"],
|
||||
"access": "public"
|
||||
},
|
||||
"admin-tools": {
|
||||
"name": "管理工具",
|
||||
"description": "管理实用程序",
|
||||
"servers": ["filesystem", "database"],
|
||||
"access": "admin"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 组管理
|
||||
|
||||
### 组配置
|
||||
|
||||
```json
|
||||
{
|
||||
"groups": {
|
||||
"production": {
|
||||
"name": "生产工具",
|
||||
"description": "稳定的生产服务器",
|
||||
"servers": ["fetch", "slack", "github"],
|
||||
"access": "authenticated",
|
||||
"rateLimit": {
|
||||
"requestsPerMinute": 100,
|
||||
"burstLimit": 20
|
||||
}
|
||||
},
|
||||
"experimental": {
|
||||
"name": "实验功能",
|
||||
"description": "测试版和实验性服务器",
|
||||
"servers": ["experimental-ai", "beta-search"],
|
||||
"access": "admin",
|
||||
"enabled": false
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 访问控制
|
||||
|
||||
| 访问级别 | 描述 |
|
||||
| --------------- | ------------------- |
|
||||
| `public` | 无需认证 |
|
||||
| `authenticated` | 需要有效的 JWT 令牌 |
|
||||
| `admin` | 需要管理员角色 |
|
||||
| `custom` | 自定义权限逻辑 |
|
||||
|
||||
## 动态配置
|
||||
|
||||
### 热重载
|
||||
|
||||
MCPHub 支持配置热重载:
|
||||
|
||||
```bash
|
||||
# 不重启重新加载配置
|
||||
curl -X POST http://localhost:3000/api/admin/reload-config \
|
||||
-H "Authorization: Bearer your-admin-token"
|
||||
```
|
||||
|
||||
### 配置验证
|
||||
|
||||
MCPHub 在启动和重新加载时验证配置:
|
||||
|
||||
```json
|
||||
{
|
||||
"validation": {
|
||||
"strict": true,
|
||||
"allowUnknownServers": false,
|
||||
"requireDocumentation": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 最佳实践
|
||||
|
||||
### 安全
|
||||
|
||||
1. **对敏感数据使用环境变量**:
|
||||
|
||||
```json
|
||||
{
|
||||
"env": {
|
||||
"API_KEY": "${API_KEY}",
|
||||
"DATABASE_PASSWORD": "${DB_PASSWORD}"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
2. **限制服务器权限**:
|
||||
```json
|
||||
{
|
||||
"filesystem": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "@modelcontextprotocol/server-filesystem", "/restricted/path"],
|
||||
"env": {
|
||||
"READONLY": "true"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 性能
|
||||
|
||||
1. **设置适当的超时**:
|
||||
|
||||
```json
|
||||
{
|
||||
"timeout": 30000,
|
||||
"maxRestarts": 3,
|
||||
"restartDelay": 5000
|
||||
}
|
||||
```
|
||||
|
||||
2. **资源限制**:
|
||||
```json
|
||||
{
|
||||
"env": {
|
||||
"NODE_OPTIONS": "--max-old-space-size=512",
|
||||
"MEMORY_LIMIT": "512MB"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 监控
|
||||
|
||||
1. **启用健康检查**:
|
||||
|
||||
```json
|
||||
{
|
||||
"healthCheck": {
|
||||
"enabled": true,
|
||||
"interval": 30000,
|
||||
"timeout": 5000
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
2. **日志配置**:
|
||||
```json
|
||||
{
|
||||
"env": {
|
||||
"LOG_LEVEL": "info",
|
||||
"LOG_FORMAT": "json"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 故障排除
|
||||
|
||||
### 常见问题
|
||||
|
||||
**服务器无法启动**:检查命令和参数
|
||||
|
||||
```bash
|
||||
# 手动测试命令
|
||||
uvx mcp-server-fetch
|
||||
```
|
||||
|
||||
**找不到环境变量**:验证 `.env` 文件
|
||||
|
||||
```bash
|
||||
# 检查环境
|
||||
printenv | grep API_KEY
|
||||
```
|
||||
|
||||
**权限错误**:检查文件权限和路径
|
||||
|
||||
```bash
|
||||
# 验证可执行权限
|
||||
ls -la /path/to/server
|
||||
```
|
||||
|
||||
### 调试配置
|
||||
|
||||
启用调试模式进行详细日志记录:
|
||||
|
||||
```json
|
||||
{
|
||||
"debug": {
|
||||
"enabled": true,
|
||||
"logLevel": "debug",
|
||||
"includeEnv": false,
|
||||
"logStartup": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 验证错误
|
||||
|
||||
常见验证错误和解决方案:
|
||||
|
||||
1. **缺少必需字段**:添加 `command` 和 `args`
|
||||
2. **无效超时**:使用数字,不是字符串
|
||||
3. **找不到环境变量**:检查 `.env` 文件
|
||||
4. **找不到命令**:验证安装和 PATH
|
||||
|
||||
这个全面的指南涵盖了在 MCPHub 中为各种用例和环境配置 MCP 服务器的所有方面。
|
||||
373
docs/zh/configuration/nginx.mdx
Normal file
@@ -0,0 +1,373 @@
|
||||
---
|
||||
title: 'Nginx 配置'
|
||||
description: '配置 Nginx 作为 MCPHub 的反向代理'
|
||||
---
|
||||
|
||||
# Nginx 配置
|
||||
|
||||
本指南说明如何配置 Nginx 作为 MCPHub 的反向代理,包括 SSL 终止、负载均衡和缓存策略。
|
||||
|
||||
## 基本反向代理设置
|
||||
|
||||
### 配置文件
|
||||
|
||||
创建或更新您的 Nginx 配置文件(`/etc/nginx/sites-available/mcphub`):
|
||||
|
||||
```nginx
|
||||
server {
|
||||
listen 80;
|
||||
server_name your-domain.com;
|
||||
|
||||
# 将 HTTP 重定向到 HTTPS
|
||||
return 301 https://$server_name$request_uri;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 443 ssl http2;
|
||||
server_name your-domain.com;
|
||||
|
||||
# SSL 配置
|
||||
ssl_certificate /path/to/your/certificate.crt;
|
||||
ssl_certificate_key /path/to/your/private.key;
|
||||
ssl_protocols TLSv1.2 TLSv1.3;
|
||||
ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384;
|
||||
ssl_prefer_server_ciphers off;
|
||||
|
||||
# 安全头
|
||||
add_header X-Frame-Options DENY;
|
||||
add_header X-Content-Type-Options nosniff;
|
||||
add_header X-XSS-Protection "1; mode=block";
|
||||
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
|
||||
|
||||
# Gzip 压缩
|
||||
gzip on;
|
||||
gzip_vary on;
|
||||
gzip_min_length 1024;
|
||||
gzip_types
|
||||
text/plain
|
||||
text/css
|
||||
text/xml
|
||||
text/javascript
|
||||
application/json
|
||||
application/javascript
|
||||
application/xml+rss
|
||||
application/atom+xml
|
||||
image/svg+xml;
|
||||
|
||||
# 主应用程序
|
||||
location / {
|
||||
proxy_pass http://127.0.0.1:3000;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection 'upgrade';
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_cache_bypass $http_upgrade;
|
||||
proxy_read_timeout 86400;
|
||||
}
|
||||
|
||||
# API 端点,为 MCP 操作设置更长的超时
|
||||
location /api/ {
|
||||
proxy_pass http://127.0.0.1:3000;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection 'upgrade';
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_cache_bypass $http_upgrade;
|
||||
proxy_read_timeout 300;
|
||||
proxy_connect_timeout 60;
|
||||
proxy_send_timeout 60;
|
||||
}
|
||||
|
||||
# 静态资源缓存
|
||||
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
|
||||
proxy_pass http://127.0.0.1:3000;
|
||||
proxy_cache_valid 200 1d;
|
||||
proxy_cache_valid 404 1m;
|
||||
add_header Cache-Control "public, immutable";
|
||||
expires 1y;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 启用配置
|
||||
|
||||
```bash
|
||||
# 创建符号链接启用站点
|
||||
sudo ln -s /etc/nginx/sites-available/mcphub /etc/nginx/sites-enabled/
|
||||
|
||||
# 测试配置
|
||||
sudo nginx -t
|
||||
|
||||
# 重新加载 Nginx
|
||||
sudo systemctl reload nginx
|
||||
```
|
||||
|
||||
## 负载均衡配置
|
||||
|
||||
对于具有多个 MCPHub 实例的高可用性设置:
|
||||
|
||||
```nginx
|
||||
upstream mcphub_backend {
|
||||
least_conn;
|
||||
server 127.0.0.1:3000 weight=1 max_fails=3 fail_timeout=30s;
|
||||
server 127.0.0.1:3001 weight=1 max_fails=3 fail_timeout=30s;
|
||||
server 127.0.0.1:3002 weight=1 max_fails=3 fail_timeout=30s;
|
||||
|
||||
# 健康检查(Nginx Plus 功能)
|
||||
# health_check interval=5s fails=3 passes=2;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 443 ssl http2;
|
||||
server_name your-domain.com;
|
||||
|
||||
# SSL 和其他配置...
|
||||
|
||||
location / {
|
||||
proxy_pass http://mcphub_backend;
|
||||
# 其他代理设置...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 缓存配置
|
||||
|
||||
### 浏览器缓存
|
||||
|
||||
```nginx
|
||||
# 缓存静态资源
|
||||
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
|
||||
proxy_pass http://127.0.0.1:3000;
|
||||
expires 1y;
|
||||
add_header Cache-Control "public, immutable";
|
||||
}
|
||||
|
||||
# 缓存 API 响应(小心动态内容)
|
||||
location /api/public/ {
|
||||
proxy_pass http://127.0.0.1:3000;
|
||||
proxy_cache mcphub_cache;
|
||||
proxy_cache_valid 200 5m;
|
||||
proxy_cache_key "$scheme$request_method$host$request_uri";
|
||||
add_header X-Cache-Status $upstream_cache_status;
|
||||
}
|
||||
```
|
||||
|
||||
### Nginx 代理缓存
|
||||
|
||||
在 `nginx.conf` 的 `http` 块中添加:
|
||||
|
||||
```nginx
|
||||
http {
|
||||
# 代理缓存配置
|
||||
proxy_cache_path /var/cache/nginx/mcphub
|
||||
levels=1:2
|
||||
keys_zone=mcphub_cache:10m
|
||||
max_size=1g
|
||||
inactive=60m
|
||||
use_temp_path=off;
|
||||
|
||||
# 其他配置...
|
||||
}
|
||||
```
|
||||
|
||||
## WebSocket 支持
|
||||
|
||||
对于实时功能和 SSE(服务器发送事件):
|
||||
|
||||
```nginx
|
||||
location /api/stream {
|
||||
proxy_pass http://127.0.0.1:3000;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
|
||||
# 禁用实时响应的缓冲
|
||||
proxy_buffering off;
|
||||
proxy_cache off;
|
||||
|
||||
# 长连接超时
|
||||
proxy_read_timeout 24h;
|
||||
proxy_send_timeout 24h;
|
||||
}
|
||||
```
|
||||
|
||||
## 安全配置
|
||||
|
||||
### 速率限制
|
||||
|
||||
```nginx
|
||||
http {
|
||||
# 定义速率限制区域
|
||||
limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;
|
||||
limit_req_zone $binary_remote_addr zone=login:10m rate=1r/s;
|
||||
|
||||
server {
|
||||
# 对 API 端点应用速率限制
|
||||
location /api/ {
|
||||
limit_req zone=api burst=20 nodelay;
|
||||
# 其他配置...
|
||||
}
|
||||
|
||||
# 登录端点的严格速率限制
|
||||
location /api/auth/login {
|
||||
limit_req zone=login burst=5;
|
||||
# 其他配置...
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### IP 白名单
|
||||
|
||||
```nginx
|
||||
# 为管理端点允许特定 IP
|
||||
location /api/admin/ {
|
||||
allow 192.168.1.0/24;
|
||||
allow 10.0.0.0/8;
|
||||
deny all;
|
||||
|
||||
proxy_pass http://127.0.0.1:3000;
|
||||
# 其他代理设置...
|
||||
}
|
||||
```
|
||||
|
||||
## 监控和日志
|
||||
|
||||
### 访问日志
|
||||
|
||||
```nginx
|
||||
http {
|
||||
# 自定义日志格式
|
||||
log_format mcphub_format '$remote_addr - $remote_user [$time_local] '
|
||||
'"$request" $status $body_bytes_sent '
|
||||
'"$http_referer" "$http_user_agent" '
|
||||
'$request_time $upstream_response_time';
|
||||
|
||||
server {
|
||||
# 启用访问日志
|
||||
access_log /var/log/nginx/mcphub_access.log mcphub_format;
|
||||
error_log /var/log/nginx/mcphub_error.log;
|
||||
|
||||
# 其他配置...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 状态页面
|
||||
|
||||
```nginx
|
||||
location /nginx_status {
|
||||
stub_status;
|
||||
allow 127.0.0.1;
|
||||
deny all;
|
||||
}
|
||||
```
|
||||
|
||||
## Docker 集成
|
||||
|
||||
当在 Docker 中运行 MCPHub 时,更新代理配置:
|
||||
|
||||
```nginx
|
||||
upstream mcphub_docker {
|
||||
server mcphub:3000; # Docker 服务名
|
||||
}
|
||||
|
||||
server {
|
||||
location / {
|
||||
proxy_pass http://mcphub_docker;
|
||||
# 其他代理设置...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 完整示例配置
|
||||
|
||||
使用提供的 `nginx.conf.example` 的生产就绪示例:
|
||||
|
||||
```bash
|
||||
# 复制示例配置
|
||||
cp nginx.conf.example /etc/nginx/sites-available/mcphub
|
||||
|
||||
# 使用您的域名和路径更新配置
|
||||
sudo nano /etc/nginx/sites-available/mcphub
|
||||
|
||||
# 启用站点
|
||||
sudo ln -s /etc/nginx/sites-available/mcphub /etc/nginx/sites-enabled/
|
||||
|
||||
# 测试并重新加载
|
||||
sudo nginx -t && sudo systemctl reload nginx
|
||||
```
|
||||
|
||||
## 故障排除
|
||||
|
||||
### 常见问题
|
||||
|
||||
**502 Bad Gateway**:检查 MCPHub 是否正在运行且可访问
|
||||
|
||||
**504 Gateway Timeout**:为长时间运行的操作增加 `proxy_read_timeout`
|
||||
|
||||
**WebSocket 连接失败**:确保正确的 `Upgrade` 和 `Connection` 头
|
||||
|
||||
**缓存问题**:清除代理缓存或在开发中禁用
|
||||
|
||||
### 调试命令
|
||||
|
||||
```bash
|
||||
# 测试 Nginx 配置
|
||||
sudo nginx -t
|
||||
|
||||
# 检查 Nginx 状态
|
||||
sudo systemctl status nginx
|
||||
|
||||
# 查看错误日志
|
||||
sudo tail -f /var/log/nginx/error.log
|
||||
|
||||
# 检查 MCPHub 是否响应
|
||||
curl -I http://localhost:3000
|
||||
```
|
||||
|
||||
## 性能优化
|
||||
|
||||
### 工作进程
|
||||
|
||||
```nginx
|
||||
# 在 nginx.conf 中
|
||||
worker_processes auto;
|
||||
worker_connections 1024;
|
||||
```
|
||||
|
||||
### 缓冲区大小
|
||||
|
||||
```nginx
|
||||
proxy_buffering on;
|
||||
proxy_buffer_size 128k;
|
||||
proxy_buffers 4 256k;
|
||||
proxy_busy_buffers_size 256k;
|
||||
```
|
||||
|
||||
### Keep-Alive
|
||||
|
||||
```nginx
|
||||
upstream mcphub_backend {
|
||||
server 127.0.0.1:3000;
|
||||
keepalive 32;
|
||||
}
|
||||
|
||||
location / {
|
||||
proxy_pass http://mcphub_backend;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Connection "";
|
||||
}
|
||||
```
|
||||
|
||||
此配置为在 Nginx 后运行 MCPHub 提供了坚实的基础,具有适当的安全性、性能和可靠性功能。
|
||||
421
docs/zh/development.mdx
Normal file
@@ -0,0 +1,421 @@
|
||||
---
|
||||
title: '开发指南'
|
||||
description: 'MCPHub 本地开发环境搭建和开发工作流指南'
|
||||
---
|
||||
|
||||
## 概述
|
||||
|
||||
本指南将帮助您搭建 MCPHub 的本地开发环境,了解项目结构,并掌握开发工作流。
|
||||
|
||||
<Info>**前提条件**:请确保已安装 Node.js 18+ 和 Git。</Info>
|
||||
|
||||
## 环境准备
|
||||
|
||||
### 系统要求
|
||||
|
||||
在开始开发之前,请确保您的系统满足以下要求:
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="软件依赖" icon="download">
|
||||
- **Node.js**: 18.0+ 版本 - **npm**: 8.0+ 版本 - **Git**: 最新版本 - **Docker**:
|
||||
可选,用于容器化开发
|
||||
</Card>
|
||||
<Card title="推荐工具" icon="toolbox">
|
||||
- **VS Code**: 推荐的代码编辑器 - **Postman**: API 测试工具 - **TablePlus**: 数据库管理工具 -
|
||||
**Docker Desktop**: 容器管理
|
||||
</Card>
|
||||
</CardGroup>
|
||||
|
||||
### 验证环境
|
||||
|
||||
```bash
|
||||
# 检查 Node.js 版本
|
||||
node --version # 应该 >= 18.0.0
|
||||
|
||||
# 检查 npm 版本
|
||||
npm --version # 应该 >= 8.0.0
|
||||
|
||||
# 检查 Git 版本
|
||||
git --version
|
||||
|
||||
# 检查 Docker(可选)
|
||||
docker --version
|
||||
```
|
||||
|
||||
## 克隆项目
|
||||
|
||||
### 获取源代码
|
||||
|
||||
```bash
|
||||
# 克隆主仓库
|
||||
git clone https://github.com/mcphub/mcphub.git
|
||||
cd mcphub
|
||||
|
||||
# 或者克隆您的 fork
|
||||
git clone https://github.com/YOUR_USERNAME/mcphub.git
|
||||
cd mcphub
|
||||
```
|
||||
|
||||
### 项目结构
|
||||
|
||||
```
|
||||
mcphub/
|
||||
├── src/ # 源代码目录
|
||||
│ ├── controllers/ # 控制器层
|
||||
│ ├── middleware/ # 中间件
|
||||
│ ├── models/ # 数据模型
|
||||
│ ├── routes/ # 路由定义
|
||||
│ ├── services/ # 业务逻辑层
|
||||
│ ├── utils/ # 工具函数
|
||||
│ └── index.ts # 应用入口
|
||||
├── tests/ # 测试文件
|
||||
├── docs/ # 文档源码
|
||||
├── docker/ # Docker 配置
|
||||
├── scripts/ # 构建脚本
|
||||
├── prisma/ # 数据库模式
|
||||
├── package.json # 项目依赖
|
||||
├── tsconfig.json # TypeScript 配置
|
||||
├── .env.example # 环境变量示例
|
||||
└── README.md # 项目说明
|
||||
```
|
||||
|
||||
## 安装依赖
|
||||
|
||||
### 安装项目依赖
|
||||
|
||||
```bash
|
||||
# 安装生产和开发依赖
|
||||
npm install
|
||||
|
||||
# 仅安装生产依赖
|
||||
npm ci --only=production
|
||||
```
|
||||
|
||||
### 全局工具安装
|
||||
|
||||
```bash
|
||||
# 安装 TypeScript 编译器
|
||||
npm install -g typescript
|
||||
|
||||
# 安装开发工具
|
||||
npm install -g tsx nodemon prisma
|
||||
|
||||
# 安装 MCPHub CLI(可选)
|
||||
npm install -g @mcphub/cli
|
||||
```
|
||||
|
||||
## 配置开发环境
|
||||
|
||||
### 环境变量配置
|
||||
|
||||
```bash
|
||||
# 复制环境变量模板
|
||||
cp .env.example .env
|
||||
|
||||
# 编辑环境变量
|
||||
nano .env
|
||||
```
|
||||
|
||||
开发环境的 `.env` 配置示例:
|
||||
|
||||
```bash title=".env"
|
||||
# 应用配置
|
||||
NODE_ENV=development
|
||||
PORT=3000
|
||||
HOST=localhost
|
||||
|
||||
# 数据库配置
|
||||
DATABASE_URL=sqlite:./data/dev.db
|
||||
|
||||
# JWT 配置
|
||||
JWT_SECRET=dev-jwt-secret-key
|
||||
JWT_EXPIRES_IN=7d
|
||||
|
||||
# 日志配置
|
||||
LOG_LEVEL=debug
|
||||
LOG_FORMAT=dev
|
||||
|
||||
# CORS 配置
|
||||
CORS_ORIGIN=http://localhost:3000,http://localhost:3001
|
||||
|
||||
# 管理员账户
|
||||
ADMIN_EMAIL=dev@mcphub.io
|
||||
ADMIN_PASSWORD=dev123
|
||||
|
||||
# 开发功能开关
|
||||
ENABLE_DEBUG_ROUTES=true
|
||||
ENABLE_SWAGGER=true
|
||||
ENABLE_HOT_RELOAD=true
|
||||
```
|
||||
|
||||
### 数据库初始化
|
||||
|
||||
```bash
|
||||
# 生成 Prisma 客户端
|
||||
npx prisma generate
|
||||
|
||||
# 运行数据库迁移
|
||||
npx prisma migrate dev --name init
|
||||
|
||||
# 填充测试数据
|
||||
npm run db:seed
|
||||
```
|
||||
|
||||
## 启动开发服务器
|
||||
|
||||
### 开发模式启动
|
||||
|
||||
```bash
|
||||
# 启动开发服务器(带热重载)
|
||||
npm run dev
|
||||
|
||||
# 或者使用 tsx 直接运行
|
||||
npx tsx watch src/index.ts
|
||||
```
|
||||
|
||||
### 后台模式启动
|
||||
|
||||
```bash
|
||||
# 使用 PM2 启动(需要先安装 PM2)
|
||||
npm install -g pm2
|
||||
npm run dev:pm2
|
||||
|
||||
# 查看进程状态
|
||||
pm2 status
|
||||
|
||||
# 查看日志
|
||||
pm2 logs mcphub-dev
|
||||
```
|
||||
|
||||
### 验证启动
|
||||
|
||||
访问以下 URL 验证服务是否正常启动:
|
||||
|
||||
- **主页**: http://localhost:3000
|
||||
- **健康检查**: http://localhost:3000/health
|
||||
- **API 文档**: http://localhost:3000/api/docs
|
||||
- **管理界面**: http://localhost:3000/admin
|
||||
|
||||
## 开发工作流
|
||||
|
||||
### 1. 功能开发流程
|
||||
|
||||
```bash
|
||||
# 1. 创建功能分支
|
||||
git checkout -b feature/your-feature-name
|
||||
|
||||
# 2. 进行开发...
|
||||
|
||||
# 3. 运行测试
|
||||
npm test
|
||||
|
||||
# 4. 代码格式化
|
||||
npm run lint:fix
|
||||
|
||||
# 5. 提交代码
|
||||
git add .
|
||||
git commit -m "feat: add your feature description"
|
||||
|
||||
# 6. 推送分支
|
||||
git push origin feature/your-feature-name
|
||||
|
||||
# 7. 创建 Pull Request
|
||||
```
|
||||
|
||||
### 2. 代码规范
|
||||
|
||||
MCPHub 项目使用以下代码规范工具:
|
||||
|
||||
```bash
|
||||
# 代码检查
|
||||
npm run lint
|
||||
|
||||
# 自动修复
|
||||
npm run lint:fix
|
||||
|
||||
# 格式化代码
|
||||
npm run format
|
||||
|
||||
# 类型检查
|
||||
npm run type-check
|
||||
```
|
||||
|
||||
### 3. 测试开发
|
||||
|
||||
```bash
|
||||
# 运行所有测试
|
||||
npm test
|
||||
|
||||
# 运行单元测试
|
||||
npm run test:unit
|
||||
|
||||
# 运行集成测试
|
||||
npm run test:integration
|
||||
|
||||
# 运行测试并生成覆盖率报告
|
||||
npm run test:coverage
|
||||
|
||||
# 监听模式运行测试
|
||||
npm run test:watch
|
||||
```
|
||||
|
||||
## 调试技巧
|
||||
|
||||
### 1. VS Code 调试配置
|
||||
|
||||
创建 `.vscode/launch.json` 文件:
|
||||
|
||||
```json title=".vscode/launch.json"
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Debug MCPHub",
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"program": "${workspaceFolder}/src/index.ts",
|
||||
"runtimeArgs": ["-r", "tsx/cjs"],
|
||||
"env": {
|
||||
"NODE_ENV": "development"
|
||||
},
|
||||
"console": "integratedTerminal",
|
||||
"skipFiles": ["<node_internals>/**"]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 日志调试
|
||||
|
||||
使用内置的日志系统进行调试:
|
||||
|
||||
```typescript
|
||||
import { logger } from '@/utils/logger';
|
||||
|
||||
// 不同级别的日志
|
||||
logger.debug('调试信息', { data });
|
||||
logger.info('信息日志', { userId });
|
||||
logger.warn('警告信息', { error });
|
||||
logger.error('错误信息', { error, stack });
|
||||
```
|
||||
|
||||
### 3. 数据库调试
|
||||
|
||||
```bash
|
||||
# 查看数据库内容
|
||||
npx prisma studio
|
||||
|
||||
# 重置数据库
|
||||
npx prisma migrate reset
|
||||
|
||||
# 查看迁移状态
|
||||
npx prisma migrate status
|
||||
```
|
||||
|
||||
## 常用开发命令
|
||||
|
||||
### 项目管理
|
||||
|
||||
```bash
|
||||
# 安装新依赖
|
||||
npm install package-name
|
||||
npm install -D package-name # 开发依赖
|
||||
|
||||
# 更新依赖
|
||||
npm update
|
||||
|
||||
# 清理缓存
|
||||
npm cache clean --force
|
||||
|
||||
# 重新安装依赖
|
||||
rm -rf node_modules package-lock.json
|
||||
npm install
|
||||
```
|
||||
|
||||
### 构建和部署
|
||||
|
||||
```bash
|
||||
# 构建项目
|
||||
npm run build
|
||||
|
||||
# 预览构建结果
|
||||
npm run preview
|
||||
|
||||
# 构建 Docker 镜像
|
||||
npm run docker:build
|
||||
|
||||
# 运行 Docker 容器
|
||||
npm run docker:run
|
||||
```
|
||||
|
||||
### 数据库操作
|
||||
|
||||
```bash
|
||||
# 创建新迁移
|
||||
npx prisma migrate dev --name your-migration-name
|
||||
|
||||
# 重置数据库
|
||||
npx prisma migrate reset
|
||||
|
||||
# 推送模式变更
|
||||
npx prisma db push
|
||||
|
||||
# 生成客户端
|
||||
npx prisma generate
|
||||
```
|
||||
|
||||
## 常见问题
|
||||
|
||||
<AccordionGroup>
|
||||
<Accordion icon="question" title="端口被占用">
|
||||
**错误信息**: `Error: listen EADDRINUSE :::3000`
|
||||
|
||||
**解决方案**:
|
||||
```bash
|
||||
# 查找占用端口的进程
|
||||
lsof -i :3000
|
||||
|
||||
# 杀死进程
|
||||
kill -9 PID
|
||||
|
||||
# 或者使用不同端口
|
||||
PORT=3001 npm run dev
|
||||
```
|
||||
</Accordion>
|
||||
|
||||
<Accordion icon="question" title="数据库连接失败">
|
||||
**可能原因**: 数据库文件权限或路径问题 **解决方案**: ```bash # 检查数据库文件 ls -la data/ #
|
||||
重新初始化数据库 rm data/dev.db npx prisma migrate dev ```
|
||||
</Accordion>
|
||||
|
||||
<Accordion icon="question" title="TypeScript 编译错误">
|
||||
**解决方案**:
|
||||
```bash
|
||||
# 清理构建缓存
|
||||
npm run clean
|
||||
|
||||
# 重新安装类型定义
|
||||
npm install @types/node @types/express
|
||||
|
||||
# 重新生成 Prisma 客户端
|
||||
npx prisma generate
|
||||
```
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
|
||||
## 进阶主题
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="架构设计" icon="building" href="/zh/development/architecture">
|
||||
了解 MCPHub 的整体架构和设计模式
|
||||
</Card>
|
||||
<Card title="API 开发" icon="code" href="/zh/development/api-development">
|
||||
学习如何开发和设计 RESTful API
|
||||
</Card>
|
||||
<Card title="性能优化" icon="rocket" href="/zh/development/performance">
|
||||
掌握性能分析和优化技巧
|
||||
</Card>
|
||||
<Card title="部署指南" icon="cloud" href="/zh/deployment/production">
|
||||
了解生产环境部署的最佳实践
|
||||
</Card>
|
||||
</CardGroup>
|
||||
244
docs/zh/development/getting-started.mdx
Normal file
@@ -0,0 +1,244 @@
|
||||
---
|
||||
title: '开发环境搭建'
|
||||
description: '学习如何为 MCPHub 搭建开发环境'
|
||||
---
|
||||
|
||||
# 开发环境搭建
|
||||
|
||||
本指南将帮助您搭建 MCPHub 的开发环境,为项目贡献代码。
|
||||
|
||||
## 先决条件
|
||||
|
||||
在开始之前,请确保您已安装以下软件:
|
||||
|
||||
- **Node.js**(版本 18 或更高)
|
||||
- **pnpm**(推荐的包管理器)
|
||||
- **Git**
|
||||
- **Docker**(可选,用于容器化开发)
|
||||
|
||||
## 搭建开发环境
|
||||
|
||||
### 1. 克隆仓库
|
||||
|
||||
```bash
|
||||
git clone https://github.com/your-username/mcphub.git
|
||||
cd mcphub
|
||||
```
|
||||
|
||||
### 2. 安装依赖
|
||||
|
||||
```bash
|
||||
pnpm install
|
||||
```
|
||||
|
||||
### 3. 环境配置
|
||||
|
||||
在根目录创建 `.env` 文件:
|
||||
|
||||
```bash
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
配置以下环境变量:
|
||||
|
||||
```env
|
||||
# 服务器配置
|
||||
PORT=3000
|
||||
NODE_ENV=development
|
||||
|
||||
# 数据库配置
|
||||
DATABASE_URL=postgresql://username:password@localhost:5432/mcphub
|
||||
|
||||
# JWT 配置
|
||||
JWT_SECRET=your-secret-key
|
||||
JWT_EXPIRES_IN=24h
|
||||
|
||||
# OpenAI 配置(用于智能路由)
|
||||
OPENAI_API_KEY=your-openai-api-key
|
||||
```
|
||||
|
||||
### 4. 数据库设置
|
||||
|
||||
如果使用 PostgreSQL,创建数据库:
|
||||
|
||||
```bash
|
||||
createdb mcphub
|
||||
```
|
||||
|
||||
### 5. MCP 服务器配置
|
||||
|
||||
创建或修改 `mcp_settings.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"fetch": {
|
||||
"command": "uvx",
|
||||
"args": ["mcp-server-fetch"]
|
||||
},
|
||||
"playwright": {
|
||||
"command": "npx",
|
||||
"args": ["@playwright/mcp@latest", "--headless"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 开发工作流
|
||||
|
||||
### 运行开发服务器
|
||||
|
||||
同时启动后端和前端开发模式:
|
||||
|
||||
```bash
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
这将启动:
|
||||
|
||||
- 后端服务器:`http://localhost:3000`
|
||||
- 前端开发服务器:`http://localhost:5173`
|
||||
|
||||
### 仅运行后端
|
||||
|
||||
```bash
|
||||
pnpm backend:dev
|
||||
```
|
||||
|
||||
### 仅运行前端
|
||||
|
||||
```bash
|
||||
pnpm frontend:dev
|
||||
```
|
||||
|
||||
### 构建项目
|
||||
|
||||
构建后端和前端:
|
||||
|
||||
```bash
|
||||
pnpm build
|
||||
```
|
||||
|
||||
## 项目结构
|
||||
|
||||
```
|
||||
mcphub/
|
||||
├── src/ # 后端源代码
|
||||
│ ├── controllers/ # Express 控制器
|
||||
│ ├── routes/ # API 路由
|
||||
│ ├── services/ # 业务逻辑
|
||||
│ ├── models/ # 数据库模型
|
||||
│ └── utils/ # 工具函数
|
||||
├── frontend/ # 前端 React 应用
|
||||
│ ├── src/
|
||||
│ │ ├── components/ # React 组件
|
||||
│ │ ├── pages/ # 页面组件
|
||||
│ │ ├── services/ # API 服务
|
||||
│ │ └── utils/ # 前端工具
|
||||
├── docs/ # 文档
|
||||
├── bin/ # CLI 脚本
|
||||
└── scripts/ # 构建和工具脚本
|
||||
```
|
||||
|
||||
## 开发工具
|
||||
|
||||
### 代码检查和格式化
|
||||
|
||||
```bash
|
||||
# 运行 ESLint
|
||||
pnpm lint
|
||||
|
||||
# 使用 Prettier 格式化代码
|
||||
pnpm format
|
||||
```
|
||||
|
||||
### 测试
|
||||
|
||||
```bash
|
||||
# 运行测试
|
||||
pnpm test
|
||||
|
||||
# 监视模式运行测试
|
||||
pnpm test --watch
|
||||
```
|
||||
|
||||
### 调试
|
||||
|
||||
使用 Node.js 检查器调试后端:
|
||||
|
||||
```bash
|
||||
pnpm backend:debug
|
||||
```
|
||||
|
||||
然后将调试器连接到 `http://localhost:9229`。
|
||||
|
||||
## 进行修改
|
||||
|
||||
### 后端开发
|
||||
|
||||
1. **控制器**:处理 HTTP 请求和响应
|
||||
2. **服务**:实现业务逻辑
|
||||
3. **模型**:定义数据库架构
|
||||
4. **路由**:定义 API 端点
|
||||
|
||||
### 前端开发
|
||||
|
||||
1. **组件**:可重用的 React 组件
|
||||
2. **页面**:特定路由的组件
|
||||
3. **服务**:API 通信
|
||||
4. **钩子**:自定义 React 钩子
|
||||
|
||||
### 添加新的 MCP 服务器
|
||||
|
||||
1. 使用新的服务器配置更新 `mcp_settings.json`
|
||||
2. 测试服务器集成
|
||||
3. 必要时更新文档
|
||||
|
||||
## 常见开发任务
|
||||
|
||||
### 添加新的 API 端点
|
||||
|
||||
1. 在 `src/controllers/` 中创建控制器
|
||||
2. 在 `src/routes/` 中定义路由
|
||||
3. 添加必要的中间件
|
||||
4. 为新端点编写测试
|
||||
|
||||
### 添加新的前端功能
|
||||
|
||||
1. 在 `frontend/src/components/` 中创建组件
|
||||
2. 根据需要添加路由
|
||||
3. 实现 API 集成
|
||||
4. 使用 Tailwind CSS 进行样式设计
|
||||
|
||||
### 数据库迁移
|
||||
|
||||
修改数据库架构时:
|
||||
|
||||
1. 更新 `src/models/` 中的模型
|
||||
2. 如果使用 TypeORM,创建迁移脚本
|
||||
3. 在本地测试迁移
|
||||
|
||||
## 故障排除
|
||||
|
||||
### 常见问题
|
||||
|
||||
**端口冲突**:确保端口 3000 和 5173 可用
|
||||
|
||||
**数据库连接**:验证 PostgreSQL 正在运行且凭据正确
|
||||
|
||||
**MCP 服务器启动**:检查 `mcp_settings.json` 中的服务器配置
|
||||
|
||||
**权限问题**:确保 MCP 服务器具有必要的权限
|
||||
|
||||
### 获取帮助
|
||||
|
||||
- 查看[贡献指南](/zh/development/contributing)
|
||||
- 阅读[架构文档](/zh/development/architecture)
|
||||
- 在 GitHub 上提交问题报告 bug
|
||||
- 加入我们的社区讨论
|
||||
|
||||
## 下一步
|
||||
|
||||
- 阅读[架构概述](/zh/development/architecture)
|
||||
- 了解[贡献指南](/zh/development/contributing)
|
||||
- 探索[配置选项](/zh/configuration/environment-variables)
|
||||
892
docs/zh/essentials/code.mdx
Normal file
@@ -0,0 +1,892 @@
|
||||
---
|
||||
title: '代码块'
|
||||
description: 'MCPHub 文档中代码块的编写和展示指南'
|
||||
---
|
||||
|
||||
## 内联代码
|
||||
|
||||
在 MCPHub 文档中使用内联代码来标记命令、配置键、文件名或短代码片段:
|
||||
|
||||
```md
|
||||
使用 `mcphub start` 命令启动服务器,配置 `MCPHUB_PORT` 环境变量。
|
||||
```
|
||||
|
||||
使用 `mcphub start` 命令启动服务器,配置 `MCPHUB_PORT` 环境变量。
|
||||
|
||||
## 代码块语法
|
||||
|
||||
### 基本代码块
|
||||
|
||||
MCPHub 支持多种编程语言的语法高亮:
|
||||
|
||||
````md
|
||||
```javascript
|
||||
// JavaScript 示例
|
||||
const mcpClient = new MCPClient({
|
||||
endpoint: process.env.MCPHUB_ENDPOINT,
|
||||
apiKey: process.env.MCPHUB_API_KEY,
|
||||
});
|
||||
```
|
||||
````
|
||||
|
||||
```javascript
|
||||
// JavaScript 示例
|
||||
const mcpClient = new MCPClient({
|
||||
endpoint: process.env.MCPHUB_ENDPOINT,
|
||||
apiKey: process.env.MCPHUB_API_KEY,
|
||||
});
|
||||
```
|
||||
|
||||
### TypeScript 代码
|
||||
|
||||
````md
|
||||
```typescript
|
||||
interface MCPServerConfig {
|
||||
id: string;
|
||||
name: string;
|
||||
endpoint: string;
|
||||
capabilities: string[];
|
||||
metadata?: Record<string, any>;
|
||||
}
|
||||
|
||||
class MCPServer implements MCPServerConfig {
|
||||
constructor(
|
||||
public id: string,
|
||||
public name: string,
|
||||
public endpoint: string,
|
||||
public capabilities: string[],
|
||||
) {}
|
||||
}
|
||||
```
|
||||
````
|
||||
|
||||
```typescript
|
||||
interface MCPServerConfig {
|
||||
id: string;
|
||||
name: string;
|
||||
endpoint: string;
|
||||
capabilities: string[];
|
||||
metadata?: Record<string, any>;
|
||||
}
|
||||
|
||||
class MCPServer implements MCPServerConfig {
|
||||
constructor(
|
||||
public id: string,
|
||||
public name: string,
|
||||
public endpoint: string,
|
||||
public capabilities: string[],
|
||||
) {}
|
||||
}
|
||||
```
|
||||
|
||||
### Python 代码
|
||||
|
||||
````md
|
||||
```python
|
||||
import requests
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
class MCPHubClient:
|
||||
def __init__(self, endpoint: str, api_key: str):
|
||||
self.endpoint = endpoint
|
||||
self.api_key = api_key
|
||||
self.headers = {
|
||||
'Authorization': f'Bearer {api_key}',
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
|
||||
def create_server(self, config: Dict) -> Dict:
|
||||
response = requests.post(
|
||||
f'{self.endpoint}/api/servers',
|
||||
json=config,
|
||||
headers=self.headers
|
||||
)
|
||||
return response.json()
|
||||
```
|
||||
````
|
||||
|
||||
```python
|
||||
import requests
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
class MCPHubClient:
|
||||
def __init__(self, endpoint: str, api_key: str):
|
||||
self.endpoint = endpoint
|
||||
self.api_key = api_key
|
||||
self.headers = {
|
||||
'Authorization': f'Bearer {api_key}',
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
|
||||
def create_server(self, config: Dict) -> Dict:
|
||||
response = requests.post(
|
||||
f'{self.endpoint}/api/servers',
|
||||
json=config,
|
||||
headers=self.headers
|
||||
)
|
||||
return response.json()
|
||||
```
|
||||
|
||||
## 配置文件
|
||||
|
||||
### YAML 配置
|
||||
|
||||
````md
|
||||
```yaml title="mcphub.yml"
|
||||
server:
|
||||
port: 3000
|
||||
host: 0.0.0.0
|
||||
|
||||
database:
|
||||
type: postgresql
|
||||
host: localhost
|
||||
port: 5432
|
||||
database: mcphub
|
||||
username: mcphub_user
|
||||
password: secure_password
|
||||
|
||||
mcp:
|
||||
servers:
|
||||
- id: ai-assistant
|
||||
name: AI Assistant Server
|
||||
endpoint: https://ai.example.com
|
||||
capabilities:
|
||||
- chat
|
||||
- completion
|
||||
- id: data-processor
|
||||
name: Data Processing Server
|
||||
endpoint: https://data.example.com
|
||||
capabilities:
|
||||
- analysis
|
||||
- transformation
|
||||
|
||||
routing:
|
||||
strategy: round_robin
|
||||
health_check:
|
||||
enabled: true
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
|
||||
logging:
|
||||
level: info
|
||||
format: json
|
||||
file: /var/log/mcphub/app.log
|
||||
```
|
||||
````
|
||||
|
||||
```yaml title="mcphub.yml"
|
||||
server:
|
||||
port: 3000
|
||||
host: 0.0.0.0
|
||||
|
||||
database:
|
||||
type: postgresql
|
||||
host: localhost
|
||||
port: 5432
|
||||
database: mcphub
|
||||
username: mcphub_user
|
||||
password: secure_password
|
||||
|
||||
mcp:
|
||||
servers:
|
||||
- id: ai-assistant
|
||||
name: AI Assistant Server
|
||||
endpoint: https://ai.example.com
|
||||
capabilities:
|
||||
- chat
|
||||
- completion
|
||||
- id: data-processor
|
||||
name: Data Processing Server
|
||||
endpoint: https://data.example.com
|
||||
capabilities:
|
||||
- analysis
|
||||
- transformation
|
||||
|
||||
routing:
|
||||
strategy: round_robin
|
||||
health_check:
|
||||
enabled: true
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
|
||||
logging:
|
||||
level: info
|
||||
format: json
|
||||
file: /var/log/mcphub/app.log
|
||||
```
|
||||
|
||||
### JSON 配置
|
||||
|
||||
````md
|
||||
```json title="package.json"
|
||||
{
|
||||
"name": "@mcphub/server",
|
||||
"version": "2.1.0",
|
||||
"description": "Model Context Protocol Hub Server",
|
||||
"main": "dist/index.js",
|
||||
"scripts": {
|
||||
"start": "node dist/index.js",
|
||||
"dev": "tsx watch src/index.ts",
|
||||
"build": "tsc",
|
||||
"test": "jest",
|
||||
"test:watch": "jest --watch",
|
||||
"lint": "eslint src/**/*.ts",
|
||||
"migrate": "prisma migrate deploy"
|
||||
},
|
||||
"dependencies": {
|
||||
"@prisma/client": "^5.7.0",
|
||||
"express": "^4.18.2",
|
||||
"helmet": "^7.1.0",
|
||||
"cors": "^2.8.5",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"winston": "^3.11.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.10.0",
|
||||
"@types/express": "^4.17.21",
|
||||
"typescript": "^5.3.0",
|
||||
"tsx": "^4.6.0",
|
||||
"jest": "^29.7.0",
|
||||
"eslint": "^8.55.0"
|
||||
}
|
||||
}
|
||||
```
|
||||
````
|
||||
|
||||
```json title="package.json"
|
||||
{
|
||||
"name": "@mcphub/server",
|
||||
"version": "2.1.0",
|
||||
"description": "Model Context Protocol Hub Server",
|
||||
"main": "dist/index.js",
|
||||
"scripts": {
|
||||
"start": "node dist/index.js",
|
||||
"dev": "tsx watch src/index.ts",
|
||||
"build": "tsc",
|
||||
"test": "jest",
|
||||
"test:watch": "jest --watch",
|
||||
"lint": "eslint src/**/*.ts",
|
||||
"migrate": "prisma migrate deploy"
|
||||
},
|
||||
"dependencies": {
|
||||
"@prisma/client": "^5.7.0",
|
||||
"express": "^4.18.2",
|
||||
"helmet": "^7.1.0",
|
||||
"cors": "^2.8.5",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"winston": "^3.11.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.10.0",
|
||||
"@types/express": "^4.17.21",
|
||||
"typescript": "^5.3.0",
|
||||
"tsx": "^4.6.0",
|
||||
"jest": "^29.7.0",
|
||||
"eslint": "^8.55.0"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Docker 配置
|
||||
|
||||
````md
|
||||
```dockerfile title="Dockerfile"
|
||||
FROM node:18-alpine AS builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# 复制依赖文件
|
||||
COPY package*.json ./
|
||||
COPY tsconfig.json ./
|
||||
|
||||
# 安装依赖
|
||||
RUN npm ci --only=production
|
||||
|
||||
# 复制源码
|
||||
COPY src/ ./src/
|
||||
|
||||
# 构建应用
|
||||
RUN npm run build
|
||||
|
||||
# 生产环境镜像
|
||||
FROM node:18-alpine AS production
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# 创建非 root 用户
|
||||
RUN addgroup -g 1001 -S nodejs
|
||||
RUN adduser -S mcphub -u 1001
|
||||
|
||||
# 复制构建产物
|
||||
COPY --from=builder /app/dist ./dist
|
||||
COPY --from=builder /app/node_modules ./node_modules
|
||||
COPY --from=builder /app/package*.json ./
|
||||
|
||||
# 设置权限
|
||||
USER mcphub
|
||||
|
||||
# 健康检查
|
||||
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
|
||||
CMD node dist/health-check.js
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
CMD ["node", "dist/index.js"]
|
||||
```
|
||||
````
|
||||
|
||||
```dockerfile title="Dockerfile"
|
||||
FROM node:18-alpine AS builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# 复制依赖文件
|
||||
COPY package*.json ./
|
||||
COPY tsconfig.json ./
|
||||
|
||||
# 安装依赖
|
||||
RUN npm ci --only=production
|
||||
|
||||
# 复制源码
|
||||
COPY src/ ./src/
|
||||
|
||||
# 构建应用
|
||||
RUN npm run build
|
||||
|
||||
# 生产环境镜像
|
||||
FROM node:18-alpine AS production
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# 创建非 root 用户
|
||||
RUN addgroup -g 1001 -S nodejs
|
||||
RUN adduser -S mcphub -u 1001
|
||||
|
||||
# 复制构建产物
|
||||
COPY --from=builder /app/dist ./dist
|
||||
COPY --from=builder /app/node_modules ./node_modules
|
||||
COPY --from=builder /app/package*.json ./
|
||||
|
||||
# 设置权限
|
||||
USER mcphub
|
||||
|
||||
# 健康检查
|
||||
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
|
||||
CMD node dist/health-check.js
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
CMD ["node", "dist/index.js"]
|
||||
```
|
||||
|
||||
## 终端命令
|
||||
|
||||
### Bash/Shell 命令
|
||||
|
||||
````md
|
||||
```bash
|
||||
# 克隆 MCPHub 仓库
|
||||
git clone https://github.com/mcphub/mcphub.git
|
||||
cd mcphub
|
||||
|
||||
# 安装依赖
|
||||
npm install
|
||||
|
||||
# 复制环境变量文件
|
||||
cp .env.example .env
|
||||
|
||||
# 设置数据库
|
||||
npm run db:setup
|
||||
|
||||
# 启动开发服务器
|
||||
npm run dev
|
||||
|
||||
# 构建生产版本
|
||||
npm run build
|
||||
|
||||
# 启动生产服务器
|
||||
npm start
|
||||
```
|
||||
````
|
||||
|
||||
```bash
|
||||
# 克隆 MCPHub 仓库
|
||||
git clone https://github.com/mcphub/mcphub.git
|
||||
cd mcphub
|
||||
|
||||
# 安装依赖
|
||||
npm install
|
||||
|
||||
# 复制环境变量文件
|
||||
cp .env.example .env
|
||||
|
||||
# 设置数据库
|
||||
npm run db:setup
|
||||
|
||||
# 启动开发服务器
|
||||
npm run dev
|
||||
|
||||
# 构建生产版本
|
||||
npm run build
|
||||
|
||||
# 启动生产服务器
|
||||
npm start
|
||||
```
|
||||
|
||||
### PowerShell 命令
|
||||
|
||||
````md
|
||||
```powershell
|
||||
# Windows PowerShell 安装步骤
|
||||
# 克隆仓库
|
||||
git clone https://github.com/mcphub/mcphub.git
|
||||
Set-Location mcphub
|
||||
|
||||
# 安装 Node.js 依赖
|
||||
npm install
|
||||
|
||||
# 复制环境变量文件
|
||||
Copy-Item .env.example .env
|
||||
|
||||
# 启动开发服务器
|
||||
npm run dev
|
||||
```
|
||||
````
|
||||
|
||||
```powershell
|
||||
# Windows PowerShell 安装步骤
|
||||
# 克隆仓库
|
||||
git clone https://github.com/mcphub/mcphub.git
|
||||
Set-Location mcphub
|
||||
|
||||
# 安装 Node.js 依赖
|
||||
npm install
|
||||
|
||||
# 复制环境变量文件
|
||||
Copy-Item .env.example .env
|
||||
|
||||
# 启动开发服务器
|
||||
npm run dev
|
||||
```
|
||||
|
||||
### Docker 命令
|
||||
|
||||
````md
|
||||
```bash
|
||||
# 使用 Docker 运行 MCPHub
|
||||
docker run -d \
|
||||
--name mcphub \
|
||||
-p 3000:3000 \
|
||||
-e NODE_ENV=production \
|
||||
-e DATABASE_URL=postgresql://user:pass@host:5432/mcphub \
|
||||
-e JWT_SECRET=your-secret-key \
|
||||
mcphub/server:latest
|
||||
|
||||
# 查看日志
|
||||
docker logs mcphub
|
||||
|
||||
# 进入容器
|
||||
docker exec -it mcphub sh
|
||||
|
||||
# 停止容器
|
||||
docker stop mcphub
|
||||
|
||||
# 使用 Docker Compose
|
||||
docker-compose up -d
|
||||
```
|
||||
````
|
||||
|
||||
```bash
|
||||
# 使用 Docker 运行 MCPHub
|
||||
docker run -d \
|
||||
--name mcphub \
|
||||
-p 3000:3000 \
|
||||
-e NODE_ENV=production \
|
||||
-e DATABASE_URL=postgresql://user:pass@host:5432/mcphub \
|
||||
-e JWT_SECRET=your-secret-key \
|
||||
mcphub/server:latest
|
||||
|
||||
# 查看日志
|
||||
docker logs mcphub
|
||||
|
||||
# 进入容器
|
||||
docker exec -it mcphub sh
|
||||
|
||||
# 停止容器
|
||||
docker stop mcphub
|
||||
|
||||
# 使用 Docker Compose
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
## API 请求示例
|
||||
|
||||
### cURL 命令
|
||||
|
||||
````md
|
||||
```bash
|
||||
# 创建新的 MCP 服务器
|
||||
curl -X POST https://api.mcphub.io/api/servers \
|
||||
-H "Authorization: Bearer YOUR_API_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"name": "AI Assistant Server",
|
||||
"endpoint": "https://ai.example.com",
|
||||
"capabilities": ["chat", "completion"],
|
||||
"groupId": "production"
|
||||
}'
|
||||
|
||||
# 获取服务器列表
|
||||
curl -X GET "https://api.mcphub.io/api/servers?limit=10&active=true" \
|
||||
-H "Authorization: Bearer YOUR_API_TOKEN"
|
||||
|
||||
# 更新服务器配置
|
||||
curl -X PUT https://api.mcphub.io/api/servers/server-123 \
|
||||
-H "Authorization: Bearer YOUR_API_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"name": "Updated AI Assistant",
|
||||
"active": true
|
||||
}'
|
||||
|
||||
# 删除服务器
|
||||
curl -X DELETE https://api.mcphub.io/api/servers/server-123 \
|
||||
-H "Authorization: Bearer YOUR_API_TOKEN"
|
||||
```
|
||||
````
|
||||
|
||||
```bash
|
||||
# 创建新的 MCP 服务器
|
||||
curl -X POST https://api.mcphub.io/api/servers \
|
||||
-H "Authorization: Bearer YOUR_API_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"name": "AI Assistant Server",
|
||||
"endpoint": "https://ai.example.com",
|
||||
"capabilities": ["chat", "completion"],
|
||||
"groupId": "production"
|
||||
}'
|
||||
|
||||
# 获取服务器列表
|
||||
curl -X GET "https://api.mcphub.io/api/servers?limit=10&active=true" \
|
||||
-H "Authorization: Bearer YOUR_API_TOKEN"
|
||||
|
||||
# 更新服务器配置
|
||||
curl -X PUT https://api.mcphub.io/api/servers/server-123 \
|
||||
-H "Authorization: Bearer YOUR_API_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"name": "Updated AI Assistant",
|
||||
"active": true
|
||||
}'
|
||||
|
||||
# 删除服务器
|
||||
curl -X DELETE https://api.mcphub.io/api/servers/server-123 \
|
||||
-H "Authorization: Bearer YOUR_API_TOKEN"
|
||||
```
|
||||
|
||||
### HTTP 请求示例
|
||||
|
||||
````md
|
||||
```http
|
||||
POST /api/servers HTTP/1.1
|
||||
Host: api.mcphub.io
|
||||
Authorization: Bearer YOUR_API_TOKEN
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"name": "AI Assistant Server",
|
||||
"endpoint": "https://ai.example.com",
|
||||
"capabilities": ["chat", "completion"],
|
||||
"groupId": "production"
|
||||
}
|
||||
```
|
||||
````
|
||||
|
||||
```http
|
||||
POST /api/servers HTTP/1.1
|
||||
Host: api.mcphub.io
|
||||
Authorization: Bearer YOUR_API_TOKEN
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"name": "AI Assistant Server",
|
||||
"endpoint": "https://ai.example.com",
|
||||
"capabilities": ["chat", "completion"],
|
||||
"groupId": "production"
|
||||
}
|
||||
```
|
||||
|
||||
## 数据库查询
|
||||
|
||||
### SQL 查询
|
||||
|
||||
````md
|
||||
```sql
|
||||
-- 查询活跃的 MCP 服务器
|
||||
SELECT
|
||||
id,
|
||||
name,
|
||||
endpoint,
|
||||
status,
|
||||
created_at
|
||||
FROM mcp_servers
|
||||
WHERE status = 'active'
|
||||
ORDER BY created_at DESC;
|
||||
|
||||
-- 统计每个组的服务器数量
|
||||
SELECT
|
||||
g.name as group_name,
|
||||
COUNT(s.id) as server_count
|
||||
FROM server_groups g
|
||||
LEFT JOIN mcp_servers s ON g.id = s.group_id
|
||||
GROUP BY g.id, g.name
|
||||
ORDER BY server_count DESC;
|
||||
|
||||
-- 查询最近的错误日志
|
||||
SELECT
|
||||
timestamp,
|
||||
level,
|
||||
message,
|
||||
metadata
|
||||
FROM logs
|
||||
WHERE level = 'error'
|
||||
AND timestamp >= NOW() - INTERVAL '1 hour'
|
||||
ORDER BY timestamp DESC
|
||||
LIMIT 50;
|
||||
```
|
||||
````
|
||||
|
||||
```sql
|
||||
-- 查询活跃的 MCP 服务器
|
||||
SELECT
|
||||
id,
|
||||
name,
|
||||
endpoint,
|
||||
status,
|
||||
created_at
|
||||
FROM mcp_servers
|
||||
WHERE status = 'active'
|
||||
ORDER BY created_at DESC;
|
||||
|
||||
-- 统计每个组的服务器数量
|
||||
SELECT
|
||||
g.name as group_name,
|
||||
COUNT(s.id) as server_count
|
||||
FROM server_groups g
|
||||
LEFT JOIN mcp_servers s ON g.id = s.group_id
|
||||
GROUP BY g.id, g.name
|
||||
ORDER BY server_count DESC;
|
||||
|
||||
-- 查询最近的错误日志
|
||||
SELECT
|
||||
timestamp,
|
||||
level,
|
||||
message,
|
||||
metadata
|
||||
FROM logs
|
||||
WHERE level = 'error'
|
||||
AND timestamp >= NOW() - INTERVAL '1 hour'
|
||||
ORDER BY timestamp DESC
|
||||
LIMIT 50;
|
||||
```
|
||||
|
||||
## 代码块最佳实践
|
||||
|
||||
### 1. 语言标识
|
||||
|
||||
始终为代码块指定正确的语言:
|
||||
|
||||
````md
|
||||
````javascript // ✅ 正确
|
||||
```js // ✅ 也可以
|
||||
```; // ❌ 避免无语言标识
|
||||
````
|
||||
````
|
||||
|
||||
### 2. 文件名标题
|
||||
|
||||
为配置文件和示例添加文件名:
|
||||
|
||||
````md
|
||||
```yaml title="docker-compose.yml"
|
||||
version: '3.8'
|
||||
services:
|
||||
mcphub:
|
||||
image: mcphub/server:latest
|
||||
```
|
||||
````
|
||||
|
||||
### 3. 突出显示重要行
|
||||
|
||||
使用行号高亮重要代码:
|
||||
|
||||
````md
|
||||
```javascript {3,7-9}
|
||||
const express = require('express');
|
||||
const app = express();
|
||||
const port = process.env.PORT || 3000; // 重要:端口配置
|
||||
|
||||
app.get('/health', (req, res) => {
|
||||
res.json({ status: 'ok' });
|
||||
});
|
||||
app.listen(port, () => {
|
||||
// 重要:服务器启动
|
||||
console.log(`Server running on port ${port}`);
|
||||
}); // 重要:结束
|
||||
```
|
||||
````
|
||||
|
||||
### 4. 代码注释
|
||||
|
||||
添加有意义的中文注释:
|
||||
|
||||
```javascript
|
||||
// 初始化 MCPHub 客户端
|
||||
const client = new MCPHubClient({
|
||||
endpoint: 'https://api.mcphub.io',
|
||||
apiKey: process.env.API_KEY,
|
||||
timeout: 30000, // 30 秒超时
|
||||
retries: 3, // 重试 3 次
|
||||
});
|
||||
|
||||
// 配置路由策略
|
||||
client.setRoutingStrategy({
|
||||
type: 'weighted', // 加权轮询
|
||||
healthCheck: true, // 启用健康检查
|
||||
fallback: 'round_robin', // 降级策略
|
||||
});
|
||||
```
|
||||
|
||||
### 5. 错误处理示例
|
||||
|
||||
展示完整的错误处理:
|
||||
|
||||
```javascript
|
||||
try {
|
||||
const server = await mcpClient.createServer({
|
||||
name: 'AI Assistant',
|
||||
endpoint: 'https://ai.example.com',
|
||||
});
|
||||
|
||||
console.log('服务器创建成功:', server.id);
|
||||
} catch (error) {
|
||||
if (error.code === 'DUPLICATE_SERVER') {
|
||||
console.log('服务器已存在,跳过创建');
|
||||
} else if (error.code === 'INVALID_ENDPOINT') {
|
||||
console.error('无效的端点地址:', error.message);
|
||||
} else {
|
||||
console.error('创建失败:', error.message);
|
||||
throw error; // 重新抛出未知错误
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 支持的语言
|
||||
|
||||
MCPHub 文档支持以下编程语言的语法高亮:
|
||||
|
||||
- **JavaScript/TypeScript**: `javascript`, `js`, `typescript`, `ts`
|
||||
- **Python**: `python`, `py`
|
||||
- **Shell/Bash**: `bash`, `shell`, `sh`
|
||||
- **PowerShell**: `powershell`, `ps1`
|
||||
- **SQL**: `sql`, `postgresql`, `mysql`
|
||||
- **YAML**: `yaml`, `yml`
|
||||
- **JSON**: `json`
|
||||
- **XML**: `xml`
|
||||
- **HTML**: `html`
|
||||
- **CSS**: `css`
|
||||
- **Dockerfile**: `dockerfile`
|
||||
- **Go**: `go`
|
||||
- **Rust**: `rust`
|
||||
- **Java**: `java`
|
||||
- **C#**: `csharp`, `cs`
|
||||
- **PHP**: `php`
|
||||
- **Ruby**: `ruby`
|
||||
- **HTTP**: `http`
|
||||
- **Markdown**: `markdown`, `md`
|
||||
|
||||
`````
|
||||
|
||||
### 使用三个反引号
|
||||
|
||||
````md
|
||||
```javascript
|
||||
console.log('Hello World');
|
||||
`````
|
||||
|
||||
`````
|
||||
|
||||
### 语法高亮
|
||||
|
||||
我们使用 [Prism](https://prismjs.com/) 来语法高亮显示。Prism 支持 [各种编程语言](https://prismjs.com/#supported-languages)。
|
||||
|
||||
要添加语法高亮显示,请在代码块的第一行指定语言。
|
||||
|
||||
````md
|
||||
```python
|
||||
def hello():
|
||||
print("Hello World")
|
||||
```
|
||||
`````
|
||||
|
||||
```python
|
||||
def hello():
|
||||
print("Hello World")
|
||||
```
|
||||
|
||||
## 代码组
|
||||
|
||||
<CodeGroup>
|
||||
|
||||
```bash npm
|
||||
npm i mintlify
|
||||
```
|
||||
|
||||
```bash yarn
|
||||
yarn add mintlify
|
||||
```
|
||||
|
||||
```bash pnpm
|
||||
pnpm add mintlify
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
|
||||
`CodeGroup` 允许您将多个代码块组合在一起,并为它们提供选项卡。
|
||||
|
||||
````md
|
||||
<CodeGroup>
|
||||
|
||||
```bash npm
|
||||
npm i mintlify
|
||||
```
|
||||
|
||||
```bash yarn
|
||||
yarn add mintlify
|
||||
```
|
||||
|
||||
```bash pnpm
|
||||
pnpm add mintlify
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
````
|
||||
|
||||
### 代码标题
|
||||
|
||||
您也可以为代码块设置标题:
|
||||
|
||||
```javascript hello.js
|
||||
const hello = 'world';
|
||||
console.log(hello);
|
||||
```
|
||||
|
||||
````md
|
||||
```javascript hello.js
|
||||
const hello = 'world';
|
||||
console.log(hello);
|
||||
```
|
||||
````
|
||||
134
docs/zh/essentials/images.mdx
Normal file
@@ -0,0 +1,134 @@
|
||||
---
|
||||
title: '图片和视频'
|
||||
description: '在您的文档中添加图片和视频'
|
||||
---
|
||||
|
||||
## 图片
|
||||
|
||||
### 使用 Markdown
|
||||
|
||||
您可以使用标准的 Markdown 语法添加图片:
|
||||
|
||||

|
||||
|
||||
```md
|
||||

|
||||
```
|
||||
|
||||
### 使用 HTML
|
||||
|
||||
您也可以使用原始 HTML 获得更多自定义选项:
|
||||
|
||||
<img height="200" src="/images/hero-light.png" />
|
||||
|
||||
```html
|
||||
<img height="200" src="/images/hero-light.png" />
|
||||
```
|
||||
|
||||
### 图片组件
|
||||
|
||||
使用内置的 `<img>` 组件来显示响应式的明暗主题图片:
|
||||
|
||||
<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" />
|
||||
|
||||
```jsx
|
||||
<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"
|
||||
/>
|
||||
```
|
||||
|
||||
## 图片缩放
|
||||
|
||||
您可以使图片在点击时可缩放(类似于中等缩放)使用 `zoom` 属性。
|
||||
|
||||
<img src="/images/hero-light.png" alt="可缩放" zoom />
|
||||
|
||||
```jsx
|
||||
<img src="/images/hero-light.png" alt="可缩放" zoom />
|
||||
```
|
||||
|
||||
## 嵌入视频
|
||||
|
||||
### YouTube
|
||||
|
||||
<iframe
|
||||
width="560"
|
||||
height="315"
|
||||
src="https://www.youtube.com/embed/4KzFe50RQkQ"
|
||||
title="YouTube 视频播放器"
|
||||
frameborder="0"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||
allowfullscreen
|
||||
></iframe>
|
||||
|
||||
```html
|
||||
<iframe
|
||||
width="560"
|
||||
height="315"
|
||||
src="https://www.youtube.com/embed/4KzFe50RQkQ"
|
||||
title="YouTube 视频播放器"
|
||||
frameborder="0"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||
allowfullscreen
|
||||
></iframe>
|
||||
```
|
||||
|
||||
### Loom
|
||||
|
||||
<iframe
|
||||
src="https://www.loom.com/embed/9019ef5b27ae417798d65b41749227ac"
|
||||
frameborder="0"
|
||||
webkitallowfullscreen
|
||||
mozallowfullscreen
|
||||
allowfullscreen
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
}}
|
||||
></iframe>
|
||||
|
||||
```html
|
||||
<iframe
|
||||
src="https://www.loom.com/embed/9019ef5b27ae417798d65b41749227ac"
|
||||
frameborder="0"
|
||||
webkitallowfullscreen
|
||||
mozallowfullscreen
|
||||
allowfullscreen
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
}}
|
||||
></iframe>
|
||||
```
|
||||
|
||||
## 图片最佳实践
|
||||
|
||||
### 大小优化
|
||||
|
||||
- 保持图片尺寸在合理范围内(通常不超过 1000px 宽度)
|
||||
- 使用适当的图片格式(PNG 用于图标,JPG 用于照片)
|
||||
- 考虑使用现代格式如 WebP 来减少文件大小
|
||||
|
||||
### 无障碍性
|
||||
|
||||
- 始终包含描述性的 `alt` 文本
|
||||
- 确保图片在各种屏幕尺寸下都能正常显示
|
||||
|
||||
### 组织
|
||||
|
||||
- 将图片存储在 `/images` 或 `/assets` 文件夹中
|
||||
- 使用描述性的文件名
|
||||
412
docs/zh/essentials/markdown.mdx
Normal file
@@ -0,0 +1,412 @@
|
||||
---
|
||||
title: 'Markdown 语法'
|
||||
description: 'MCPHub 文档的 Markdown 编写指南和最佳实践'
|
||||
---
|
||||
|
||||
## 标题
|
||||
|
||||
在 MCPHub 文档中,每个页面应该只使用一个 `#` 标题,它会自动成为页面标题。
|
||||
|
||||
```md
|
||||
# MCP 服务器配置指南
|
||||
|
||||
## 快速开始
|
||||
|
||||
### 安装依赖
|
||||
|
||||
#### 系统要求
|
||||
|
||||
##### Node.js 版本
|
||||
|
||||
###### 推荐版本
|
||||
```
|
||||
|
||||
<ResponseExample>
|
||||
|
||||
# 标题 1
|
||||
|
||||
## 标题 2
|
||||
|
||||
### 标题 3
|
||||
|
||||
#### 标题 4
|
||||
|
||||
##### 标题 5
|
||||
|
||||
###### 标题 6
|
||||
|
||||
</ResponseExample>
|
||||
|
||||
## 文本格式
|
||||
|
||||
### 基本格式
|
||||
|
||||
MCPHub 文档支持标准的 Markdown 文本格式:
|
||||
|
||||
```md
|
||||
**粗体文本** - 用于强调重要概念
|
||||
_斜体文本_ - 用于强调或引用
|
||||
`行内代码` - 用于命令、配置键或代码片段
|
||||
~~删除线~~ - 用于标记过时的内容
|
||||
```
|
||||
|
||||
**粗体文本** - 用于强调重要概念
|
||||
_斜体文本_ - 用于强调或引用
|
||||
`行内代码` - 用于命令、配置键或代码片段
|
||||
~~删除线~~ - 用于标记过时的内容
|
||||
|
||||
### 链接
|
||||
|
||||
#### 内部链接
|
||||
|
||||
链接到其他文档页面:
|
||||
|
||||
```md
|
||||
查看 [服务器配置指南](/zh/configuration/mcp-settings) 获取详细信息。
|
||||
```
|
||||
|
||||
查看 [服务器配置指南](/zh/configuration/mcp-settings) 获取详细信息。
|
||||
|
||||
#### 外部链接
|
||||
|
||||
```md
|
||||
访问 [Model Context Protocol 官网](https://modelcontextprotocol.io) 了解更多。
|
||||
```
|
||||
|
||||
访问 [Model Context Protocol 官网](https://modelcontextprotocol.io) 了解更多。
|
||||
|
||||
## 列表
|
||||
|
||||
### 无序列表
|
||||
|
||||
适用于功能列表、要求等:
|
||||
|
||||
```md
|
||||
MCPHub 主要功能:
|
||||
|
||||
- 智能路由分发
|
||||
- 服务器组管理
|
||||
- 实时监控
|
||||
- 身份认证
|
||||
- JWT 令牌
|
||||
- API 密钥
|
||||
- OAuth 2.0
|
||||
```
|
||||
|
||||
MCPHub 主要功能:
|
||||
|
||||
- 智能路由分发
|
||||
- 服务器组管理
|
||||
- 实时监控
|
||||
- 身份认证
|
||||
- JWT 令牌
|
||||
- API 密钥
|
||||
- OAuth 2.0
|
||||
|
||||
### 有序列表
|
||||
|
||||
适用于步骤说明、安装指南等:
|
||||
|
||||
```md
|
||||
快速部署步骤:
|
||||
|
||||
1. 克隆仓库
|
||||
2. 安装依赖
|
||||
3. 配置环境变量
|
||||
4. 启动服务
|
||||
5. 验证部署
|
||||
```
|
||||
|
||||
快速部署步骤:
|
||||
|
||||
1. 克隆仓库
|
||||
2. 安装依赖
|
||||
3. 配置环境变量
|
||||
4. 启动服务
|
||||
5. 验证部署
|
||||
|
||||
## 代码块
|
||||
|
||||
### 基本代码块
|
||||
|
||||
````md
|
||||
```javascript
|
||||
// MCPHub 客户端初始化
|
||||
const mcpClient = new MCPClient({
|
||||
endpoint: 'https://api.mcphub.io',
|
||||
apiKey: process.env.MCPHUB_API_KEY,
|
||||
});
|
||||
```
|
||||
````
|
||||
|
||||
```javascript
|
||||
// MCPHub 客户端初始化
|
||||
const mcpClient = new MCPClient({
|
||||
endpoint: 'https://api.mcphub.io',
|
||||
apiKey: process.env.MCPHUB_API_KEY,
|
||||
});
|
||||
```
|
||||
|
||||
### 配置文件示例
|
||||
|
||||
````md
|
||||
```yaml title="docker-compose.yml"
|
||||
version: '3.8'
|
||||
services:
|
||||
mcphub:
|
||||
image: mcphub/server:latest
|
||||
ports:
|
||||
- '3000:3000'
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
- DATABASE_URL=postgresql://user:pass@db:5432/mcphub
|
||||
```
|
||||
````
|
||||
|
||||
```yaml title="docker-compose.yml"
|
||||
version: '3.8'
|
||||
services:
|
||||
mcphub:
|
||||
image: mcphub/server:latest
|
||||
ports:
|
||||
- '3000:3000'
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
- DATABASE_URL=postgresql://user:pass@db:5432/mcphub
|
||||
```
|
||||
|
||||
### 终端命令
|
||||
|
||||
````md
|
||||
```bash
|
||||
# 安装 MCPHub CLI
|
||||
npm install -g @mcphub/cli
|
||||
|
||||
# 初始化项目
|
||||
mcphub init my-project
|
||||
|
||||
# 启动开发服务器
|
||||
mcphub dev
|
||||
```
|
||||
````
|
||||
|
||||
```bash
|
||||
# 安装 MCPHub CLI
|
||||
npm install -g @mcphub/cli
|
||||
|
||||
# 初始化项目
|
||||
mcphub init my-project
|
||||
|
||||
# 启动开发服务器
|
||||
mcphub dev
|
||||
```
|
||||
|
||||
## 表格
|
||||
|
||||
### 基本表格
|
||||
|
||||
```md
|
||||
| 功能 | 开源版 | 企业版 |
|
||||
| ------------ | ------ | ------ |
|
||||
| 基础路由 | ✅ | ✅ |
|
||||
| 智能负载均衡 | ❌ | ✅ |
|
||||
| 高级监控 | ❌ | ✅ |
|
||||
| 24/7 支持 | ❌ | ✅ |
|
||||
```
|
||||
|
||||
| 功能 | 开源版 | 企业版 |
|
||||
| ------------ | ------ | ------ |
|
||||
| 基础路由 | ✅ | ✅ |
|
||||
| 智能负载均衡 | ❌ | ✅ |
|
||||
| 高级监控 | ❌ | ✅ |
|
||||
| 24/7 支持 | ❌ | ✅ |
|
||||
|
||||
### API 参数表格
|
||||
|
||||
```md
|
||||
| 参数名 | 类型 | 必需 | 描述 |
|
||||
| ---------- | ------- | ---- | ---------------------- |
|
||||
| `serverId` | string | 是 | 服务器唯一标识符 |
|
||||
| `groupId` | string | 否 | 服务器组 ID |
|
||||
| `active` | boolean | 否 | 是否激活(默认:true) |
|
||||
```
|
||||
|
||||
| 参数名 | 类型 | 必需 | 描述 |
|
||||
| ---------- | ------- | ---- | ---------------------- |
|
||||
| `serverId` | string | 是 | 服务器唯一标识符 |
|
||||
| `groupId` | string | 否 | 服务器组 ID |
|
||||
| `active` | boolean | 否 | 是否激活(默认:true) |
|
||||
|
||||
## 引用块
|
||||
|
||||
### 信息提示
|
||||
|
||||
```md
|
||||
> 📝 **提示**
|
||||
> 在生产环境中部署前,请确保已正确配置所有环境变量。
|
||||
```
|
||||
|
||||
> 📝 **提示**
|
||||
> 在生产环境中部署前,请确保已正确配置所有环境变量。
|
||||
|
||||
### 警告信息
|
||||
|
||||
```md
|
||||
> ⚠️ **警告**
|
||||
> 修改核心配置可能会影响系统稳定性,请谨慎操作。
|
||||
```
|
||||
|
||||
> ⚠️ **警告**
|
||||
> 修改核心配置可能会影响系统稳定性,请谨慎操作。
|
||||
|
||||
## 任务列表
|
||||
|
||||
```md
|
||||
- [x] 完成服务器配置
|
||||
- [x] 设置数据库连接
|
||||
- [ ] 配置负载均衡
|
||||
- [ ] 设置监控告警
|
||||
- [ ] 编写单元测试
|
||||
```
|
||||
|
||||
- [x] 完成服务器配置
|
||||
- [x] 设置数据库连接
|
||||
- [ ] 配置负载均衡
|
||||
- [ ] 设置监控告警
|
||||
- [ ] 编写单元测试
|
||||
|
||||
## 水平分割线
|
||||
|
||||
用于分隔不同的内容部分:
|
||||
|
||||
```md
|
||||
## 第一部分
|
||||
|
||||
内容...
|
||||
|
||||
---
|
||||
|
||||
## 第二部分
|
||||
|
||||
更多内容...
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 转义字符
|
||||
|
||||
当需要显示 Markdown 特殊字符时:
|
||||
|
||||
```md
|
||||
\*这不是斜体\*
|
||||
\`这不是代码\`
|
||||
\[这不是链接\]
|
||||
```
|
||||
|
||||
\*这不是斜体\*
|
||||
\`这不是代码\`
|
||||
\[这不是链接\]
|
||||
|
||||
## MCPHub 文档特定约定
|
||||
|
||||
### 配置项格式
|
||||
|
||||
环境变量和配置项使用特定格式:
|
||||
|
||||
```md
|
||||
设置 `MCPHUB_PORT` 环境变量为 `3000`。
|
||||
```
|
||||
|
||||
设置 `MCPHUB_PORT` 环境变量为 `3000`。
|
||||
|
||||
### API 端点格式
|
||||
|
||||
```md
|
||||
`GET /api/servers/{id}` - 获取服务器详情
|
||||
```
|
||||
|
||||
`GET /api/servers/{id}` - 获取服务器详情
|
||||
|
||||
### 版本标记
|
||||
|
||||
```md
|
||||
该功能在 v2.1.0+ 版本中可用。
|
||||
```
|
||||
|
||||
该功能在 v2.1.0+ 版本中可用。
|
||||
|
||||
## 最佳实践
|
||||
|
||||
1. **标题层级**:保持清晰的标题层级结构
|
||||
2. **代码示例**:为所有代码块指定语言
|
||||
3. **链接检查**:确保所有内部链接有效
|
||||
4. **图片描述**:为图片添加有意义的 alt 文本
|
||||
5. **一致性**:在整个文档中保持术语和格式一致
|
||||
|
||||
### 文档模板示例
|
||||
|
||||
````md
|
||||
---
|
||||
title: '功能名称'
|
||||
description: '简短的功能描述'
|
||||
---
|
||||
|
||||
## 概述
|
||||
|
||||
简要介绍该功能的用途和重要性。
|
||||
|
||||
## 快速开始
|
||||
|
||||
### 前提条件
|
||||
|
||||
- 系统要求
|
||||
- 依赖软件
|
||||
|
||||
### 安装步骤
|
||||
|
||||
1. 第一步
|
||||
2. 第二步
|
||||
3. 第三步
|
||||
|
||||
```bash
|
||||
# 示例命令
|
||||
npm install example
|
||||
```
|
||||
|
||||
## 配置
|
||||
|
||||
### 基本配置
|
||||
|
||||
| 配置项 | 类型 | 描述 |
|
||||
| --------- | ------ | -------- |
|
||||
| `option1` | string | 选项描述 |
|
||||
|
||||
### 高级配置
|
||||
|
||||
详细的配置说明...
|
||||
|
||||
## 示例
|
||||
|
||||
### 基本用法
|
||||
|
||||
```javascript
|
||||
// 代码示例
|
||||
const example = new Example();
|
||||
```
|
||||
|
||||
### 高级用法
|
||||
|
||||
更复杂的使用场景...
|
||||
|
||||
## 故障排除
|
||||
|
||||
### 常见问题
|
||||
|
||||
**问题**:描述问题
|
||||
**解决方案**:解决步骤
|
||||
|
||||
## 参考资料
|
||||
|
||||
- [相关文档链接](/link)
|
||||
- [外部资源](https://example.com)
|
||||
````
|
||||
596
docs/zh/essentials/navigation.mdx
Normal file
@@ -0,0 +1,596 @@
|
||||
---
|
||||
title: '导航配置'
|
||||
description: 'MCPHub 文档的导航结构配置指南'
|
||||
---
|
||||
|
||||
## 基础导航
|
||||
|
||||
MCPHub 文档的导航在 `docs.json` 文件中配置。基本导航结构包含组和页面:
|
||||
|
||||
```json title="docs.json"
|
||||
{
|
||||
"navigation": [
|
||||
{
|
||||
"group": "快速开始",
|
||||
"pages": ["zh/index", "zh/quickstart"]
|
||||
},
|
||||
{
|
||||
"group": "开发指南",
|
||||
"pages": [
|
||||
"zh/development/getting-started",
|
||||
"zh/development/api-integration",
|
||||
"zh/development/testing"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "配置管理",
|
||||
"pages": [
|
||||
"zh/configuration/environment-variables",
|
||||
"zh/configuration/mcp-settings",
|
||||
"zh/configuration/docker-setup",
|
||||
"zh/configuration/nginx"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## 标签导航
|
||||
|
||||
当您的文档有多个主要部分时,可以使用标签来组织内容。
|
||||
|
||||
```json docs.json
|
||||
{
|
||||
"navigation": {
|
||||
"tabs": [
|
||||
{
|
||||
"tab": "指南",
|
||||
"groups": [
|
||||
{
|
||||
"group": "基础",
|
||||
"pages": ["basics/introduction"]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tab": "API 参考",
|
||||
"groups": [
|
||||
{
|
||||
"group": "端点",
|
||||
"pages": ["api/users", "api/products"]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 页面引用
|
||||
|
||||
### 文件路径引用
|
||||
|
||||
最常见的方式是通过文件路径引用页面(不包含 `.mdx` 扩展名):
|
||||
|
||||
```json
|
||||
{
|
||||
"pages": ["quickstart", "advanced/configuration"]
|
||||
}
|
||||
```
|
||||
|
||||
### 外部链接
|
||||
|
||||
您也可以在导航中包含外部链接:
|
||||
|
||||
```json
|
||||
{
|
||||
"pages": [
|
||||
"introduction",
|
||||
{
|
||||
"page": "GitHub",
|
||||
"href": "https://github.com"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## 分组
|
||||
|
||||
### 基本分组
|
||||
|
||||
每个组都有一个名称和页面列表:
|
||||
|
||||
```json
|
||||
{
|
||||
"group": "API 基础",
|
||||
"pages": ["api/authentication", "api/errors", "api/rate-limits"]
|
||||
}
|
||||
```
|
||||
|
||||
### 分组版本控制
|
||||
|
||||
您可以为组指定版本:
|
||||
|
||||
```json
|
||||
{
|
||||
"group": "API v2",
|
||||
"version": "v2.0",
|
||||
"pages": ["api/v2/users"]
|
||||
}
|
||||
```
|
||||
|
||||
## 全局导航元素
|
||||
|
||||
### 锚点
|
||||
|
||||
在所有页面上显示的持久链接:
|
||||
|
||||
```json docs.json
|
||||
{
|
||||
"navigation": {
|
||||
"global": {
|
||||
"anchors": [
|
||||
{
|
||||
"anchor": "API 参考",
|
||||
"href": "/api-reference",
|
||||
"icon": "square-terminal"
|
||||
},
|
||||
{
|
||||
"anchor": "社区",
|
||||
"href": "https://community.example.com",
|
||||
"icon": "discord"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 导航栏
|
||||
|
||||
配置顶部导航栏的链接:
|
||||
|
||||
```json docs.json
|
||||
{
|
||||
"navbar": {
|
||||
"links": [
|
||||
{
|
||||
"label": "支持",
|
||||
"href": "mailto:support@example.com"
|
||||
}
|
||||
],
|
||||
"primary": {
|
||||
"type": "button",
|
||||
"label": "仪表板",
|
||||
"href": "https://dashboard.example.com"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 分层导航结构
|
||||
|
||||
### 多级导航
|
||||
|
||||
MCPHub 文档支持多级分层导航:
|
||||
|
||||
```json title="docs.json"
|
||||
{
|
||||
"navigation": [
|
||||
{
|
||||
"group": "核心功能",
|
||||
"pages": [
|
||||
{
|
||||
"group": "服务器管理",
|
||||
"pages": [
|
||||
"zh/features/server-management",
|
||||
"zh/features/server-health",
|
||||
"zh/features/server-scaling"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "智能路由",
|
||||
"pages": [
|
||||
"zh/features/smart-routing",
|
||||
"zh/features/load-balancing",
|
||||
"zh/features/failover"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 条件导航
|
||||
|
||||
根据用户权限或版本显示不同的导航项:
|
||||
|
||||
```json title="docs.json"
|
||||
{
|
||||
"navigation": [
|
||||
{
|
||||
"group": "API 参考",
|
||||
"pages": [
|
||||
"zh/api-reference/introduction",
|
||||
"zh/api-reference/authentication",
|
||||
{
|
||||
"group": "端点",
|
||||
"pages": [
|
||||
"zh/api-reference/endpoint/get",
|
||||
"zh/api-reference/endpoint/create",
|
||||
"zh/api-reference/endpoint/delete",
|
||||
"zh/api-reference/endpoint/webhook"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "企业功能",
|
||||
"icon": "crown",
|
||||
"version": "enterprise",
|
||||
"pages": ["zh/enterprise/sso", "zh/enterprise/audit-logs", "zh/enterprise/compliance"]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## 标签导航
|
||||
|
||||
对于多产品或多语言文档,使用标签组织内容:
|
||||
|
||||
```json title="docs.json"
|
||||
{
|
||||
"tabs": [
|
||||
{
|
||||
"name": "文档",
|
||||
"url": "https://docs.mcphub.io"
|
||||
},
|
||||
{
|
||||
"name": "API",
|
||||
"url": "https://api.mcphub.io"
|
||||
},
|
||||
{
|
||||
"name": "SDK",
|
||||
"url": "https://sdk.mcphub.io"
|
||||
}
|
||||
],
|
||||
"navigation": {
|
||||
"文档": [
|
||||
{
|
||||
"group": "开始使用",
|
||||
"pages": ["zh/index", "zh/quickstart"]
|
||||
}
|
||||
],
|
||||
"API": [
|
||||
{
|
||||
"group": "API 参考",
|
||||
"pages": ["zh/api-reference/introduction", "zh/api-reference/authentication"]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 导航图标
|
||||
|
||||
为导航项添加图标以提高可读性:
|
||||
|
||||
```json title="docs.json"
|
||||
{
|
||||
"navigation": [
|
||||
{
|
||||
"group": "快速开始",
|
||||
"icon": "rocket",
|
||||
"pages": ["zh/index", "zh/quickstart"]
|
||||
},
|
||||
{
|
||||
"group": "配置",
|
||||
"icon": "gear",
|
||||
"pages": ["zh/configuration/environment-variables", "zh/configuration/mcp-settings"]
|
||||
},
|
||||
{
|
||||
"group": "监控",
|
||||
"icon": "chart-line",
|
||||
"pages": ["zh/features/monitoring", "zh/features/analytics"]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 支持的图标
|
||||
|
||||
MCPHub 文档支持以下图标库的图标:
|
||||
|
||||
- **Heroicons**: `hero-icon-name`
|
||||
- **Font Awesome**: `fa-icon-name`
|
||||
- **Feather**: `feather-icon-name`
|
||||
- **Lucide**: `lucide-icon-name`
|
||||
|
||||
常用图标示例:
|
||||
|
||||
| 功能 | 图标 | 代码 |
|
||||
| ---- | ---- | ------------- |
|
||||
| 首页 | 🏠 | `"home"` |
|
||||
| 设置 | ⚙️ | `"gear"` |
|
||||
| API | 🔌 | `"plug"` |
|
||||
| 安全 | 🔒 | `"lock"` |
|
||||
| 监控 | 📊 | `"chart-bar"` |
|
||||
| 文档 | 📖 | `"book"` |
|
||||
| 开发 | 💻 | `"code"` |
|
||||
|
||||
## 外部链接
|
||||
|
||||
在导航中包含外部资源链接:
|
||||
|
||||
```json title="docs.json"
|
||||
{
|
||||
"navigation": [
|
||||
{
|
||||
"group": "资源",
|
||||
"pages": [
|
||||
{
|
||||
"name": "GitHub 仓库",
|
||||
"url": "https://github.com/mcphub/mcphub",
|
||||
"icon": "github"
|
||||
},
|
||||
{
|
||||
"name": "Discord 社区",
|
||||
"url": "https://discord.gg/mcphub",
|
||||
"icon": "discord"
|
||||
},
|
||||
{
|
||||
"name": "状态页面",
|
||||
"url": "https://status.mcphub.io",
|
||||
"icon": "status"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## 导航排序
|
||||
|
||||
### 自动排序
|
||||
|
||||
默认情况下,导航项按字母顺序排列。可以通过文件名前缀控制排序:
|
||||
|
||||
```
|
||||
zh/
|
||||
├── 01-index.mdx
|
||||
├── 02-quickstart.mdx
|
||||
├── development/
|
||||
│ ├── 01-getting-started.mdx
|
||||
│ ├── 02-api-integration.mdx
|
||||
│ └── 03-testing.mdx
|
||||
└── configuration/
|
||||
├── 01-environment-variables.mdx
|
||||
├── 02-mcp-settings.mdx
|
||||
└── 03-docker-setup.mdx
|
||||
```
|
||||
|
||||
### 手动排序
|
||||
|
||||
在 `docs.json` 中明确指定顺序:
|
||||
|
||||
```json title="docs.json"
|
||||
{
|
||||
"navigation": [
|
||||
{
|
||||
"group": "核心概念",
|
||||
"pages": [
|
||||
"zh/concepts/introduction",
|
||||
"zh/concepts/architecture",
|
||||
"zh/concepts/mcp-protocol",
|
||||
"zh/concepts/routing"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## 隐藏导航项
|
||||
|
||||
### 草稿页面
|
||||
|
||||
使用 `draft: true` 隐藏未完成的页面:
|
||||
|
||||
```yaml title="draft-page.mdx"
|
||||
---
|
||||
title: '开发中的功能'
|
||||
description: '此功能正在开发中'
|
||||
draft: true
|
||||
---
|
||||
```
|
||||
|
||||
### 条件显示
|
||||
|
||||
根据用户角色或环境显示导航:
|
||||
|
||||
```json title="docs.json"
|
||||
{
|
||||
"navigation": [
|
||||
{
|
||||
"group": "管理功能",
|
||||
"hidden": "user",
|
||||
"pages": ["zh/admin/user-management", "zh/admin/system-settings"]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## 导航元数据
|
||||
|
||||
### 页面元数据
|
||||
|
||||
在页面头部添加导航相关的元数据:
|
||||
|
||||
```yaml title="page.mdx"
|
||||
---
|
||||
title: '服务器管理'
|
||||
description: 'MCPHub 服务器管理功能详解'
|
||||
icon: 'server'
|
||||
order: 1
|
||||
hidden: false
|
||||
version: '2.0+'
|
||||
tags: ['管理', '服务器', '配置']
|
||||
---
|
||||
```
|
||||
|
||||
### 组元数据
|
||||
|
||||
为导航组添加描述和图标:
|
||||
|
||||
```json title="docs.json"
|
||||
{
|
||||
"navigation": [
|
||||
{
|
||||
"group": "API 参考",
|
||||
"icon": "api",
|
||||
"description": "完整的 API 接口文档",
|
||||
"version": "v2",
|
||||
"pages": ["zh/api-reference/introduction"]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## 搜索优化
|
||||
|
||||
### 搜索关键词
|
||||
|
||||
为页面添加搜索关键词:
|
||||
|
||||
```yaml title="page.mdx"
|
||||
---
|
||||
title: 'Docker 部署'
|
||||
description: '使用 Docker 部署 MCPHub'
|
||||
keywords: ['docker', '部署', '容器', '生产环境']
|
||||
searchable: true
|
||||
---
|
||||
```
|
||||
|
||||
### 搜索权重
|
||||
|
||||
控制页面在搜索结果中的权重:
|
||||
|
||||
```yaml title="important-page.mdx"
|
||||
---
|
||||
title: '快速开始'
|
||||
description: '5 分钟快速部署 MCPHub'
|
||||
searchWeight: 10
|
||||
featured: true
|
||||
---
|
||||
```
|
||||
|
||||
## 面包屑导航
|
||||
|
||||
自动生成面包屑导航:
|
||||
|
||||
```json title="docs.json"
|
||||
{
|
||||
"breadcrumbs": {
|
||||
"enabled": true,
|
||||
"separator": "›",
|
||||
"home": "首页"
|
||||
},
|
||||
"navigation": [
|
||||
{
|
||||
"group": "配置管理",
|
||||
"pages": ["zh/configuration/environment-variables"]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
显示效果:`首页 › 配置管理 › 环境变量`
|
||||
|
||||
## 导航最佳实践
|
||||
|
||||
### 1. 逻辑分组
|
||||
|
||||
按功能和用户需求逻辑分组:
|
||||
|
||||
```json
|
||||
{
|
||||
"navigation": [
|
||||
{
|
||||
"group": "新手指南",
|
||||
"pages": ["introduction", "quickstart", "first-server"]
|
||||
},
|
||||
{
|
||||
"group": "进阶配置",
|
||||
"pages": ["advanced-routing", "scaling", "monitoring"]
|
||||
},
|
||||
{
|
||||
"group": "参考文档",
|
||||
"pages": ["api-reference", "cli-reference", "troubleshooting"]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 渐进式学习路径
|
||||
|
||||
设计符合学习曲线的导航结构:
|
||||
|
||||
1. **入门** → 快速开始、基础概念
|
||||
2. **实践** → 配置、部署、集成
|
||||
3. **进阶** → 优化、监控、故障排除
|
||||
4. **参考** → API 文档、CLI 手册
|
||||
|
||||
### 3. 移动端友好
|
||||
|
||||
确保导航在移动设备上的可用性:
|
||||
|
||||
```json title="docs.json"
|
||||
{
|
||||
"navigation": [
|
||||
{
|
||||
"group": "快速开始",
|
||||
"collapsed": false,
|
||||
"pages": ["zh/index", "zh/quickstart"]
|
||||
},
|
||||
{
|
||||
"group": "详细文档",
|
||||
"collapsed": true,
|
||||
"pages": ["zh/advanced/..."]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 4. 国际化支持
|
||||
|
||||
为多语言文档配置导航:
|
||||
|
||||
```json title="docs.json"
|
||||
{
|
||||
"i18n": {
|
||||
"defaultLocale": "zh",
|
||||
"locales": ["zh", "en"]
|
||||
},
|
||||
"navigation": {
|
||||
"zh": [
|
||||
{
|
||||
"group": "快速开始",
|
||||
"pages": ["zh/index", "zh/quickstart"]
|
||||
}
|
||||
],
|
||||
"en": [
|
||||
{
|
||||
"group": "Getting Started",
|
||||
"pages": ["en/index", "en/quickstart"]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 5. 性能优化
|
||||
|
||||
- 使用懒加载减少初始加载时间
|
||||
- 合理设置导航深度(建议不超过 3 层)
|
||||
- 避免过多的外部链接
|
||||
- 定期清理无效的导航项
|
||||
144
docs/zh/essentials/reusable-snippets.mdx
Normal file
@@ -0,0 +1,144 @@
|
||||
---
|
||||
title: '可重用代码片段'
|
||||
description: '学习如何创建和使用代码片段来保持文档的一致性'
|
||||
---
|
||||
|
||||
## 什么是代码片段?
|
||||
|
||||
代码片段允许您在文档的多个位置重用内容块。这有助于保持一致性并减少重复内容的维护工作。
|
||||
|
||||
## 创建代码片段
|
||||
|
||||
代码片段存储在 `snippets/` 文件夹中,使用 `.mdx` 扩展名。
|
||||
|
||||
### 基本代码片段
|
||||
|
||||
创建 `snippets/api-key-setup.mdx`:
|
||||
|
||||
```md
|
||||
获取您的 API 密钥:
|
||||
|
||||
1. 登录到您的仪表板
|
||||
2. 导航到 **设置** > **API 密钥**
|
||||
3. 点击 **生成新密钥**
|
||||
4. 复制密钥并安全地存储
|
||||
```
|
||||
|
||||
### 带参数的代码片段
|
||||
|
||||
您可以创建接受参数的动态代码片段。创建 `snippets/code-example.mdx`:
|
||||
|
||||
````jsx
|
||||
<CodeGroup>
|
||||
|
||||
```bash {props.packageManager}
|
||||
{props.packageManager} install {props.packageName}
|
||||
````
|
||||
|
||||
</CodeGroup>
|
||||
```
|
||||
|
||||
## 使用代码片段
|
||||
|
||||
### 基本使用
|
||||
|
||||
使用 `<Snippet>` 组件来包含代码片段:
|
||||
|
||||
```jsx
|
||||
<Snippet file="api-key-setup.mdx" />
|
||||
```
|
||||
|
||||
<Snippet file="snippet-intro.mdx" />
|
||||
|
||||
### 带参数使用
|
||||
|
||||
```jsx
|
||||
<Snippet file="code-example.mdx" packageManager="npm" packageName="mintlify" />
|
||||
```
|
||||
|
||||
## 代码片段最佳实践
|
||||
|
||||
### 文件组织
|
||||
|
||||
```
|
||||
snippets/
|
||||
├── setup/
|
||||
│ ├── installation.mdx
|
||||
│ └── configuration.mdx
|
||||
├── examples/
|
||||
│ ├── basic-usage.mdx
|
||||
│ └── advanced-usage.mdx
|
||||
└── common/
|
||||
├── prerequisites.mdx
|
||||
└── troubleshooting.mdx
|
||||
```
|
||||
|
||||
### 命名约定
|
||||
|
||||
- 使用描述性文件名
|
||||
- 使用连字符分隔单词
|
||||
- 按主题分组到子文件夹
|
||||
|
||||
### 内容指导原则
|
||||
|
||||
1. **保持简洁** - 代码片段应该是独立的内容块
|
||||
2. **避免硬编码** - 对可变内容使用参数
|
||||
3. **文档化参数** - 在代码片段中注释必需的参数
|
||||
|
||||
### 参数文档
|
||||
|
||||
在代码片段文件的顶部记录所需参数:
|
||||
|
||||
```md
|
||||
<!--
|
||||
参数:
|
||||
- packageManager: 包管理器名称(npm, yarn, pnpm)
|
||||
- packageName: 要安装的包名称
|
||||
-->
|
||||
|
||||
安装说明...
|
||||
```
|
||||
|
||||
## 高级代码片段
|
||||
|
||||
### 条件内容
|
||||
|
||||
您可以使用条件逻辑来根据参数显示不同的内容:
|
||||
|
||||
```jsx
|
||||
{
|
||||
props.framework === 'react' && <div>React 特定的内容...</div>;
|
||||
}
|
||||
|
||||
{
|
||||
props.framework === 'vue' && <div>Vue 特定的内容...</div>;
|
||||
}
|
||||
```
|
||||
|
||||
### 嵌套代码片段
|
||||
|
||||
代码片段可以包含其他代码片段:
|
||||
|
||||
```jsx
|
||||
<Snippet file="common/prerequisites.mdx" />
|
||||
|
||||
## 安装步骤
|
||||
|
||||
<Snippet file="setup/installation.mdx" framework={props.framework} />
|
||||
```
|
||||
|
||||
## 维护代码片段
|
||||
|
||||
### 版本控制
|
||||
|
||||
当更新代码片段时:
|
||||
|
||||
1. 考虑向后兼容性
|
||||
2. 更新所有使用该代码片段的页面
|
||||
3. 测试更改在所有上下文中的效果
|
||||
|
||||
### 重构检查清单
|
||||
|
||||
- [ ] 确认所有参数仍然有效
|
||||
- [ ] 验证代码片段在所有使用位置正确渲染
|
||||
- [ ] 更新相关文档
|
||||
172
docs/zh/essentials/settings.mdx
Normal file
@@ -0,0 +1,172 @@
|
||||
---
|
||||
title: '设置'
|
||||
description: '了解如何配置您的文档'
|
||||
---
|
||||
|
||||
## 全局配置
|
||||
|
||||
所有的全局配置都在项目根目录的 `docs.json` 文件中设置。
|
||||
|
||||
### 名称
|
||||
|
||||
在配置的顶层设置文档的名称。
|
||||
|
||||
```json docs.json
|
||||
{
|
||||
"name": "Mintlify 文档"
|
||||
}
|
||||
```
|
||||
|
||||
### Logo
|
||||
|
||||
您可以显示浅色和深色模式的 logo。
|
||||
|
||||
```json docs.json
|
||||
{
|
||||
"logo": {
|
||||
"light": "/logo/light.svg",
|
||||
"dark": "/logo/dark.svg"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Favicon
|
||||
|
||||
```json docs.json
|
||||
{
|
||||
"favicon": "/favicon.ico"
|
||||
}
|
||||
```
|
||||
|
||||
### 颜色
|
||||
|
||||
自定义文档的主色调以匹配您的品牌。
|
||||
|
||||
```json docs.json
|
||||
{
|
||||
"colors": {
|
||||
"primary": "#9563FF",
|
||||
"light": "#AE87FF",
|
||||
"dark": "#9563FF"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
<Tip>设置一种颜色系统,通过仅更改主色调来协调您文档的配色方案。</Tip>
|
||||
|
||||
### 导航
|
||||
|
||||
您的侧边栏导航在 `navigation` 字段中设置。文档页面必须嵌套在组下,组必须嵌套在导航下。
|
||||
|
||||
```json docs.json
|
||||
{
|
||||
"navigation": [
|
||||
{
|
||||
"group": "开始使用",
|
||||
"pages": ["introduction", "quickstart", "development"]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
#### 标签
|
||||
|
||||
您可以将页面分组为不同的标签。当您想要将概念或 API 参考组织到不同的部分时,这很有用。
|
||||
|
||||
```json docs.json
|
||||
{
|
||||
"navigation": {
|
||||
"tabs": [
|
||||
{
|
||||
"tab": "主要",
|
||||
"groups": [
|
||||
{
|
||||
"group": "开始使用",
|
||||
"pages": ["introduction"]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tab": "API 参考",
|
||||
"groups": [
|
||||
{
|
||||
"group": "端点",
|
||||
"pages": ["api-reference/users"]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 页脚
|
||||
|
||||
您可以在 `footer` 字段中配置页脚链接。
|
||||
|
||||
```json docs.json
|
||||
{
|
||||
"footer": {
|
||||
"socials": {
|
||||
"website": "https://mintlify.com",
|
||||
"github": "https://github.com/mintlify",
|
||||
"slack": "https://mintlify.com/community"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 搜索
|
||||
|
||||
您可以通过多种方式配置搜索,包括替换默认搜索或添加搜索锚点。
|
||||
|
||||
```json docs.json
|
||||
{
|
||||
"search": {
|
||||
"prompt": "搜索..."
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 页面配置
|
||||
|
||||
页面配置在每个 MDX 文件顶部的 frontmatter 中设置。
|
||||
|
||||
### 标题和描述
|
||||
|
||||
```md
|
||||
---
|
||||
title: '介绍'
|
||||
description: '欢迎来到我们的产品!'
|
||||
---
|
||||
```
|
||||
|
||||
### 侧边栏标题
|
||||
|
||||
```md
|
||||
---
|
||||
sidebarTitle: '主页'
|
||||
---
|
||||
```
|
||||
|
||||
设置不同于页面标题的侧边栏标题。
|
||||
|
||||
### 图标
|
||||
|
||||
```md
|
||||
---
|
||||
icon: 'star'
|
||||
---
|
||||
```
|
||||
|
||||
为侧边栏中的页面设置 [FontAwesome](https://fontawesome.com/search?s=solid&m=free) 图标。
|
||||
|
||||
### 模式
|
||||
|
||||
```md
|
||||
---
|
||||
mode: 'wide'
|
||||
---
|
||||
```
|
||||
|
||||
设置页面的显示模式。选项包括 `"default"` 和 `"wide"`。
|
||||
330
docs/zh/features/authentication.mdx
Normal file
@@ -0,0 +1,330 @@
|
||||
---
|
||||
title: '身份认证与安全'
|
||||
description: '为 MCPHub 配置身份认证和安全设置'
|
||||
---
|
||||
|
||||
## 概述
|
||||
|
||||
MCPHub 提供灵活的身份认证机制来保护您的 MCP 服务器管理平台。系统支持多种身份认证方法和基于角色的访问控制。
|
||||
|
||||
## 身份认证方法
|
||||
|
||||
### 基于环境变量的认证
|
||||
|
||||
使用环境变量配置基础认证:
|
||||
|
||||
```bash
|
||||
# 基础认证凭据
|
||||
AUTH_USERNAME=admin
|
||||
AUTH_PASSWORD=your-secure-password
|
||||
|
||||
# JWT 设置
|
||||
JWT_SECRET=your-jwt-secret-key
|
||||
JWT_EXPIRES_IN=24h
|
||||
```
|
||||
|
||||
### 数据库认证
|
||||
|
||||
对于生产环境部署,启用基于数据库的用户管理:
|
||||
|
||||
```json
|
||||
{
|
||||
"auth": {
|
||||
"provider": "database",
|
||||
"database": {
|
||||
"url": "postgresql://user:pass@localhost:5432/mcphub",
|
||||
"userTable": "users"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 用户管理
|
||||
|
||||
### 创建用户
|
||||
|
||||
通过管理界面或 API 创建用户:
|
||||
|
||||
```bash
|
||||
# 通过 API
|
||||
curl -X POST http://localhost:3000/api/auth/users \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer $ADMIN_TOKEN" \
|
||||
-d '{
|
||||
"username": "newuser",
|
||||
"email": "user@example.com",
|
||||
"password": "securepassword",
|
||||
"role": "user"
|
||||
}'
|
||||
```
|
||||
|
||||
### 用户角色
|
||||
|
||||
MCPHub 支持基于角色的访问控制:
|
||||
|
||||
- **管理员**: 完整系统访问权限、用户管理、服务器配置
|
||||
- **管理者**: 服务器管理、组管理、监控
|
||||
- **用户**: 在分配组内的基本服务器访问权限
|
||||
- **查看者**: 对分配资源的只读访问权限
|
||||
|
||||
## 基于组的访问控制
|
||||
|
||||
### 将用户分配到组
|
||||
|
||||
```bash
|
||||
# 添加用户到组
|
||||
curl -X POST http://localhost:3000/api/groups/{groupId}/users \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-d '{"userId": "user123"}'
|
||||
```
|
||||
|
||||
### 组权限
|
||||
|
||||
配置组级别权限:
|
||||
|
||||
```json
|
||||
{
|
||||
"groupId": "dev-team",
|
||||
"permissions": {
|
||||
"servers": ["read", "write", "execute"],
|
||||
"tools": ["read", "execute"],
|
||||
"logs": ["read"],
|
||||
"config": ["read"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## API 认证
|
||||
|
||||
### JWT 令牌认证
|
||||
|
||||
```javascript
|
||||
// 获取认证令牌
|
||||
const response = await fetch('/api/auth/login', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
username: 'your-username',
|
||||
password: 'your-password',
|
||||
}),
|
||||
});
|
||||
|
||||
const { token } = await response.json();
|
||||
|
||||
// 在后续请求中使用令牌
|
||||
const protectedResponse = await fetch('/api/servers', {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### API 密钥认证
|
||||
|
||||
为系统集成生成 API 密钥:
|
||||
|
||||
```bash
|
||||
# 生成新的 API 密钥
|
||||
curl -X POST http://localhost:3000/api/auth/api-keys \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-d '{
|
||||
"name": "Integration Key",
|
||||
"permissions": ["servers:read", "servers:write"],
|
||||
"expiresAt": "2024-12-31T23:59:59.000Z"
|
||||
}'
|
||||
```
|
||||
|
||||
## 安全设置
|
||||
|
||||
### HTTPS 配置
|
||||
|
||||
为生产环境启用 HTTPS:
|
||||
|
||||
```nginx
|
||||
server {
|
||||
listen 443 ssl http2;
|
||||
server_name mcphub.example.com;
|
||||
|
||||
ssl_certificate /path/to/certificate.crt;
|
||||
ssl_certificate_key /path/to/private.key;
|
||||
|
||||
location / {
|
||||
proxy_pass http://localhost:3000;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 会话安全
|
||||
|
||||
配置安全的会话设置:
|
||||
|
||||
```javascript
|
||||
// 会话配置
|
||||
{
|
||||
"session": {
|
||||
"secret": "your-session-secret",
|
||||
"secure": true, // 生产环境中需要 HTTPS
|
||||
"httpOnly": true,
|
||||
"maxAge": 86400000, // 24 小时
|
||||
"sameSite": "strict"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 速率限制
|
||||
|
||||
实施 API 速率限制:
|
||||
|
||||
```javascript
|
||||
{
|
||||
"rateLimit": {
|
||||
"windowMs": 900000, // 15 分钟
|
||||
"max": 100, // 每个 IP 限制 100 个请求
|
||||
"message": "请求过于频繁,请稍后再试",
|
||||
"standardHeaders": true,
|
||||
"legacyHeaders": false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 多因素认证 (MFA)
|
||||
|
||||
### 启用 TOTP
|
||||
|
||||
为管理员帐户启用基于时间的一次性密码:
|
||||
|
||||
```bash
|
||||
# 启用 MFA
|
||||
curl -X POST http://localhost:3000/api/auth/mfa/enable \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-d '{
|
||||
"type": "totp",
|
||||
"appName": "MCPHub"
|
||||
}'
|
||||
```
|
||||
|
||||
### 验证 MFA 代码
|
||||
|
||||
```javascript
|
||||
// 登录时验证 MFA
|
||||
const loginResponse = await fetch('/api/auth/login', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
username: 'admin',
|
||||
password: 'password',
|
||||
mfaCode: '123456', // 来自认证器应用的 6 位数字
|
||||
}),
|
||||
});
|
||||
```
|
||||
|
||||
## 审计日志
|
||||
|
||||
### 启用审计日志
|
||||
|
||||
跟踪所有认证和授权事件:
|
||||
|
||||
```json
|
||||
{
|
||||
"audit": {
|
||||
"enabled": true,
|
||||
"logLevel": "info",
|
||||
"events": [
|
||||
"login",
|
||||
"logout",
|
||||
"password_change",
|
||||
"role_change",
|
||||
"permission_change",
|
||||
"server_access",
|
||||
"config_change"
|
||||
],
|
||||
"storage": {
|
||||
"type": "database",
|
||||
"retention": "90d"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 查看审计日志
|
||||
|
||||
```bash
|
||||
# 获取审计日志
|
||||
curl -X GET "http://localhost:3000/api/audit/logs?startDate=2024-01-01&endDate=2024-01-31" \
|
||||
-H "Authorization: Bearer $TOKEN"
|
||||
```
|
||||
|
||||
## 密码策略
|
||||
|
||||
### 配置密码要求
|
||||
|
||||
```json
|
||||
{
|
||||
"passwordPolicy": {
|
||||
"minLength": 12,
|
||||
"requireUppercase": true,
|
||||
"requireLowercase": true,
|
||||
"requireNumbers": true,
|
||||
"requireSpecialChars": true,
|
||||
"preventCommonPasswords": true,
|
||||
"preventReuse": 5, // 防止重复使用最近 5 个密码
|
||||
"maxAge": 7776000 // 90 天后过期
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 故障排除
|
||||
|
||||
### 常见认证问题
|
||||
|
||||
1. **JWT 令牌过期**
|
||||
|
||||
```bash
|
||||
# 检查令牌有效期
|
||||
curl -X GET http://localhost:3000/api/auth/verify \
|
||||
-H "Authorization: Bearer $TOKEN"
|
||||
```
|
||||
|
||||
2. **权限被拒绝**
|
||||
|
||||
```bash
|
||||
# 检查用户权限
|
||||
curl -X GET http://localhost:3000/api/auth/permissions \
|
||||
-H "Authorization: Bearer $TOKEN"
|
||||
```
|
||||
|
||||
3. **会话问题**
|
||||
- 清除浏览器 cookies
|
||||
- 检查会话配置
|
||||
- 验证服务器时间同步
|
||||
|
||||
### 调试认证流程
|
||||
|
||||
启用调试日志:
|
||||
|
||||
```bash
|
||||
# 设置环境变量
|
||||
export DEBUG=auth:*
|
||||
export LOG_LEVEL=debug
|
||||
|
||||
# 启动服务器
|
||||
npm start
|
||||
```
|
||||
|
||||
## 安全最佳实践
|
||||
|
||||
1. **定期更新凭据**: 定期轮换 JWT 密钥和 API 密钥
|
||||
2. **最小权限原则**: 只授予用户执行其任务所需的最小权限
|
||||
3. **监控异常活动**: 设置警报以检测可疑的登录模式
|
||||
4. **备份配置**: 定期备份认证配置和用户数据
|
||||
5. **安全更新**: 保持 MCPHub 和依赖项的最新状态
|
||||
|
||||
更多安全配置选项,请参阅 [环境变量配置](/zh/configuration/environment-variables) 和 [Docker 设置](/zh/configuration/docker-setup) 文档。
|
||||
567
docs/zh/features/group-management.mdx
Normal file
@@ -0,0 +1,567 @@
|
||||
---
|
||||
title: '组管理'
|
||||
description: '组织用户和服务器为逻辑组,实现高效的访问控制'
|
||||
---
|
||||
|
||||
## 概述
|
||||
|
||||
MCPHub 的组管理系统允许您将用户和服务器组织成逻辑组,从而简化权限管理和访问控制。组提供了一种灵活的方式来管理大规模部署中的资源。
|
||||
|
||||
## 创建组
|
||||
|
||||
### 通过仪表板
|
||||
|
||||
1. **导航到组部分**: 在主仪表板中点击"组"
|
||||
2. **点击"创建组"**: 开始组创建流程
|
||||
3. **填写组详细信息**:
|
||||
- **组名**: 唯一的组标识符
|
||||
- **显示名称**: 用户友好的组名称
|
||||
- **描述**: 组的目的和范围
|
||||
- **父组**: 可选的层次结构
|
||||
|
||||
### 通过 API
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:3000/api/groups \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-d '{
|
||||
"name": "development-team",
|
||||
"displayName": "开发团队",
|
||||
"description": "前端和后端开发人员",
|
||||
"parentGroup": null,
|
||||
"settings": {
|
||||
"autoAssign": false,
|
||||
"maxMembers": 50,
|
||||
"requireApproval": true
|
||||
}
|
||||
}'
|
||||
```
|
||||
|
||||
### 通过配置文件
|
||||
|
||||
在 `groups.json` 中定义组:
|
||||
|
||||
```json
|
||||
{
|
||||
"groups": {
|
||||
"dev-team": {
|
||||
"displayName": "开发团队",
|
||||
"description": "应用程序开发人员",
|
||||
"permissions": {
|
||||
"servers": ["read", "write", "execute"],
|
||||
"tools": ["read", "execute"],
|
||||
"logs": ["read"]
|
||||
},
|
||||
"members": ["user1", "user2"],
|
||||
"servers": ["dev-server-1", "dev-server-2"]
|
||||
},
|
||||
"qa-team": {
|
||||
"displayName": "质量保证团队",
|
||||
"description": "测试和质量保证",
|
||||
"permissions": {
|
||||
"servers": ["read", "execute"],
|
||||
"tools": ["read", "execute"],
|
||||
"logs": ["read"]
|
||||
},
|
||||
"members": ["qa1", "qa2"],
|
||||
"servers": ["test-server", "staging-server"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 组层次结构
|
||||
|
||||
### 嵌套组
|
||||
|
||||
创建组层次结构以实现更好的组织:
|
||||
|
||||
```json
|
||||
{
|
||||
"groups": {
|
||||
"engineering": {
|
||||
"displayName": "工程部",
|
||||
"description": "所有工程团队",
|
||||
"children": ["frontend", "backend", "devops"]
|
||||
},
|
||||
"frontend": {
|
||||
"displayName": "前端团队",
|
||||
"parent": "engineering",
|
||||
"servers": ["frontend-dev", "frontend-staging"]
|
||||
},
|
||||
"backend": {
|
||||
"displayName": "后端团队",
|
||||
"parent": "engineering",
|
||||
"servers": ["api-server", "database-server"]
|
||||
},
|
||||
"devops": {
|
||||
"displayName": "运维团队",
|
||||
"parent": "engineering",
|
||||
"servers": ["monitoring", "deployment"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 继承权限
|
||||
|
||||
子组从父组继承权限:
|
||||
|
||||
```bash
|
||||
# 检查继承的权限
|
||||
curl -X GET http://localhost:3000/api/groups/frontend/permissions?inherited=true \
|
||||
-H "Authorization: Bearer $TOKEN"
|
||||
```
|
||||
|
||||
## 用户管理
|
||||
|
||||
### 添加用户到组
|
||||
|
||||
```bash
|
||||
# 添加单个用户
|
||||
curl -X POST http://localhost:3000/api/groups/dev-team/members \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-d '{
|
||||
"userId": "user123",
|
||||
"role": "member"
|
||||
}'
|
||||
|
||||
# 批量添加用户
|
||||
curl -X POST http://localhost:3000/api/groups/dev-team/members/bulk \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-d '{
|
||||
"users": [
|
||||
{"userId": "user1", "role": "member"},
|
||||
{"userId": "user2", "role": "admin"},
|
||||
{"userId": "user3", "role": "member"}
|
||||
]
|
||||
}'
|
||||
```
|
||||
|
||||
### 用户角色
|
||||
|
||||
每个组内支持不同的用户角色:
|
||||
|
||||
- **组管理员**: 完整的组管理权限
|
||||
- **成员**: 标准组访问权限
|
||||
- **查看者**: 只读访问权限
|
||||
- **访客**: 有限的临时访问权限
|
||||
|
||||
### 移除用户
|
||||
|
||||
```bash
|
||||
# 从组中移除用户
|
||||
curl -X DELETE http://localhost:3000/api/groups/dev-team/members/user123 \
|
||||
-H "Authorization: Bearer $TOKEN"
|
||||
```
|
||||
|
||||
## 服务器分配
|
||||
|
||||
### 分配服务器到组
|
||||
|
||||
```bash
|
||||
# 分配单个服务器
|
||||
curl -X POST http://localhost:3000/api/groups/dev-team/servers \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-d '{
|
||||
"serverId": "my-server",
|
||||
"permissions": ["read", "write", "execute"]
|
||||
}'
|
||||
|
||||
# 批量分配服务器
|
||||
curl -X POST http://localhost:3000/api/groups/dev-team/servers/bulk \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-d '{
|
||||
"servers": [
|
||||
{
|
||||
"serverId": "server1",
|
||||
"permissions": ["read", "write"]
|
||||
},
|
||||
{
|
||||
"serverId": "server2",
|
||||
"permissions": ["read", "execute"]
|
||||
}
|
||||
]
|
||||
}'
|
||||
```
|
||||
|
||||
### 服务器权限级别
|
||||
|
||||
为每个组-服务器对定义细粒度权限:
|
||||
|
||||
```json
|
||||
{
|
||||
"serverId": "my-server",
|
||||
"permissions": {
|
||||
"execute": {
|
||||
"allowed": true,
|
||||
"tools": ["filesystem", "web-search"],
|
||||
"restrictions": {
|
||||
"maxRequests": 100,
|
||||
"timeWindow": "1h"
|
||||
}
|
||||
},
|
||||
"configure": {
|
||||
"allowed": false
|
||||
},
|
||||
"logs": {
|
||||
"allowed": true,
|
||||
"level": ["info", "warn", "error"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 权限管理
|
||||
|
||||
### 组权限模型
|
||||
|
||||
```json
|
||||
{
|
||||
"groupId": "dev-team",
|
||||
"permissions": {
|
||||
"servers": {
|
||||
"create": false,
|
||||
"read": true,
|
||||
"update": true,
|
||||
"delete": false,
|
||||
"execute": true
|
||||
},
|
||||
"tools": {
|
||||
"filesystem": {
|
||||
"read": true,
|
||||
"write": true,
|
||||
"paths": ["/app/data", "/tmp"]
|
||||
},
|
||||
"web-search": {
|
||||
"enabled": true,
|
||||
"maxQueries": 50
|
||||
}
|
||||
},
|
||||
"monitoring": {
|
||||
"viewLogs": true,
|
||||
"viewMetrics": true,
|
||||
"exportData": false
|
||||
},
|
||||
"administration": {
|
||||
"manageUsers": false,
|
||||
"manageServers": true,
|
||||
"manageGroups": false
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 动态权限
|
||||
|
||||
基于条件的动态权限:
|
||||
|
||||
```json
|
||||
{
|
||||
"permissions": {
|
||||
"servers": {
|
||||
"execute": {
|
||||
"condition": "time.hour >= 9 && time.hour <= 17",
|
||||
"message": "服务器执行仅在工作时间内允许"
|
||||
}
|
||||
},
|
||||
"tools": {
|
||||
"filesystem": {
|
||||
"write": {
|
||||
"condition": "user.role === 'admin' || group.name === 'senior-devs'",
|
||||
"message": "写入权限需要管理员或高级开发者角色"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 配额管理
|
||||
|
||||
### 设置组配额
|
||||
|
||||
```bash
|
||||
curl -X PUT http://localhost:3000/api/groups/dev-team/quotas \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-d '{
|
||||
"requests": {
|
||||
"daily": 1000,
|
||||
"monthly": 30000
|
||||
},
|
||||
"storage": {
|
||||
"maxSize": "10GB",
|
||||
"retention": "30d"
|
||||
},
|
||||
"compute": {
|
||||
"maxConcurrentRequests": 10,
|
||||
"maxExecutionTime": "5m"
|
||||
}
|
||||
}'
|
||||
```
|
||||
|
||||
### 监控配额使用
|
||||
|
||||
```bash
|
||||
# 获取当前配额使用情况
|
||||
curl -X GET http://localhost:3000/api/groups/dev-team/quotas/usage \
|
||||
-H "Authorization: Bearer $TOKEN"
|
||||
```
|
||||
|
||||
响应示例:
|
||||
|
||||
```json
|
||||
{
|
||||
"groupId": "dev-team",
|
||||
"period": "2024-01-01T00:00:00Z",
|
||||
"usage": {
|
||||
"requests": {
|
||||
"used": 750,
|
||||
"limit": 1000,
|
||||
"remaining": 250
|
||||
},
|
||||
"storage": {
|
||||
"used": "7.2GB",
|
||||
"limit": "10GB",
|
||||
"remaining": "2.8GB"
|
||||
},
|
||||
"compute": {
|
||||
"currentConcurrent": 3,
|
||||
"maxConcurrent": 10,
|
||||
"avgExecutionTime": "2m 15s"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 组策略
|
||||
|
||||
### 定义组策略
|
||||
|
||||
```json
|
||||
{
|
||||
"groupId": "dev-team",
|
||||
"policies": {
|
||||
"security": {
|
||||
"requireMFA": false,
|
||||
"sessionTimeout": "8h",
|
||||
"ipWhitelist": ["192.168.1.0/24", "10.0.0.0/8"]
|
||||
},
|
||||
"usage": {
|
||||
"allowWeekendAccess": true,
|
||||
"restrictHolidays": false,
|
||||
"maxSessionDuration": "12h"
|
||||
},
|
||||
"data": {
|
||||
"encryptionRequired": true,
|
||||
"dataRetention": "90d",
|
||||
"exportAllowed": false
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 策略继承
|
||||
|
||||
```bash
|
||||
# 应用策略模板
|
||||
curl -X POST http://localhost:3000/api/groups/dev-team/policies/apply \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-d '{
|
||||
"template": "development-team-template",
|
||||
"overrides": {
|
||||
"security.sessionTimeout": "4h"
|
||||
}
|
||||
}'
|
||||
```
|
||||
|
||||
## 自动化组管理
|
||||
|
||||
### 自动用户分配
|
||||
|
||||
基于属性自动分配用户:
|
||||
|
||||
```json
|
||||
{
|
||||
"autoAssignment": {
|
||||
"enabled": true,
|
||||
"rules": [
|
||||
{
|
||||
"condition": "user.department === '开发'",
|
||||
"action": {
|
||||
"addToGroup": "dev-team",
|
||||
"role": "member"
|
||||
}
|
||||
},
|
||||
{
|
||||
"condition": "user.title.includes('高级')",
|
||||
"action": {
|
||||
"addToGroup": "senior-devs",
|
||||
"role": "admin"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 定时任务
|
||||
|
||||
```bash
|
||||
# 创建定时清理任务
|
||||
curl -X POST http://localhost:3000/api/groups/dev-team/jobs \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-d '{
|
||||
"name": "cleanup-inactive-users",
|
||||
"schedule": "0 2 * * *",
|
||||
"action": "removeInactiveUsers",
|
||||
"params": {
|
||||
"inactiveDays": 30
|
||||
}
|
||||
}'
|
||||
```
|
||||
|
||||
## 组通知
|
||||
|
||||
### 配置通知
|
||||
|
||||
```json
|
||||
{
|
||||
"groupId": "dev-team",
|
||||
"notifications": {
|
||||
"channels": {
|
||||
"email": {
|
||||
"enabled": true,
|
||||
"recipients": ["team-lead@company.com"]
|
||||
},
|
||||
"slack": {
|
||||
"enabled": true,
|
||||
"webhook": "https://hooks.slack.com/...",
|
||||
"channel": "#dev-team"
|
||||
}
|
||||
},
|
||||
"events": ["userJoined", "userLeft", "serverAdded", "quotaExceeded", "securityAlert"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 发送组通知
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:3000/api/groups/dev-team/notifications \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-d '{
|
||||
"type": "announcement",
|
||||
"title": "维护通知",
|
||||
"message": "服务器将在今晚 10 点进行维护",
|
||||
"priority": "high",
|
||||
"channels": ["email", "slack"]
|
||||
}'
|
||||
```
|
||||
|
||||
## 组分析
|
||||
|
||||
### 使用统计
|
||||
|
||||
```bash
|
||||
# 获取组使用统计
|
||||
curl -X GET http://localhost:3000/api/groups/dev-team/analytics \
|
||||
-H "Authorization: Bearer $TOKEN"
|
||||
```
|
||||
|
||||
响应示例:
|
||||
|
||||
```json
|
||||
{
|
||||
"groupId": "dev-team",
|
||||
"period": "30d",
|
||||
"stats": {
|
||||
"activeUsers": 12,
|
||||
"totalRequests": 15750,
|
||||
"avgResponseTime": "250ms",
|
||||
"errorRate": "0.5%",
|
||||
"mostUsedTools": [
|
||||
{ "name": "filesystem", "usage": 8500 },
|
||||
{ "name": "web-search", "usage": 4200 },
|
||||
{ "name": "database", "usage": 3050 }
|
||||
],
|
||||
"peakUsageHours": [9, 10, 14, 15, 16]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 生成报告
|
||||
|
||||
```bash
|
||||
# 生成月度报告
|
||||
curl -X POST http://localhost:3000/api/groups/dev-team/reports \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-d '{
|
||||
"type": "monthly",
|
||||
"format": "pdf",
|
||||
"includeDetails": true,
|
||||
"recipients": ["manager@company.com"]
|
||||
}'
|
||||
```
|
||||
|
||||
## 故障排除
|
||||
|
||||
### 常见问题
|
||||
|
||||
1. **用户无法访问组资源**
|
||||
|
||||
```bash
|
||||
# 检查用户组成员身份
|
||||
curl -X GET http://localhost:3000/api/users/user123/groups \
|
||||
-H "Authorization: Bearer $TOKEN"
|
||||
```
|
||||
|
||||
2. **权限配置错误**
|
||||
|
||||
```bash
|
||||
# 验证权限设置
|
||||
curl -X GET http://localhost:3000/api/groups/dev-team/permissions/validate \
|
||||
-H "Authorization: Bearer $TOKEN"
|
||||
```
|
||||
|
||||
3. **配额超限**
|
||||
```bash
|
||||
# 检查配额状态
|
||||
curl -X GET http://localhost:3000/api/groups/dev-team/quotas/status \
|
||||
-H "Authorization: Bearer $TOKEN"
|
||||
```
|
||||
|
||||
### 调试组权限
|
||||
|
||||
启用权限调试:
|
||||
|
||||
```bash
|
||||
# 调试用户权限
|
||||
curl -X GET http://localhost:3000/api/debug/permissions \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-d '{
|
||||
"userId": "user123",
|
||||
"resource": "server:my-server",
|
||||
"action": "execute"
|
||||
}'
|
||||
```
|
||||
|
||||
## 最佳实践
|
||||
|
||||
1. **组织结构**: 使用层次化组结构镜像您的组织架构
|
||||
2. **权限最小化**: 只授予执行任务所需的最小权限
|
||||
3. **定期审核**: 定期审核组成员身份和权限
|
||||
4. **自动化**: 使用自动化规则减少手动管理开销
|
||||
5. **监控**: 设置监控和警报以跟踪组活动
|
||||
|
||||
有关更多信息,请参阅 [身份认证与安全](/zh/features/authentication) 和 [监控](/zh/features/monitoring) 文档。
|
||||
613
docs/zh/features/monitoring.mdx
Normal file
@@ -0,0 +1,613 @@
|
||||
---
|
||||
title: '监控与分析'
|
||||
description: '全面监控 MCP 服务器性能、健康状况和使用情况'
|
||||
---
|
||||
|
||||
## 概述
|
||||
|
||||
MCPHub 提供全面的监控和分析功能,帮助您跟踪 MCP 服务器的性能、健康状况和使用模式。系统提供实时指标、历史分析和智能警报。
|
||||
|
||||
## 实时监控
|
||||
|
||||
### 仪表板概览
|
||||
|
||||
主监控仪表板显示关键指标:
|
||||
|
||||
```bash
|
||||
# 访问监控仪表板
|
||||
curl -X GET http://localhost:3000/api/monitoring/dashboard \
|
||||
-H "Authorization: Bearer $TOKEN"
|
||||
```
|
||||
|
||||
响应示例:
|
||||
|
||||
```json
|
||||
{
|
||||
"overview": {
|
||||
"totalServers": 12,
|
||||
"activeServers": 10,
|
||||
"totalRequests": 15420,
|
||||
"avgResponseTime": "245ms",
|
||||
"errorRate": "0.3%",
|
||||
"uptime": "99.9%"
|
||||
},
|
||||
"realtime": {
|
||||
"requestsPerMinute": 85,
|
||||
"activeConnections": 156,
|
||||
"memoryUsage": "68%",
|
||||
"cpuUsage": "42%"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 服务器健康状态
|
||||
|
||||
```bash
|
||||
# 获取所有服务器状态
|
||||
curl -X GET http://localhost:3000/api/monitoring/servers/health \
|
||||
-H "Authorization: Bearer $TOKEN"
|
||||
```
|
||||
|
||||
## 性能指标
|
||||
|
||||
### 响应时间监控
|
||||
|
||||
```json
|
||||
{
|
||||
"responseTime": {
|
||||
"metrics": [
|
||||
{
|
||||
"serverId": "server-1",
|
||||
"average": "180ms",
|
||||
"p50": "150ms",
|
||||
"p95": "300ms",
|
||||
"p99": "500ms"
|
||||
}
|
||||
],
|
||||
"alerts": {
|
||||
"slowResponse": {
|
||||
"threshold": "1s",
|
||||
"current": "180ms",
|
||||
"status": "normal"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 吞吐量分析
|
||||
|
||||
```bash
|
||||
# 获取吞吐量统计
|
||||
curl -X GET http://localhost:3000/api/monitoring/throughput \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-G -d "timeRange=1h" -d "granularity=5m"
|
||||
```
|
||||
|
||||
响应示例:
|
||||
|
||||
```json
|
||||
{
|
||||
"timeRange": "1h",
|
||||
"granularity": "5m",
|
||||
"data": [
|
||||
{
|
||||
"timestamp": "2024-01-01T12:00:00Z",
|
||||
"totalRequests": 450,
|
||||
"successfulRequests": 448,
|
||||
"failedRequests": 2,
|
||||
"avgResponseTime": "230ms"
|
||||
}
|
||||
],
|
||||
"summary": {
|
||||
"peakThroughput": 520,
|
||||
"averageThroughput": 412,
|
||||
"totalRequests": 4944
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 资源使用监控
|
||||
|
||||
```json
|
||||
{
|
||||
"resources": {
|
||||
"cpu": {
|
||||
"usage": "42%",
|
||||
"cores": 8,
|
||||
"processes": [
|
||||
{ "name": "mcp-server-1", "cpu": "15%" },
|
||||
{ "name": "mcp-server-2", "cpu": "12%" }
|
||||
]
|
||||
},
|
||||
"memory": {
|
||||
"total": "32GB",
|
||||
"used": "21.6GB",
|
||||
"available": "10.4GB",
|
||||
"byProcess": [
|
||||
{ "name": "mcp-server-1", "memory": "2.1GB" },
|
||||
{ "name": "mcp-server-2", "memory": "1.8GB" }
|
||||
]
|
||||
},
|
||||
"network": {
|
||||
"bytesIn": "1.2GB",
|
||||
"bytesOut": "890MB",
|
||||
"packetsIn": 1250000,
|
||||
"packetsOut": 980000
|
||||
},
|
||||
"disk": {
|
||||
"total": "1TB",
|
||||
"used": "340GB",
|
||||
"available": "660GB",
|
||||
"iops": 1200
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 日志管理
|
||||
|
||||
### 集中化日志收集
|
||||
|
||||
配置日志聚合:
|
||||
|
||||
```json
|
||||
{
|
||||
"logging": {
|
||||
"centralized": true,
|
||||
"storage": {
|
||||
"type": "elasticsearch",
|
||||
"config": {
|
||||
"hosts": ["localhost:9200"],
|
||||
"index": "mcphub-logs",
|
||||
"retention": "30d"
|
||||
}
|
||||
},
|
||||
"levels": ["error", "warn", "info", "debug"],
|
||||
"sources": ["application", "mcp-servers", "nginx", "system"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 实时日志查看
|
||||
|
||||
```bash
|
||||
# 获取实时日志流
|
||||
curl -X GET http://localhost:3000/api/monitoring/logs/stream \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-H "Accept: text/event-stream"
|
||||
|
||||
# 过滤日志
|
||||
curl -X GET http://localhost:3000/api/monitoring/logs \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-G -d "level=error" -d "server=server-1" -d "limit=100"
|
||||
```
|
||||
|
||||
### 日志分析
|
||||
|
||||
```bash
|
||||
# 错误模式分析
|
||||
curl -X GET http://localhost:3000/api/monitoring/logs/analysis \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-G -d "type=error-patterns" -d "timeRange=24h"
|
||||
```
|
||||
|
||||
响应示例:
|
||||
|
||||
```json
|
||||
{
|
||||
"analysis": {
|
||||
"errorPatterns": [
|
||||
{
|
||||
"pattern": "Connection timeout",
|
||||
"occurrences": 45,
|
||||
"trend": "increasing",
|
||||
"affectedServers": ["server-2", "server-3"]
|
||||
},
|
||||
{
|
||||
"pattern": "Memory allocation failed",
|
||||
"occurrences": 12,
|
||||
"trend": "stable",
|
||||
"affectedServers": ["server-1"]
|
||||
}
|
||||
],
|
||||
"recommendations": ["检查服务器网络连接", "增加内存限制配置"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 警报系统
|
||||
|
||||
### 配置警报规则
|
||||
|
||||
```json
|
||||
{
|
||||
"alerts": [
|
||||
{
|
||||
"name": "high-response-time",
|
||||
"description": "响应时间过高警报",
|
||||
"condition": "avg(response_time) > 1000ms over 5m",
|
||||
"severity": "warning",
|
||||
"notifications": {
|
||||
"email": ["admin@company.com"],
|
||||
"slack": "#alerts",
|
||||
"webhook": "https://hooks.company.com/alerts"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "server-down",
|
||||
"description": "服务器宕机警报",
|
||||
"condition": "server_status == 'down'",
|
||||
"severity": "critical",
|
||||
"notifications": {
|
||||
"email": ["oncall@company.com"],
|
||||
"sms": ["+1234567890"],
|
||||
"pagerduty": "service-key"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "high-error-rate",
|
||||
"description": "错误率过高",
|
||||
"condition": "error_rate > 5% over 10m",
|
||||
"severity": "error",
|
||||
"notifications": {
|
||||
"slack": "#dev-team"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 警报通知
|
||||
|
||||
```bash
|
||||
# 测试警报通知
|
||||
curl -X POST http://localhost:3000/api/monitoring/alerts/test \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-d '{
|
||||
"alertName": "high-response-time",
|
||||
"channel": "email"
|
||||
}'
|
||||
```
|
||||
|
||||
### 警报历史
|
||||
|
||||
```bash
|
||||
# 获取警报历史
|
||||
curl -X GET http://localhost:3000/api/monitoring/alerts/history \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-G -d "timeRange=7d" -d "severity=critical"
|
||||
```
|
||||
|
||||
## 用户活动监控
|
||||
|
||||
### 用户会话跟踪
|
||||
|
||||
```json
|
||||
{
|
||||
"userActivity": {
|
||||
"activeSessions": 156,
|
||||
"totalUsers": 89,
|
||||
"sessionsByUser": [
|
||||
{
|
||||
"userId": "user123",
|
||||
"sessions": 3,
|
||||
"lastActivity": "2024-01-01T12:30:00Z",
|
||||
"totalRequests": 245
|
||||
}
|
||||
],
|
||||
"topUsers": [
|
||||
{ "userId": "power-user", "requests": 1250 },
|
||||
{ "userId": "regular-user", "requests": 890 }
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 使用模式分析
|
||||
|
||||
```bash
|
||||
# 获取使用模式分析
|
||||
curl -X GET http://localhost:3000/api/monitoring/usage-patterns \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-G -d "timeRange=30d"
|
||||
```
|
||||
|
||||
响应示例:
|
||||
|
||||
```json
|
||||
{
|
||||
"patterns": {
|
||||
"peakHours": [9, 10, 14, 15, 16],
|
||||
"peakDays": ["tuesday", "wednesday", "thursday"],
|
||||
"toolUsage": [
|
||||
{"tool": "filesystem", "usage": 45%},
|
||||
{"tool": "web-search", "usage": 30%},
|
||||
{"tool": "database", "usage": 25%}
|
||||
],
|
||||
"userBehavior": {
|
||||
"avgSessionDuration": "45m",
|
||||
"avgRequestsPerSession": 23,
|
||||
"returnRate": "78%"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 容量规划
|
||||
|
||||
### 资源预测
|
||||
|
||||
```json
|
||||
{
|
||||
"capacityPlanning": {
|
||||
"currentCapacity": {
|
||||
"cpu": "42%",
|
||||
"memory": "68%",
|
||||
"network": "35%",
|
||||
"storage": "34%"
|
||||
},
|
||||
"predictions": {
|
||||
"timeHorizon": "30d",
|
||||
"cpuForecast": [
|
||||
{ "date": "2024-01-07", "usage": "48%" },
|
||||
{ "date": "2024-01-14", "usage": "52%" },
|
||||
{ "date": "2024-01-21", "usage": "56%" },
|
||||
{ "date": "2024-01-28", "usage": "61%" }
|
||||
],
|
||||
"recommendations": [
|
||||
"考虑在第3周增加CPU资源",
|
||||
"内存使用稳定,暂不需要扩容",
|
||||
"建议监控网络带宽趋势"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 自动扩缩容
|
||||
|
||||
```json
|
||||
{
|
||||
"autoScaling": {
|
||||
"enabled": true,
|
||||
"policies": [
|
||||
{
|
||||
"metric": "cpu_usage",
|
||||
"scaleUp": {
|
||||
"threshold": "80%",
|
||||
"duration": "5m",
|
||||
"action": "add_instance"
|
||||
},
|
||||
"scaleDown": {
|
||||
"threshold": "30%",
|
||||
"duration": "15m",
|
||||
"action": "remove_instance"
|
||||
}
|
||||
},
|
||||
{
|
||||
"metric": "request_queue_length",
|
||||
"scaleUp": {
|
||||
"threshold": 100,
|
||||
"duration": "2m",
|
||||
"action": "add_instance"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 性能分析报告
|
||||
|
||||
### 生成性能报告
|
||||
|
||||
```bash
|
||||
# 生成周报
|
||||
curl -X POST http://localhost:3000/api/monitoring/reports \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-d '{
|
||||
"type": "performance",
|
||||
"timeRange": "7d",
|
||||
"format": "pdf",
|
||||
"sections": [
|
||||
"overview",
|
||||
"response_times",
|
||||
"error_analysis",
|
||||
"resource_usage",
|
||||
"recommendations"
|
||||
],
|
||||
"recipients": ["manager@company.com"]
|
||||
}'
|
||||
```
|
||||
|
||||
### 自定义报告
|
||||
|
||||
```json
|
||||
{
|
||||
"customReport": {
|
||||
"name": "monthly-summary",
|
||||
"schedule": "0 0 1 * *",
|
||||
"template": "executive-summary",
|
||||
"data": {
|
||||
"kpis": ["uptime", "avg_response_time", "total_requests", "error_rate", "user_satisfaction"],
|
||||
"comparisons": {
|
||||
"previousMonth": true,
|
||||
"yearOverYear": true
|
||||
}
|
||||
},
|
||||
"distribution": {
|
||||
"email": ["executives@company.com"],
|
||||
"dashboard": true,
|
||||
"archive": true
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 监控 API
|
||||
|
||||
### 指标查询
|
||||
|
||||
```bash
|
||||
# 查询自定义指标
|
||||
curl -X POST http://localhost:3000/api/monitoring/query \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-d '{
|
||||
"query": "avg(response_time) by (server_id)",
|
||||
"timeRange": "1h",
|
||||
"step": "5m"
|
||||
}'
|
||||
```
|
||||
|
||||
### 事件跟踪
|
||||
|
||||
```bash
|
||||
# 记录自定义事件
|
||||
curl -X POST http://localhost:3000/api/monitoring/events \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-d '{
|
||||
"type": "deployment",
|
||||
"title": "服务器更新",
|
||||
"description": "部署新版本 v2.1.0",
|
||||
"tags": ["deployment", "version-2.1.0"],
|
||||
"metadata": {
|
||||
"version": "2.1.0",
|
||||
"servers": ["server-1", "server-2"]
|
||||
}
|
||||
}'
|
||||
```
|
||||
|
||||
## 第三方集成
|
||||
|
||||
### Prometheus 集成
|
||||
|
||||
```yaml
|
||||
# prometheus.yml
|
||||
global:
|
||||
scrape_interval: 15s
|
||||
|
||||
scrape_configs:
|
||||
- job_name: 'mcphub'
|
||||
static_configs:
|
||||
- targets: ['localhost:3000']
|
||||
metrics_path: '/api/monitoring/prometheus'
|
||||
scrape_interval: 30s
|
||||
```
|
||||
|
||||
### Grafana 仪表板
|
||||
|
||||
```json
|
||||
{
|
||||
"dashboard": {
|
||||
"title": "MCPHub 监控",
|
||||
"panels": [
|
||||
{
|
||||
"title": "请求率",
|
||||
"type": "graph",
|
||||
"targets": [
|
||||
{
|
||||
"expr": "rate(mcphub_requests_total[5m])",
|
||||
"legendFormat": "{{server_id}}"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "响应时间",
|
||||
"type": "graph",
|
||||
"targets": [
|
||||
{
|
||||
"expr": "histogram_quantile(0.95, mcphub_response_time_histogram)",
|
||||
"legendFormat": "95th percentile"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### ELK Stack 集成
|
||||
|
||||
```json
|
||||
{
|
||||
"logstash": {
|
||||
"input": {
|
||||
"beats": {
|
||||
"port": 5044
|
||||
}
|
||||
},
|
||||
"filter": {
|
||||
"if": "[fields][service] == 'mcphub'",
|
||||
"json": {
|
||||
"source": "message"
|
||||
},
|
||||
"date": {
|
||||
"match": ["timestamp", "ISO8601"]
|
||||
}
|
||||
},
|
||||
"output": {
|
||||
"elasticsearch": {
|
||||
"hosts": ["localhost:9200"],
|
||||
"index": "mcphub-logs-%{+YYYY.MM.dd}"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 故障排除
|
||||
|
||||
### 监控问题诊断
|
||||
|
||||
```bash
|
||||
# 检查监控服务状态
|
||||
curl -X GET http://localhost:3000/api/monitoring/health \
|
||||
-H "Authorization: Bearer $TOKEN"
|
||||
|
||||
# 验证指标收集
|
||||
curl -X GET http://localhost:3000/api/monitoring/metrics/validation \
|
||||
-H "Authorization: Bearer $TOKEN"
|
||||
```
|
||||
|
||||
### 性能问题分析
|
||||
|
||||
```bash
|
||||
# 性能瓶颈分析
|
||||
curl -X GET http://localhost:3000/api/monitoring/performance/bottlenecks \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-G -d "timeRange=1h"
|
||||
```
|
||||
|
||||
### 常见监控问题
|
||||
|
||||
1. **指标丢失**
|
||||
|
||||
- 检查收集器配置
|
||||
- 验证网络连接
|
||||
- 检查存储空间
|
||||
|
||||
2. **警报不触发**
|
||||
|
||||
- 验证警报规则语法
|
||||
- 检查通知配置
|
||||
- 测试通知渠道
|
||||
|
||||
3. **仪表板加载缓慢**
|
||||
- 优化查询时间范围
|
||||
- 增加数据聚合
|
||||
- 检查数据库性能
|
||||
|
||||
## 最佳实践
|
||||
|
||||
1. **监控层次化**: 建立应用、基础设施和业务三层监控
|
||||
2. **合理的警报**: 避免警报疲劳,设置合适的阈值
|
||||
3. **数据保留**: 根据业务需求设置适当的数据保留期
|
||||
4. **安全监控**: 监控安全相关的指标和事件
|
||||
5. **持续优化**: 定期审查和优化监控配置
|
||||
|
||||
有关更多信息,请参阅 [智能路由](/zh/features/smart-routing) 和 [服务器管理](/zh/features/server-management) 文档。
|
||||
496
docs/zh/features/server-management.mdx
Normal file
@@ -0,0 +1,496 @@
|
||||
---
|
||||
title: '服务器管理'
|
||||
description: '通过热插拔配置集中管理多个 MCP 服务器'
|
||||
---
|
||||
|
||||
## 概述
|
||||
|
||||
MCPHub 的服务器管理系统允许您从单个仪表板集中配置、监控和控制多个 MCP(模型上下文协议)服务器。所有更改都会实时应用,无需重启服务器。
|
||||
|
||||
## 添加 MCP 服务器
|
||||
|
||||
### 通过仪表板
|
||||
|
||||
1. **访问仪表板**: 导航到 `http://localhost:3000` 并登录
|
||||
2. **点击"添加服务器"**: 位于服务器部分
|
||||
3. **填写服务器详细信息**:
|
||||
- **名称**: 服务器的唯一标识符
|
||||
- **命令**: 可执行命令(例如 `npx`、`uvx`、`python`)
|
||||
- **参数**: 命令参数数组
|
||||
- **环境变量**: 环境设置的键值对
|
||||
- **工作目录**: 命令的可选工作目录
|
||||
|
||||
### 通过配置文件
|
||||
|
||||
编辑您的 `mcp_settings.json` 文件:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"server-name": {
|
||||
"command": "command-to-run",
|
||||
"args": ["arg1", "arg2"],
|
||||
"env": {
|
||||
"API_KEY": "your-api-key",
|
||||
"CONFIG_VALUE": "some-value"
|
||||
},
|
||||
"cwd": "/optional/working/directory"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 通过 API
|
||||
|
||||
使用 REST API 以编程方式添加服务器:
|
||||
|
||||
```bash
|
||||
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"
|
||||
}'
|
||||
```
|
||||
|
||||
## 服务器配置
|
||||
|
||||
### 通用配置选项
|
||||
|
||||
```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
|
||||
}
|
||||
```
|
||||
|
||||
### Python 服务器示例
|
||||
|
||||
```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"
|
||||
}
|
||||
```
|
||||
|
||||
### Node.js 服务器示例
|
||||
|
||||
```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"
|
||||
}
|
||||
```
|
||||
|
||||
## 服务器生命周期管理
|
||||
|
||||
### 启动服务器
|
||||
|
||||
```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"
|
||||
```
|
||||
|
||||
### 停止服务器
|
||||
|
||||
```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}'
|
||||
```
|
||||
|
||||
### 重启服务器
|
||||
|
||||
```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"
|
||||
}
|
||||
}
|
||||
}'
|
||||
```
|
||||
|
||||
## 服务器状态监控
|
||||
|
||||
### 检查服务器状态
|
||||
|
||||
```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"
|
||||
}
|
||||
```
|
||||
|
||||
### 健康检查
|
||||
|
||||
配置自动健康检查:
|
||||
|
||||
```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
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 监控资源使用
|
||||
|
||||
```bash
|
||||
# 获取资源使用统计
|
||||
curl -X GET http://localhost:3000/api/servers/my-server/resources \
|
||||
-H "Authorization: Bearer $TOKEN"
|
||||
```
|
||||
|
||||
## 日志管理
|
||||
|
||||
### 配置日志记录
|
||||
|
||||
```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"
|
||||
}
|
||||
}
|
||||
}'
|
||||
```
|
||||
|
||||
## 故障排除
|
||||
|
||||
### 常见问题
|
||||
|
||||
1. **服务器启动失败**
|
||||
|
||||
```bash
|
||||
# 检查服务器日志
|
||||
curl -X GET http://localhost:3000/api/servers/my-server/logs?level=error \
|
||||
-H "Authorization: Bearer $TOKEN"
|
||||
```
|
||||
|
||||
2. **配置无效**
|
||||
|
||||
```bash
|
||||
# 验证配置
|
||||
curl -X POST http://localhost:3000/api/servers/validate \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-d @server-config.json
|
||||
```
|
||||
|
||||
3. **性能问题**
|
||||
```bash
|
||||
# 获取性能指标
|
||||
curl -X GET http://localhost:3000/api/servers/my-server/metrics \
|
||||
-H "Authorization: Bearer $TOKEN"
|
||||
```
|
||||
|
||||
### 调试模式
|
||||
|
||||
启用详细调试:
|
||||
|
||||
```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
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 高级配置
|
||||
|
||||
### 自定义钩子
|
||||
|
||||
```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) 文档。
|
||||
691
docs/zh/features/smart-routing.mdx
Normal file
@@ -0,0 +1,691 @@
|
||||
---
|
||||
title: '智能路由'
|
||||
description: '自动负载均衡和请求路由到最佳的 MCP 服务器实例'
|
||||
---
|
||||
|
||||
## 概述
|
||||
|
||||
MCPHub 的智能路由系统自动将传入请求路由到最适合的 MCP 服务器实例。系统考虑服务器负载、响应时间、功能可用性和业务规则来做出路由决策。
|
||||
|
||||
## 路由策略
|
||||
|
||||
### 轮询路由
|
||||
|
||||
最简单的路由策略,按顺序分发请求:
|
||||
|
||||
```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
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 加权轮询
|
||||
|
||||
基于服务器容量分配不同权重:
|
||||
|
||||
```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"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 最少连接数
|
||||
|
||||
将请求路由到当前连接数最少的服务器:
|
||||
|
||||
```json
|
||||
{
|
||||
"routing": {
|
||||
"strategy": "least-connections",
|
||||
"balancingMode": "dynamic",
|
||||
"healthCheck": {
|
||||
"enabled": true,
|
||||
"interval": 10000
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 基于响应时间
|
||||
|
||||
路由到响应时间最短的服务器:
|
||||
|
||||
```json
|
||||
{
|
||||
"routing": {
|
||||
"strategy": "fastest-response",
|
||||
"metrics": {
|
||||
"measurementWindow": "5m",
|
||||
"sampleSize": 100,
|
||||
"excludeSlowRequests": true,
|
||||
"slowRequestThreshold": "5s"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 基于功能的路由
|
||||
|
||||
### 工具特定路由
|
||||
|
||||
根据请求的工具类型路由到专门的服务器:
|
||||
|
||||
```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"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 内容感知路由
|
||||
|
||||
基于请求内容进行智能路由:
|
||||
|
||||
```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": "图像处理专用服务器"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 地理位置路由
|
||||
|
||||
### 基于客户端位置
|
||||
|
||||
根据客户端地理位置路由到最近的服务器:
|
||||
|
||||
```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 '{
|
||||
"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
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 预测性负载均衡
|
||||
|
||||
```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
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 故障转移和恢复
|
||||
|
||||
### 自动故障转移
|
||||
|
||||
```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 \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-d '{
|
||||
"strategy": "weighted-round-robin",
|
||||
"weights": {
|
||||
"server-1": 3,
|
||||
"server-2": 2,
|
||||
"server-3": 1
|
||||
},
|
||||
"applyImmediately": true
|
||||
}'
|
||||
```
|
||||
|
||||
### 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 故障排除
|
||||
|
||||
### 路由调试
|
||||
|
||||
```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
|
||||
}'
|
||||
```
|
||||
|
||||
### 路由性能分析
|
||||
|
||||
```bash
|
||||
# 获取路由性能报告
|
||||
curl -X GET http://localhost:3000/api/routing/performance \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-G -d "timeRange=1h" -d "detailed=true"
|
||||
```
|
||||
|
||||
### 常见问题
|
||||
|
||||
1. **不均匀的负载分布**
|
||||
|
||||
- 检查服务器权重配置
|
||||
- 验证健康检查设置
|
||||
- 分析请求模式
|
||||
|
||||
2. **频繁的故障转移**
|
||||
|
||||
- 调整健康检查阈值
|
||||
- 检查网络连接稳定性
|
||||
- 优化服务器资源
|
||||
|
||||
3. **路由延迟过高**
|
||||
- 简化路由规则
|
||||
- 优化路由算法
|
||||
- 使用缓存加速决策
|
||||
|
||||
有关更多信息,请参阅 [监控](/zh/features/monitoring) 和 [服务器管理](/zh/features/server-management) 文档。
|
||||
97
docs/zh/index.mdx
Normal file
@@ -0,0 +1,97 @@
|
||||
---
|
||||
title: '欢迎使用 MCPHub'
|
||||
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" />
|
||||
|
||||
## 什么是 MCPHub?
|
||||
|
||||
MCPHub 是一个现代化的 Model Context Protocol (MCP) 服务器管理平台,旨在简化 AI 模型服务的部署、管理和监控。通过智能路由和负载均衡技术,MCPHub 帮助您构建高可用、可扩展的 AI 服务架构。
|
||||
|
||||
### 核心功能
|
||||
|
||||
- **🚀 智能路由** - 基于负载、延迟和健康状态的智能请求分发
|
||||
- **⚖️ 负载均衡** - 多种负载均衡策略,确保最优性能
|
||||
- **📊 实时监控** - 全面的性能指标和健康检查
|
||||
- **🔐 安全认证** - 企业级身份认证和访问控制
|
||||
- **🏗️ 服务器组管理** - 灵活的服务器分组和配置管理
|
||||
- **🔄 故障转移** - 自动故障检测和流量切换
|
||||
|
||||
## 快速开始
|
||||
|
||||
立即开始使用 MCPHub,只需几分钟即可部署您的第一个 MCP 服务器。
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="5 分钟快速部署" icon="rocket" href="/zh/quickstart">
|
||||
跟随我们的快速开始指南,5 分钟内部署 MCPHub 并连接您的第一个 MCP 服务器
|
||||
</Card>
|
||||
<Card title="开发环境搭建" icon="code" href="/zh/development/getting-started">
|
||||
设置本地开发环境,了解 MCPHub 的架构和开发工作流
|
||||
</Card>
|
||||
</CardGroup>
|
||||
|
||||
## 核心概念
|
||||
|
||||
了解 MCPHub 的核心概念,为深入使用做好准备。
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="MCP 协议介绍" icon="network-wired" href="/zh/concepts/mcp-protocol">
|
||||
深入了解 Model Context Protocol 的工作原理和最佳实践
|
||||
</Card>
|
||||
<Card title="智能路由机制" icon="route" href="/zh/features/smart-routing">
|
||||
学习 MCPHub 的智能路由算法和配置策略
|
||||
</Card>
|
||||
<Card title="服务器管理" icon="server" href="/zh/features/server-management">
|
||||
掌握 MCP 服务器的添加、配置和管理技巧
|
||||
</Card>
|
||||
<Card title="监控与分析" icon="chart-line" href="/zh/features/monitoring">
|
||||
使用内置的监控工具跟踪性能和识别问题
|
||||
</Card>
|
||||
</CardGroup>
|
||||
|
||||
## 部署选项
|
||||
|
||||
MCPHub 支持多种部署方式,满足不同规模和场景的需求。
|
||||
|
||||
<CardGroup cols={3}>
|
||||
<Card title="Docker 部署" icon="docker" href="/zh/configuration/docker-setup">
|
||||
使用 Docker 容器快速部署,支持单机和集群模式
|
||||
</Card>
|
||||
<Card title="云服务部署" icon="cloud" href="/zh/deployment/cloud">
|
||||
在 AWS、GCP、Azure 等云平台上部署 MCPHub
|
||||
</Card>
|
||||
<Card title="Kubernetes" icon="dharmachakra" href="/zh/deployment/kubernetes">
|
||||
在 Kubernetes 集群中部署高可用的 MCPHub 服务
|
||||
</Card>
|
||||
</CardGroup>
|
||||
|
||||
## API 和集成
|
||||
|
||||
MCPHub 提供完整的 RESTful API 和多语言 SDK,方便与现有系统集成。
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="API 参考文档" icon="code" href="/zh/api-reference/introduction">
|
||||
完整的 API 接口文档,包含详细的请求示例和响应格式
|
||||
</Card>
|
||||
<Card title="SDK 和工具" icon="toolbox" href="/zh/sdk">
|
||||
官方 SDK 和命令行工具,加速开发集成
|
||||
</Card>
|
||||
</CardGroup>
|
||||
|
||||
## 社区和支持
|
||||
|
||||
加入 MCPHub 社区,获取帮助和分享经验。
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="GitHub 仓库" icon="github" href="https://github.com/mcphub/mcphub">
|
||||
查看源代码、提交问题和贡献代码
|
||||
</Card>
|
||||
<Card title="Discord 社区" icon="discord" href="https://discord.gg/mcphub">
|
||||
与其他开发者交流,获取实时帮助
|
||||
</Card>
|
||||
<Card title="Sponsor" icon="heart" href="https://ko-fi.com/samanhappy">
|
||||
支持 MCPHub 的开发和维护,帮助我们持续改进
|
||||
</Card>
|
||||
</CardGroup>
|
||||
304
docs/zh/quickstart.mdx
Normal file
@@ -0,0 +1,304 @@
|
||||
---
|
||||
title: '快速开始'
|
||||
description: '5 分钟内部署 MCPHub 并连接您的第一个 MCP 服务器'
|
||||
---
|
||||
|
||||
## 欢迎使用 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 命令行工具:
|
||||
|
||||
```bash
|
||||
npm install -g @mcphub/cli
|
||||
```
|
||||
|
||||
验证安装:
|
||||
```bash
|
||||
mcphub --version
|
||||
```
|
||||
|
||||
</Accordion>
|
||||
|
||||
<Accordion icon="folder-plus" title="创建新项目">
|
||||
创建一个新的 MCPHub 项目:
|
||||
|
||||
```bash
|
||||
# 创建项目
|
||||
mcphub init my-mcphub-project
|
||||
cd my-mcphub-project
|
||||
|
||||
# 安装依赖
|
||||
npm install
|
||||
```
|
||||
|
||||
</Accordion>
|
||||
|
||||
<Accordion icon="gear" title="配置环境">
|
||||
复制并编辑环境变量文件:
|
||||
|
||||
```bash
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
编辑 `.env` 文件,设置基本配置:
|
||||
```bash
|
||||
# 服务器配置
|
||||
PORT=3000
|
||||
NODE_ENV=development
|
||||
|
||||
# 数据库配置(使用内置 SQLite)
|
||||
DATABASE_URL=sqlite:./data/mcphub.db
|
||||
|
||||
# JWT 密钥(请更改为安全的随机字符串)
|
||||
JWT_SECRET=your-super-secret-jwt-key-change-me
|
||||
|
||||
# 管理员账户
|
||||
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
|
||||
```
|
||||
|
||||
### 生产模式启动
|
||||
|
||||
```bash
|
||||
# 构建应用
|
||||
npm run build
|
||||
|
||||
# 启动生产服务器
|
||||
npm start
|
||||
```
|
||||
|
||||
<Note>开发模式下,MCPHub 会在 `http://localhost:3000` 启动,并具有热重载功能。</Note>
|
||||
|
||||
## 首次访问和配置
|
||||
|
||||
### 1. 访问管理界面
|
||||
|
||||
打开浏览器,访问 `http://localhost:3000`,您将看到 MCPHub 的欢迎页面。
|
||||
|
||||
### 2. 登录管理员账户
|
||||
|
||||
使用您在 `.env` 文件中设置的管理员凭据登录:
|
||||
|
||||
- **邮箱**: `admin@example.com`
|
||||
- **密码**: `admin123`
|
||||
|
||||
<Warning>首次登录后,请立即更改默认密码以确保安全!</Warning>
|
||||
|
||||
### 3. 完成初始配置
|
||||
|
||||
登录后,系统会引导您完成初始配置:
|
||||
|
||||
1. **更改管理员密码**
|
||||
2. **设置组织信息**
|
||||
3. **配置基本设置**
|
||||
|
||||
## 添加您的第一个 MCP 服务器
|
||||
|
||||
### 1. 准备 MCP 服务器
|
||||
|
||||
如果您还没有 MCP 服务器,可以使用我们的示例服务器进行测试:
|
||||
|
||||
```bash
|
||||
# 克隆示例服务器
|
||||
git clone https://github.com/mcphub/example-mcp-server.git
|
||||
cd example-mcp-server
|
||||
|
||||
# 安装依赖并启动
|
||||
npm install
|
||||
npm start
|
||||
```
|
||||
|
||||
示例服务器将在 `http://localhost:3001` 启动。
|
||||
|
||||
### 2. 在 MCPHub 中添加服务器
|
||||
|
||||
在 MCPHub 管理界面中:
|
||||
|
||||
1. 点击 **"添加服务器"** 按钮
|
||||
2. 填写服务器信息:
|
||||
```
|
||||
名称: Example MCP Server
|
||||
端点: http://localhost:3001
|
||||
描述: 示例 MCP 服务器用于测试
|
||||
```
|
||||
3. 选择功能类型(如:chat、completion、analysis)
|
||||
4. 点击 **"测试连接"** 验证服务器可达性
|
||||
5. 点击 **"保存"** 完成添加
|
||||
|
||||
### 3. 验证服务器状态
|
||||
|
||||
添加成功后,您应该能在服务器列表中看到新添加的服务器,状态显示为 **"活跃"**(绿色)。
|
||||
|
||||
## 测试路由功能
|
||||
|
||||
### 发送测试请求
|
||||
|
||||
使用 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!"
|
||||
}
|
||||
]
|
||||
}'
|
||||
```
|
||||
|
||||
### 查看请求日志
|
||||
|
||||
在 MCPHub 管理界面的 **"监控"** 页面中,您可以实时查看:
|
||||
|
||||
- 请求数量和响应时间
|
||||
- 服务器健康状态
|
||||
- 错误日志和统计
|
||||
|
||||
## 后续步骤
|
||||
|
||||
恭喜!您已经成功部署了 MCPHub 并添加了第一个 MCP 服务器。接下来您可以:
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="配置负载均衡" icon="balance-scale" href="/zh/features/smart-routing">
|
||||
学习如何配置智能路由和负载均衡策略
|
||||
</Card>
|
||||
<Card title="添加更多服务器" icon="plus" href="/zh/features/server-management">
|
||||
了解服务器管理的高级功能
|
||||
</Card>
|
||||
<Card title="设置监控告警" icon="bell" href="/zh/features/monitoring">
|
||||
配置性能监控和告警通知
|
||||
</Card>
|
||||
<Card title="API 集成" icon="code" href="/zh/api-reference/introduction">
|
||||
将 MCPHub 集成到您的应用程序中
|
||||
</Card>
|
||||
</CardGroup>
|
||||
|
||||
## 常见问题
|
||||
|
||||
<AccordionGroup>
|
||||
<Accordion icon="question" title="无法连接到 MCP 服务器">
|
||||
**可能原因**:
|
||||
- 服务器地址错误或服务器未启动
|
||||
- 防火墙阻止连接
|
||||
- 网络配置问题
|
||||
|
||||
**解决方案**:
|
||||
1. 验证服务器是否正在运行:`curl http://localhost:3001/health`
|
||||
2. 检查防火墙设置
|
||||
3. 确认网络连接正常
|
||||
|
||||
</Accordion>
|
||||
|
||||
<Accordion icon="question" title="服务器状态显示为离线">
|
||||
**可能原因**:
|
||||
- 健康检查失败
|
||||
- 服务器响应超时
|
||||
- 服务器崩溃或重启
|
||||
|
||||
**解决方案**:
|
||||
1. 检查服务器日志
|
||||
2. 调整健康检查间隔
|
||||
3. 重启服务器进程
|
||||
|
||||
</Accordion>
|
||||
|
||||
<Accordion icon="question" title="忘记管理员密码">
|
||||
**解决方案**:
|
||||
```bash
|
||||
# 重置管理员密码
|
||||
npm run reset-admin-password
|
||||
```
|
||||
或者删除数据库文件重新初始化:
|
||||
```bash
|
||||
rm data/mcphub.db
|
||||
npm run db:setup
|
||||
```
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
|
||||
## 获取帮助
|
||||
|
||||
如果您在设置过程中遇到问题:
|
||||
|
||||
- 📖 查看 [完整文档](/zh/development/getting-started)
|
||||
- 🐛 在 [GitHub](https://github.com/mcphub/mcphub/issues) 上报告问题
|
||||
- 💬 加入 [Discord 社区](https://discord.gg/mcphub) 获取实时帮助
|
||||
- 📧 发送邮件至 support@mcphub.io
|
||||
256
examples/openapi-schema-config.json
Normal file
@@ -0,0 +1,256 @@
|
||||
{
|
||||
"mcpServers": {
|
||||
"example-api-url": {
|
||||
"type": "openapi",
|
||||
"openapi": {
|
||||
"url": "https://api.example.com/openapi.json",
|
||||
"version": "3.1.0",
|
||||
"security": {
|
||||
"type": "apiKey",
|
||||
"apiKey": {
|
||||
"name": "X-API-Key",
|
||||
"in": "header",
|
||||
"value": "your-api-key-here"
|
||||
}
|
||||
}
|
||||
},
|
||||
"headers": {
|
||||
"User-Agent": "MCPHub/1.0"
|
||||
},
|
||||
"enabled": true
|
||||
},
|
||||
"example-api-schema": {
|
||||
"type": "openapi",
|
||||
"openapi": {
|
||||
"schema": {
|
||||
"openapi": "3.1.0",
|
||||
"info": {
|
||||
"title": "Example API",
|
||||
"version": "1.0.0",
|
||||
"description": "A sample API for demonstration"
|
||||
},
|
||||
"servers": [
|
||||
{
|
||||
"url": "https://api.example.com",
|
||||
"description": "Production server"
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"/users": {
|
||||
"get": {
|
||||
"operationId": "listUsers",
|
||||
"summary": "List all users",
|
||||
"description": "Retrieve a list of all users in the system",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "limit",
|
||||
"in": "query",
|
||||
"description": "Maximum number of users to return",
|
||||
"required": false,
|
||||
"schema": {
|
||||
"type": "integer",
|
||||
"minimum": 1,
|
||||
"maximum": 100,
|
||||
"default": 10
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "offset",
|
||||
"in": "query",
|
||||
"description": "Number of users to skip",
|
||||
"required": false,
|
||||
"schema": {
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"default": 0
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "List of users",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"users": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/User"
|
||||
}
|
||||
},
|
||||
"total": {
|
||||
"type": "integer",
|
||||
"description": "Total number of users"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"post": {
|
||||
"operationId": "createUser",
|
||||
"summary": "Create a new user",
|
||||
"description": "Create a new user in the system",
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/CreateUserRequest"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"201": {
|
||||
"description": "User created successfully",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/User"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/users/{userId}": {
|
||||
"get": {
|
||||
"operationId": "getUserById",
|
||||
"summary": "Get user by ID",
|
||||
"description": "Retrieve a specific user by their ID",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "userId",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"description": "ID of the user to retrieve",
|
||||
"schema": {
|
||||
"type": "integer",
|
||||
"minimum": 1
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "User details",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/User"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "User not found"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
"schemas": {
|
||||
"User": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "integer",
|
||||
"description": "Unique identifier for the user"
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "Full name of the user"
|
||||
},
|
||||
"email": {
|
||||
"type": "string",
|
||||
"format": "email",
|
||||
"description": "Email address of the user"
|
||||
},
|
||||
"createdAt": {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"description": "Timestamp when the user was created"
|
||||
},
|
||||
"status": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"active",
|
||||
"inactive",
|
||||
"suspended"
|
||||
],
|
||||
"description": "Current status of the user"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"name",
|
||||
"email"
|
||||
]
|
||||
},
|
||||
"CreateUserRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"maxLength": 100,
|
||||
"description": "Full name of the user"
|
||||
},
|
||||
"email": {
|
||||
"type": "string",
|
||||
"format": "email",
|
||||
"description": "Email address of the user"
|
||||
},
|
||||
"status": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"active",
|
||||
"inactive"
|
||||
],
|
||||
"default": "active",
|
||||
"description": "Initial status of the user"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"name",
|
||||
"email"
|
||||
]
|
||||
}
|
||||
},
|
||||
"securitySchemes": {
|
||||
"ApiKeyAuth": {
|
||||
"type": "apiKey",
|
||||
"in": "header",
|
||||
"name": "X-API-Key"
|
||||
}
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"ApiKeyAuth": []
|
||||
}
|
||||
]
|
||||
},
|
||||
"version": "3.1.0",
|
||||
"security": {
|
||||
"type": "apiKey",
|
||||
"apiKey": {
|
||||
"name": "X-API-Key",
|
||||
"in": "header",
|
||||
"value": "your-api-key-here"
|
||||
}
|
||||
}
|
||||
},
|
||||
"headers": {
|
||||
"User-Agent": "MCPHub/1.0"
|
||||
},
|
||||
"enabled": true
|
||||
}
|
||||
}
|
||||
}
|
||||
BIN
frontend/favicon.ico
Normal file → Executable file
|
Before Width: | Height: | Size: 5.1 KiB After Width: | Height: | Size: 5.1 KiB |
@@ -1,13 +1,19 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>MCP Hub Dashboard</title>
|
||||
<link rel="icon" type="image/x-icon" href="/favicon.ico">
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
||||
</head>
|
||||
|
||||
<body class="bg-gray-100">
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -9,33 +9,37 @@ import LoginPage from './pages/LoginPage';
|
||||
import DashboardPage from './pages/Dashboard';
|
||||
import ServersPage from './pages/ServersPage';
|
||||
import GroupsPage from './pages/GroupsPage';
|
||||
import UsersPage from './pages/UsersPage';
|
||||
import SettingsPage from './pages/SettingsPage';
|
||||
import MarketPage from './pages/MarketPage';
|
||||
import LogsPage from './pages/LogsPage';
|
||||
import { getBasePath } from './utils/runtime';
|
||||
|
||||
function App() {
|
||||
const basename = getBasePath();
|
||||
return (
|
||||
<ThemeProvider>
|
||||
<AuthProvider>
|
||||
<ToastProvider>
|
||||
<Router>
|
||||
<Router basename={basename}>
|
||||
<Routes>
|
||||
{/* 公共路由 */}
|
||||
<Route path="/login" element={<LoginPage />} />
|
||||
|
||||
|
||||
{/* 受保护的路由,使用 MainLayout 作为布局容器 */}
|
||||
<Route element={<ProtectedRoute />}>
|
||||
<Route element={<MainLayout />}>
|
||||
<Route path="/" element={<DashboardPage />} />
|
||||
<Route path="/servers" element={<ServersPage />} />
|
||||
<Route path="/groups" element={<GroupsPage />} />
|
||||
<Route path="/users" element={<UsersPage />} />
|
||||
<Route path="/market" element={<MarketPage />} />
|
||||
<Route path="/market/:serverName" element={<MarketPage />} />
|
||||
<Route path="/logs" element={<LogsPage />} />
|
||||
<Route path="/settings" element={<SettingsPage />} />
|
||||
</Route>
|
||||
</Route>
|
||||
|
||||
|
||||
{/* 未匹配的路由重定向到首页 */}
|
||||
<Route path="*" element={<Navigate to="/" />} />
|
||||
</Routes>
|
||||
|
||||
@@ -2,8 +2,8 @@ import { useState, useEffect } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useGroupData } from '@/hooks/useGroupData'
|
||||
import { useServerData } from '@/hooks/useServerData'
|
||||
import { GroupFormData, Server } from '@/types'
|
||||
import { ToggleGroup } from './ui/ToggleGroup'
|
||||
import { GroupFormData, Server, IGroupServerConfig } from '@/types'
|
||||
import { ServerToolConfig } from './ServerToolConfig'
|
||||
|
||||
interface AddGroupFormProps {
|
||||
onAdd: () => void
|
||||
@@ -21,7 +21,7 @@ const AddGroupForm = ({ onAdd, onCancel }: AddGroupFormProps) => {
|
||||
const [formData, setFormData] = useState<GroupFormData>({
|
||||
name: '',
|
||||
description: '',
|
||||
servers: []
|
||||
servers: [] as IGroupServerConfig[]
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
@@ -50,7 +50,7 @@ const AddGroupForm = ({ onAdd, onCancel }: AddGroupFormProps) => {
|
||||
}
|
||||
|
||||
const result = await createGroup(formData.name, formData.description, formData.servers)
|
||||
|
||||
|
||||
if (!result) {
|
||||
setError(t('groups.createError'))
|
||||
setIsSubmitting(false)
|
||||
@@ -66,64 +66,68 @@ const AddGroupForm = ({ onAdd, onCancel }: AddGroupFormProps) => {
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 bg-black/50 z-50 flex items-center justify-center p-4">
|
||||
<div className="bg-white rounded-lg shadow-lg max-w-md w-full">
|
||||
<div className="p-6">
|
||||
<div className="bg-white rounded-lg shadow-lg max-w-2xl w-full max-h-[90vh] flex flex-col">
|
||||
<div className="p-6 flex-shrink-0">
|
||||
<h2 className="text-xl font-semibold text-gray-800 mb-4">{t('groups.addNew')}</h2>
|
||||
|
||||
|
||||
{error && (
|
||||
<div className="mb-4 p-3 bg-red-100 text-red-700 rounded">
|
||||
<div className="mb-4 p-3 bg-red-100 text-red-700 rounded-md border border-gray-200">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="mb-4">
|
||||
<label className="block text-gray-700 text-sm font-bold mb-2" htmlFor="name">
|
||||
{t('groups.name')} *
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="name"
|
||||
name="name"
|
||||
value={formData.name}
|
||||
onChange={handleChange}
|
||||
className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
|
||||
placeholder={t('groups.namePlaceholder')}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<ToggleGroup
|
||||
className="mb-6"
|
||||
label={t('groups.servers')}
|
||||
noOptionsText={t('groups.noServerOptions')}
|
||||
values={formData.servers}
|
||||
options={availableServers.map(server => ({
|
||||
value: server.name,
|
||||
label: server.name
|
||||
}))}
|
||||
onChange={(servers) => setFormData(prev => ({ ...prev, servers }))}
|
||||
/>
|
||||
|
||||
<div className="flex justify-end space-x-3">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onCancel}
|
||||
className="px-4 py-2 text-gray-600 hover:text-gray-800"
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
{t('common.cancel')}
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 disabled:opacity-50"
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
{isSubmitting ? t('common.submitting') : t('common.create')}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit} className="flex flex-col flex-1 min-h-0">
|
||||
<div className="flex-1 overflow-y-auto px-6">
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-gray-700 text-sm font-bold mb-2" htmlFor="name">
|
||||
{t('groups.name')} *
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="name"
|
||||
name="name"
|
||||
value={formData.name}
|
||||
onChange={handleChange}
|
||||
className="w-full border border-gray-300 rounded-md px-3 py-2 text-gray-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
placeholder={t('groups.namePlaceholder')}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-gray-700 text-sm font-bold mb-2">
|
||||
{t('groups.configureTools')}
|
||||
</label>
|
||||
<ServerToolConfig
|
||||
servers={availableServers}
|
||||
value={formData.servers as IGroupServerConfig[]}
|
||||
onChange={(servers) => setFormData(prev => ({ ...prev, servers }))}
|
||||
className="border border-gray-200 rounded-lg p-4 bg-gray-50"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end space-x-3 p-6 pt-4 border-t border-gray-200 flex-shrink-0">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onCancel}
|
||||
className="px-4 py-2 text-gray-600 hover:text-gray-800 border border-gray-300 rounded-md hover:bg-gray-50 transition-colors"
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
{t('common.cancel')}
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
className="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 disabled:opacity-50 transition-colors"
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
{isSubmitting ? t('common.submitting') : t('common.create')}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import ServerForm from './ServerForm'
|
||||
import { getApiUrl } from '../utils/runtime'
|
||||
import { detectVariables } from '../utils/variableDetection'
|
||||
|
||||
interface AddServerFormProps {
|
||||
onAdd: () => void
|
||||
@@ -10,17 +12,30 @@ const AddServerForm = ({ onAdd }: AddServerFormProps) => {
|
||||
const { t } = useTranslation()
|
||||
const [modalVisible, setModalVisible] = useState(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [confirmationVisible, setConfirmationVisible] = useState(false)
|
||||
const [pendingPayload, setPendingPayload] = useState<any>(null)
|
||||
const [detectedVariables, setDetectedVariables] = useState<string[]>([])
|
||||
|
||||
const toggleModal = () => {
|
||||
setModalVisible(!modalVisible)
|
||||
setError(null) // Clear any previous errors when toggling modal
|
||||
setConfirmationVisible(false) // Close confirmation dialog
|
||||
setPendingPayload(null) // Clear pending payload
|
||||
}
|
||||
|
||||
const handleSubmit = async (payload: any) => {
|
||||
const handleConfirmSubmit = async () => {
|
||||
if (pendingPayload) {
|
||||
await submitServer(pendingPayload)
|
||||
setConfirmationVisible(false)
|
||||
setPendingPayload(null)
|
||||
}
|
||||
}
|
||||
|
||||
const submitServer = async (payload: any) => {
|
||||
try {
|
||||
setError(null)
|
||||
const token = localStorage.getItem('mcphub_token');
|
||||
const response = await fetch('/api/servers', {
|
||||
const response = await fetch(getApiUrl('/servers'), {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
@@ -64,13 +79,36 @@ const AddServerForm = ({ onAdd }: AddServerFormProps) => {
|
||||
}
|
||||
}
|
||||
|
||||
const handleSubmit = async (payload: any) => {
|
||||
try {
|
||||
// Check for variables in the payload
|
||||
const variables = detectVariables(payload)
|
||||
|
||||
if (variables.length > 0) {
|
||||
// Show confirmation dialog
|
||||
setDetectedVariables(variables)
|
||||
setPendingPayload(payload)
|
||||
setConfirmationVisible(true)
|
||||
} else {
|
||||
// Submit directly if no variables found
|
||||
await submitServer(payload)
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error processing server submission:', err)
|
||||
setError(t('errors.serverAdd'))
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<button
|
||||
onClick={toggleModal}
|
||||
className="w-full bg-blue-500 hover:bg-blue-600 text-white font-medium py-2 px-4 rounded"
|
||||
className="w-full bg-blue-100 text-blue-800 rounded hover:bg-blue-200 py-2 px-4 flex items-center justify-center btn-primary"
|
||||
>
|
||||
{t('server.addServer')}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4 mr-2" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fillRule="evenodd" d="M10 3a1 1 0 011 1v5h5a1 1 0 110 2h-5v5a1 1 0 11-2 0v-5H4a1 1 0 110-2h5V4a1 1 0 011-1z" clipRule="evenodd" />
|
||||
</svg>
|
||||
{t('server.add')}
|
||||
</button>
|
||||
|
||||
{modalVisible && (
|
||||
@@ -83,6 +121,60 @@ const AddServerForm = ({ onAdd }: AddServerFormProps) => {
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{confirmationVisible && (
|
||||
<div className="fixed inset-0 bg-black/50 z-[60] flex items-center justify-center p-4">
|
||||
<div className="bg-white rounded-lg p-6 w-full max-w-md">
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-4">
|
||||
{t('server.confirmVariables')}
|
||||
</h3>
|
||||
<p className="text-gray-600 mb-4">
|
||||
{t('server.variablesDetected')}
|
||||
</p>
|
||||
<div className="bg-yellow-50 border border-yellow-200 rounded p-3 mb-4">
|
||||
<div className="flex items-start">
|
||||
<div className="flex-shrink-0">
|
||||
<svg className="h-5 w-5 text-yellow-400" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fillRule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clipRule="evenodd" />
|
||||
</svg>
|
||||
</div>
|
||||
<div className="ml-3">
|
||||
<h4 className="text-sm font-medium text-yellow-800">
|
||||
{t('server.detectedVariables')}:
|
||||
</h4>
|
||||
<ul className="mt-1 text-sm text-yellow-700">
|
||||
{detectedVariables.map((variable, index) => (
|
||||
<li key={index} className="font-mono">
|
||||
${`{${variable}}`}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-gray-600 text-sm mb-6">
|
||||
{t('server.confirmVariablesMessage')}
|
||||
</p>
|
||||
<div className="flex justify-end space-x-3">
|
||||
<button
|
||||
onClick={() => {
|
||||
setConfirmationVisible(false)
|
||||
setPendingPayload(null)
|
||||
}}
|
||||
className="px-4 py-2 text-gray-600 border border-gray-300 rounded hover:bg-gray-50 btn-secondary"
|
||||
>
|
||||
{t('common.cancel')}
|
||||
</button>
|
||||
<button
|
||||
onClick={handleConfirmSubmit}
|
||||
className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 btn-primary"
|
||||
>
|
||||
{t('server.confirmAndAdd')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -31,17 +31,17 @@ const ChangePasswordForm: React.FC<ChangePasswordFormProps> = ({ onSuccess, onCa
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setError(null);
|
||||
|
||||
|
||||
// Validate passwords match
|
||||
if (formData.newPassword !== confirmPassword) {
|
||||
setError(t('auth.passwordsNotMatch'));
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const response = await changePassword(formData);
|
||||
|
||||
|
||||
if (response.success) {
|
||||
setSuccess(true);
|
||||
if (onSuccess) {
|
||||
@@ -60,7 +60,7 @@ const ChangePasswordForm: React.FC<ChangePasswordFormProps> = ({ onSuccess, onCa
|
||||
return (
|
||||
<div className="p-6 bg-white rounded-lg shadow-md">
|
||||
<h2 className="text-xl font-bold mb-4">{t('auth.changePassword')}</h2>
|
||||
|
||||
|
||||
{success ? (
|
||||
<div className="bg-green-100 border border-green-400 text-green-700 px-4 py-3 rounded mb-4">
|
||||
{t('auth.changePasswordSuccess')}
|
||||
@@ -72,7 +72,7 @@ const ChangePasswordForm: React.FC<ChangePasswordFormProps> = ({ onSuccess, onCa
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
<div className="mb-4">
|
||||
<label className="block text-gray-700 text-sm font-bold mb-2" htmlFor="currentPassword">
|
||||
{t('auth.currentPassword')}
|
||||
@@ -81,13 +81,13 @@ const ChangePasswordForm: React.FC<ChangePasswordFormProps> = ({ onSuccess, onCa
|
||||
type="password"
|
||||
id="currentPassword"
|
||||
name="currentPassword"
|
||||
className="w-full p-2 border rounded focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
className="w-full p-2 border rounded focus:outline-none focus:ring-2 focus:ring-blue-500 form-input"
|
||||
value={formData.currentPassword}
|
||||
onChange={handleChange}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="mb-4">
|
||||
<label className="block text-gray-700 text-sm font-bold mb-2" htmlFor="newPassword">
|
||||
{t('auth.newPassword')}
|
||||
@@ -96,14 +96,14 @@ const ChangePasswordForm: React.FC<ChangePasswordFormProps> = ({ onSuccess, onCa
|
||||
type="password"
|
||||
id="newPassword"
|
||||
name="newPassword"
|
||||
className="w-full p-2 border rounded focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
className="w-full p-2 border rounded focus:outline-none focus:ring-2 focus:ring-blue-500 form-input"
|
||||
value={formData.newPassword}
|
||||
onChange={handleChange}
|
||||
required
|
||||
minLength={6}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="mb-6">
|
||||
<label className="block text-gray-700 text-sm font-bold mb-2" htmlFor="confirmPassword">
|
||||
{t('auth.confirmPassword')}
|
||||
@@ -112,14 +112,14 @@ const ChangePasswordForm: React.FC<ChangePasswordFormProps> = ({ onSuccess, onCa
|
||||
type="password"
|
||||
id="confirmPassword"
|
||||
name="confirmPassword"
|
||||
className="w-full p-2 border rounded focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
className="w-full p-2 border rounded focus:outline-none focus:ring-2 focus:ring-blue-500 form-input"
|
||||
value={confirmPassword}
|
||||
onChange={handleChange}
|
||||
required
|
||||
minLength={6}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="flex justify-end space-x-2">
|
||||
{onCancel && (
|
||||
<button
|
||||
@@ -134,7 +134,7 @@ const ChangePasswordForm: React.FC<ChangePasswordFormProps> = ({ onSuccess, onCa
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isLoading}
|
||||
className="py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
|
||||
className="py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 btn-primary"
|
||||
>
|
||||
{isLoading ? (
|
||||
<span className="flex items-center">
|
||||
|
||||
413
frontend/src/components/DxtUploadForm.tsx
Normal file
@@ -0,0 +1,413 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { getApiUrl } from '@/utils/runtime';
|
||||
import ConfirmDialog from '@/components/ui/ConfirmDialog';
|
||||
|
||||
interface DxtUploadFormProps {
|
||||
onSuccess: (serverConfig: any) => void;
|
||||
onCancel: () => void;
|
||||
}
|
||||
|
||||
interface DxtUploadResponse {
|
||||
success: boolean;
|
||||
data?: {
|
||||
manifest: any;
|
||||
extractDir: string;
|
||||
};
|
||||
message?: string;
|
||||
}
|
||||
|
||||
const DxtUploadForm: React.FC<DxtUploadFormProps> = ({ onSuccess, onCancel }) => {
|
||||
const { t } = useTranslation();
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
const [isUploading, setIsUploading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [selectedFile, setSelectedFile] = useState<File | null>(null);
|
||||
const [showServerForm, setShowServerForm] = useState(false);
|
||||
const [manifestData, setManifestData] = useState<any>(null);
|
||||
const [extractDir, setExtractDir] = useState<string>('');
|
||||
const [showConfirmDialog, setShowConfirmDialog] = useState(false);
|
||||
const [pendingServerName, setPendingServerName] = useState<string>('');
|
||||
|
||||
const handleDragOver = (e: React.DragEvent) => {
|
||||
e.preventDefault();
|
||||
setIsDragging(true);
|
||||
};
|
||||
|
||||
const handleDragLeave = (e: React.DragEvent) => {
|
||||
e.preventDefault();
|
||||
setIsDragging(false);
|
||||
};
|
||||
|
||||
const handleDrop = (e: React.DragEvent) => {
|
||||
e.preventDefault();
|
||||
setIsDragging(false);
|
||||
|
||||
const files = e.dataTransfer.files;
|
||||
if (files.length > 0) {
|
||||
const file = files[0];
|
||||
if (file.name.endsWith('.dxt')) {
|
||||
setSelectedFile(file);
|
||||
setError(null);
|
||||
} else {
|
||||
setError(t('dxt.invalidFileType'));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const files = e.target.files;
|
||||
if (files && files.length > 0) {
|
||||
const file = files[0];
|
||||
if (file.name.endsWith('.dxt')) {
|
||||
setSelectedFile(file);
|
||||
setError(null);
|
||||
} else {
|
||||
setError(t('dxt.invalidFileType'));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleUpload = async () => {
|
||||
if (!selectedFile) {
|
||||
setError(t('dxt.noFileSelected'));
|
||||
return;
|
||||
}
|
||||
|
||||
setIsUploading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append('dxtFile', selectedFile);
|
||||
|
||||
const token = localStorage.getItem('mcphub_token');
|
||||
const response = await fetch(getApiUrl('/dxt/upload'), {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'x-auth-token': token || '',
|
||||
},
|
||||
body: formData,
|
||||
});
|
||||
|
||||
const result: DxtUploadResponse = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(result.message || `HTTP error! Status: ${response.status}`);
|
||||
}
|
||||
|
||||
if (result.success && result.data) {
|
||||
setManifestData(result.data.manifest);
|
||||
setExtractDir(result.data.extractDir);
|
||||
setShowServerForm(true);
|
||||
} else {
|
||||
throw new Error(result.message || t('dxt.uploadFailed'));
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('DXT upload error:', err);
|
||||
setError(err instanceof Error ? err.message : t('dxt.uploadFailed'));
|
||||
} finally {
|
||||
setIsUploading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleInstallServer = async (serverName: string, forceOverride: boolean = false) => {
|
||||
setIsUploading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
// Convert DXT manifest to MCPHub stdio server configuration
|
||||
const serverConfig = convertDxtToMcpConfig(manifestData, extractDir, serverName);
|
||||
|
||||
const token = localStorage.getItem('mcphub_token');
|
||||
|
||||
// First, check if server exists
|
||||
if (!forceOverride) {
|
||||
const checkResponse = await fetch(getApiUrl('/servers'), {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'x-auth-token': token || '',
|
||||
},
|
||||
});
|
||||
|
||||
if (checkResponse.ok) {
|
||||
const checkResult = await checkResponse.json();
|
||||
const existingServer = checkResult.data?.find((server: any) => server.name === serverName);
|
||||
|
||||
if (existingServer) {
|
||||
// Server exists, show confirmation dialog
|
||||
setPendingServerName(serverName);
|
||||
setShowConfirmDialog(true);
|
||||
setIsUploading(false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Install or override the server
|
||||
const method = forceOverride ? 'PUT' : 'POST';
|
||||
const url = forceOverride ? getApiUrl(`/servers/${encodeURIComponent(serverName)}`) : getApiUrl('/servers');
|
||||
|
||||
const response = await fetch(url, {
|
||||
method,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'x-auth-token': token || '',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
name: serverName,
|
||||
config: serverConfig,
|
||||
}),
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(result.message || `HTTP error! Status: ${response.status}`);
|
||||
}
|
||||
|
||||
if (result.success) {
|
||||
onSuccess(serverConfig);
|
||||
} else {
|
||||
throw new Error(result.message || t('dxt.installFailed'));
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('DXT install error:', err);
|
||||
setError(err instanceof Error ? err.message : t('dxt.installFailed'));
|
||||
setIsUploading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleConfirmOverride = () => {
|
||||
setShowConfirmDialog(false);
|
||||
if (pendingServerName) {
|
||||
handleInstallServer(pendingServerName, true);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCancelOverride = () => {
|
||||
setShowConfirmDialog(false);
|
||||
setPendingServerName('');
|
||||
setIsUploading(false);
|
||||
};
|
||||
|
||||
const convertDxtToMcpConfig = (manifest: any, extractPath: string, _serverName: string) => {
|
||||
const mcpConfig = manifest.server?.mcp_config || {};
|
||||
|
||||
// Convert DXT manifest to MCPHub stdio configuration
|
||||
const config: any = {
|
||||
type: 'stdio',
|
||||
command: mcpConfig.command || 'node',
|
||||
args: (mcpConfig.args || []).map((arg: string) =>
|
||||
arg.replace('${__dirname}', extractPath)
|
||||
),
|
||||
};
|
||||
|
||||
// Add environment variables if they exist
|
||||
if (mcpConfig.env && Object.keys(mcpConfig.env).length > 0) {
|
||||
config.env = { ...mcpConfig.env };
|
||||
|
||||
// Replace ${__dirname} in environment variables
|
||||
Object.keys(config.env).forEach(key => {
|
||||
if (typeof config.env[key] === 'string') {
|
||||
config.env[key] = config.env[key].replace('${__dirname}', extractPath);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return config;
|
||||
};
|
||||
|
||||
if (showServerForm && manifestData) {
|
||||
return (
|
||||
<>
|
||||
<ConfirmDialog
|
||||
isOpen={showConfirmDialog}
|
||||
onClose={handleCancelOverride}
|
||||
onConfirm={handleConfirmOverride}
|
||||
title={t('dxt.serverExistsTitle')}
|
||||
message={t('dxt.serverExistsConfirm', { serverName: pendingServerName })}
|
||||
confirmText={t('dxt.override')}
|
||||
cancelText={t('common.cancel')}
|
||||
variant="warning"
|
||||
/>
|
||||
|
||||
<div className={`fixed inset-0 bg-black/50 z-50 flex items-center justify-center p-4 ${showConfirmDialog ? 'opacity-50 pointer-events-none' : ''}`}>
|
||||
<div className="bg-white shadow rounded-lg p-6 w-full max-w-2xl max-h-screen overflow-y-auto">
|
||||
<div className="flex justify-between items-center mb-6">
|
||||
<h2 className="text-xl font-semibold text-gray-900">{t('dxt.installServer')}</h2>
|
||||
<button onClick={onCancel} className="text-gray-500 hover:text-gray-700">
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="mb-4 bg-red-50 border-l-4 border-red-500 p-4 rounded">
|
||||
<p className="text-red-700">{error}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-6">
|
||||
{/* Extension Info */}
|
||||
<div className="bg-gray-50 p-4 rounded-lg">
|
||||
<h3 className="font-medium text-gray-900 mb-2">{t('dxt.extensionInfo')}</h3>
|
||||
<div className="space-y-2 text-sm">
|
||||
<div><strong>{t('dxt.name')}:</strong> {manifestData.display_name || manifestData.name}</div>
|
||||
<div><strong>{t('dxt.version')}:</strong> {manifestData.version}</div>
|
||||
<div><strong>{t('dxt.description')}:</strong> {manifestData.description}</div>
|
||||
{manifestData.author && (
|
||||
<div><strong>{t('dxt.author')}:</strong> {manifestData.author.name}</div>
|
||||
)}
|
||||
{manifestData.tools && manifestData.tools.length > 0 && (
|
||||
<div>
|
||||
<strong>{t('dxt.tools')}:</strong>
|
||||
<ul className="list-disc list-inside ml-4">
|
||||
{manifestData.tools.map((tool: any, index: number) => (
|
||||
<li key={index}>{tool.name} - {tool.description}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Server Configuration */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
{t('dxt.serverName')}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="serverName"
|
||||
defaultValue={manifestData.name}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 form-input"
|
||||
placeholder={t('dxt.serverNamePlaceholder')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Action Buttons */}
|
||||
<div className="flex justify-end space-x-4">
|
||||
<button
|
||||
onClick={onCancel}
|
||||
disabled={isUploading}
|
||||
className="px-4 py-2 text-gray-700 bg-gray-200 rounded hover:bg-gray-300 disabled:opacity-50 btn-secondary"
|
||||
>
|
||||
{t('common.cancel')}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
const nameInput = document.getElementById('serverName') as HTMLInputElement;
|
||||
const serverName = nameInput?.value.trim() || manifestData.name;
|
||||
handleInstallServer(serverName);
|
||||
}}
|
||||
disabled={isUploading}
|
||||
className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 disabled:opacity-50 flex items-center btn-primary"
|
||||
>
|
||||
{isUploading ? (
|
||||
<>
|
||||
<svg className="animate-spin h-4 w-4 mr-2" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
|
||||
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
{t('dxt.installing')}
|
||||
</>
|
||||
) : (
|
||||
t('dxt.install')
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 bg-black/50 z-50 flex items-center justify-center p-4">
|
||||
<div className="bg-white shadow rounded-lg p-6 w-full max-w-lg">
|
||||
<div className="flex justify-between items-center mb-6">
|
||||
<h2 className="text-xl font-semibold text-gray-900">{t('dxt.uploadTitle')}</h2>
|
||||
<button onClick={onCancel} className="text-gray-500 hover:text-gray-700">
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="mb-4 bg-red-50 border-l-4 border-red-500 p-4 rounded">
|
||||
<p className="text-red-700">{error}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* File Drop Zone */}
|
||||
<div
|
||||
className={`relative border-2 border-dashed rounded-lg p-8 text-center transition-colors ${isDragging
|
||||
? 'border-blue-500 bg-blue-50'
|
||||
: selectedFile
|
||||
? 'border-gray-500 '
|
||||
: 'border-gray-300 hover:border-gray-400'
|
||||
}`}
|
||||
onDragOver={handleDragOver}
|
||||
onDragLeave={handleDragLeave}
|
||||
onDrop={handleDrop}
|
||||
>
|
||||
{selectedFile ? (
|
||||
<div className="space-y-2">
|
||||
<svg className="mx-auto h-12 w-12 text-green-200" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
<p className="text-sm text-gray-900 font-medium">{selectedFile.name}</p>
|
||||
<p className="text-xs text-gray-500">{(selectedFile.size / 1024 / 1024).toFixed(2)} MB</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
<svg className="mx-auto h-12 w-12 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4" />
|
||||
</svg>
|
||||
<div>
|
||||
<p className="text-sm text-gray-900">{t('dxt.dropFileHere')}</p>
|
||||
<p className="text-xs text-gray-500">{t('dxt.orClickToSelect')}</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<input
|
||||
type="file"
|
||||
accept=".dxt"
|
||||
onChange={handleFileSelect}
|
||||
className="absolute inset-0 w-full h-full opacity-0 cursor-pointer"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mt-6 flex justify-end space-x-4">
|
||||
<button
|
||||
onClick={onCancel}
|
||||
disabled={isUploading}
|
||||
className="px-4 py-2 text-gray-700 bg-gray-200 rounded hover:bg-gray-300 disabled:opacity-50 btn-secondary"
|
||||
>
|
||||
{t('common.cancel')}
|
||||
</button>
|
||||
<button
|
||||
onClick={handleUpload}
|
||||
disabled={!selectedFile || isUploading}
|
||||
className="px-4 py-2 text-white rounded hover:bg-blue-700 disabled:opacity-50 flex items-center btn-primary"
|
||||
>
|
||||
{isUploading ? (
|
||||
<>
|
||||
<svg className="animate-spin h-4 w-4 mr-2" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
|
||||
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
{t('dxt.uploading')}
|
||||
</>
|
||||
) : (
|
||||
t('dxt.upload')
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DxtUploadForm;
|
||||
@@ -1,9 +1,9 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Group, GroupFormData, Server } from '@/types'
|
||||
import { Group, GroupFormData, Server, IGroupServerConfig } from '@/types'
|
||||
import { useGroupData } from '@/hooks/useGroupData'
|
||||
import { useServerData } from '@/hooks/useServerData'
|
||||
import { ToggleGroup } from './ui/ToggleGroup'
|
||||
import { ServerToolConfig } from './ServerToolConfig'
|
||||
|
||||
interface EditGroupFormProps {
|
||||
group: Group
|
||||
@@ -38,18 +38,6 @@ const EditGroupForm = ({ group, onEdit, onCancel }: EditGroupFormProps) => {
|
||||
}))
|
||||
}
|
||||
|
||||
const handleServerToggle = (serverName: string) => {
|
||||
setFormData(prev => {
|
||||
const isSelected = prev.servers.includes(serverName)
|
||||
return {
|
||||
...prev,
|
||||
servers: isSelected
|
||||
? prev.servers.filter(name => name !== serverName)
|
||||
: [...prev.servers, serverName]
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
setIsSubmitting(true)
|
||||
@@ -67,7 +55,7 @@ const EditGroupForm = ({ group, onEdit, onCancel }: EditGroupFormProps) => {
|
||||
description: formData.description,
|
||||
servers: formData.servers
|
||||
})
|
||||
|
||||
|
||||
if (!result) {
|
||||
setError(t('groups.updateError'))
|
||||
setIsSubmitting(false)
|
||||
@@ -83,64 +71,68 @@ const EditGroupForm = ({ group, onEdit, onCancel }: EditGroupFormProps) => {
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 bg-black/50 z-50 flex items-center justify-center p-4">
|
||||
<div className="bg-white rounded-lg shadow-lg max-w-md w-full">
|
||||
<div className="p-6">
|
||||
<div className="bg-white rounded-lg shadow-lg max-w-2xl w-full max-h-[90vh] flex flex-col">
|
||||
<div className="p-6 flex-shrink-0">
|
||||
<h2 className="text-xl font-semibold text-gray-800 mb-4">{t('groups.edit')}</h2>
|
||||
|
||||
|
||||
{error && (
|
||||
<div className="mb-4 p-3 bg-red-100 text-red-700 rounded">
|
||||
<div className="mb-4 p-3 bg-red-100 text-red-700 rounded-md border border-gray-200">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="mb-4">
|
||||
<label className="block text-gray-700 text-sm font-bold mb-2" htmlFor="name">
|
||||
{t('groups.name')} *
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="name"
|
||||
name="name"
|
||||
value={formData.name}
|
||||
onChange={handleChange}
|
||||
className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
|
||||
placeholder={t('groups.namePlaceholder')}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<ToggleGroup
|
||||
className="mb-6"
|
||||
label={t('groups.servers')}
|
||||
noOptionsText={t('groups.noServerOptions')}
|
||||
values={formData.servers}
|
||||
options={availableServers.map(server => ({
|
||||
value: server.name,
|
||||
label: server.name
|
||||
}))}
|
||||
onChange={(servers) => setFormData(prev => ({ ...prev, servers }))}
|
||||
/>
|
||||
|
||||
<div className="flex justify-end space-x-3">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onCancel}
|
||||
className="px-4 py-2 text-gray-600 hover:text-gray-800"
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
{t('common.cancel')}
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 disabled:opacity-50"
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
{isSubmitting ? t('common.submitting') : t('common.save')}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit} className="flex flex-col flex-1 min-h-0">
|
||||
<div className="flex-1 overflow-y-auto px-6">
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-gray-700 text-sm font-bold mb-2" htmlFor="name">
|
||||
{t('groups.name')} *
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="name"
|
||||
name="name"
|
||||
value={formData.name}
|
||||
onChange={handleChange}
|
||||
className="w-full border border-gray-300 rounded-md px-3 py-2 text-gray-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
placeholder={t('groups.namePlaceholder')}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-gray-700 text-sm font-bold mb-2">
|
||||
{t('groups.configureTools')}
|
||||
</label>
|
||||
<ServerToolConfig
|
||||
servers={availableServers}
|
||||
value={formData.servers as IGroupServerConfig[]}
|
||||
onChange={(servers) => setFormData(prev => ({ ...prev, servers }))}
|
||||
className="border border-gray-200 rounded-lg p-4 bg-gray-50"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end space-x-3 p-6 pt-4 border-t border-gray-200 flex-shrink-0">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onCancel}
|
||||
className="px-4 py-2 text-gray-600 hover:text-gray-800 border border-gray-300 rounded-md hover:bg-gray-50 transition-colors"
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
{t('common.cancel')}
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
className="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 disabled:opacity-50 transition-colors"
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
{isSubmitting ? t('common.submitting') : t('common.save')}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Server } from '@/types'
|
||||
import { getApiUrl } from '../utils/runtime'
|
||||
import ServerForm from './ServerForm'
|
||||
|
||||
interface EditServerFormProps {
|
||||
@@ -17,7 +18,7 @@ const EditServerForm = ({ server, onEdit, onCancel }: EditServerFormProps) => {
|
||||
try {
|
||||
setError(null)
|
||||
const token = localStorage.getItem('mcphub_token');
|
||||
const response = await fetch(`/api/servers/${server.name}`, {
|
||||
const response = await fetch(getApiUrl(`/servers/${server.name}`), {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { useState } from 'react'
|
||||
import { useState, useRef, useEffect } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Group, Server } from '@/types'
|
||||
import { Edit, Trash, Copy, Check } from '@/components/icons/LucideIcons'
|
||||
import { Group, Server, IGroupServerConfig } from '@/types'
|
||||
import { Edit, Trash, Copy, Check, Link, FileCode, DropdownIcon, Wrench } from '@/components/icons/LucideIcons'
|
||||
import DeleteDialog from '@/components/ui/DeleteDialog'
|
||||
import { useToast } from '@/contexts/ToastContext'
|
||||
import { useSettingsData } from '@/hooks/useSettingsData'
|
||||
|
||||
interface GroupCardProps {
|
||||
group: Group
|
||||
@@ -20,8 +21,26 @@ const GroupCard = ({
|
||||
}: GroupCardProps) => {
|
||||
const { t } = useTranslation()
|
||||
const { showToast } = useToast()
|
||||
const { installConfig } = useSettingsData()
|
||||
const [showDeleteDialog, setShowDeleteDialog] = useState(false)
|
||||
const [copied, setCopied] = useState(false)
|
||||
const [showCopyDropdown, setShowCopyDropdown] = useState(false)
|
||||
const [expandedServer, setExpandedServer] = useState<string | null>(null)
|
||||
const dropdownRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
// Close dropdown when clicking outside
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
|
||||
setShowCopyDropdown(false)
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('mousedown', handleClickOutside)
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', handleClickOutside)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const handleEdit = () => {
|
||||
onEdit(group)
|
||||
@@ -36,16 +55,18 @@ const GroupCard = ({
|
||||
setShowDeleteDialog(false)
|
||||
}
|
||||
|
||||
const copyToClipboard = () => {
|
||||
const copyToClipboard = (text: string) => {
|
||||
if (navigator.clipboard && window.isSecureContext) {
|
||||
navigator.clipboard.writeText(group.id).then(() => {
|
||||
navigator.clipboard.writeText(text).then(() => {
|
||||
setCopied(true)
|
||||
setShowCopyDropdown(false)
|
||||
showToast(t('common.copySuccess'), 'success')
|
||||
setTimeout(() => setCopied(false), 2000)
|
||||
})
|
||||
} else {
|
||||
// Fallback for HTTP or unsupported clipboard API
|
||||
const textArea = document.createElement('textarea')
|
||||
textArea.value = group.id
|
||||
textArea.value = text
|
||||
// Avoid scrolling to bottom
|
||||
textArea.style.position = 'fixed'
|
||||
textArea.style.left = '-9999px'
|
||||
@@ -55,6 +76,8 @@ const GroupCard = ({
|
||||
try {
|
||||
document.execCommand('copy')
|
||||
setCopied(true)
|
||||
setShowCopyDropdown(false)
|
||||
showToast(t('common.copySuccess'), 'success')
|
||||
setTimeout(() => setCopied(false), 2000)
|
||||
} catch (err) {
|
||||
showToast(t('common.copyFailed') || 'Copy failed', 'error')
|
||||
@@ -64,24 +87,92 @@ const GroupCard = ({
|
||||
}
|
||||
}
|
||||
|
||||
const handleCopyId = () => {
|
||||
copyToClipboard(group.id)
|
||||
}
|
||||
|
||||
const handleCopyUrl = () => {
|
||||
copyToClipboard(`${installConfig.baseUrl}/mcp/${group.id}`)
|
||||
}
|
||||
|
||||
const handleCopyJson = () => {
|
||||
const jsonConfig = {
|
||||
mcpServers: {
|
||||
mcphub: {
|
||||
url: `${installConfig.baseUrl}/mcp/${group.id}`,
|
||||
headers: {
|
||||
Authorization: "Bearer <your-access-token>"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
copyToClipboard(JSON.stringify(jsonConfig, null, 2))
|
||||
}
|
||||
|
||||
// Helper function to normalize group servers to get server names
|
||||
const getServerNames = (servers: string[] | IGroupServerConfig[]): string[] => {
|
||||
return servers.map(server => typeof server === 'string' ? server : server.name);
|
||||
};
|
||||
|
||||
// Helper function to get server configuration
|
||||
const getServerConfig = (serverName: string): IGroupServerConfig | undefined => {
|
||||
const server = group.servers.find(s =>
|
||||
typeof s === 'string' ? s === serverName : s.name === serverName
|
||||
);
|
||||
if (typeof server === 'string') {
|
||||
return { name: server, tools: 'all' };
|
||||
}
|
||||
return server;
|
||||
};
|
||||
|
||||
// Get servers that belong to this group
|
||||
const groupServers = servers.filter(server => group.servers.includes(server.name))
|
||||
const serverNames = getServerNames(group.servers);
|
||||
const groupServers = servers.filter(server => serverNames.includes(server.name));
|
||||
|
||||
return (
|
||||
<div className="bg-white shadow rounded-lg p-6">
|
||||
<div className="bg-white shadow rounded-lg p-6 ">
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<div>
|
||||
<div className="flex items-center">
|
||||
<h2 className="text-xl font-semibold text-gray-800">{group.name}</h2>
|
||||
<div className="flex items-center ml-3">
|
||||
<span className="text-xs text-gray-500 mr-1">{group.id}</span>
|
||||
<button
|
||||
onClick={copyToClipboard}
|
||||
className="p-1 text-gray-400 hover:text-gray-600 transition-colors"
|
||||
title={t('common.copy')}
|
||||
>
|
||||
{copied ? <Check size={14} className="text-green-500" /> : <Copy size={14} />}
|
||||
</button>
|
||||
<div className="relative" ref={dropdownRef}>
|
||||
<button
|
||||
onClick={() => setShowCopyDropdown(!showCopyDropdown)}
|
||||
className="p-1 text-gray-400 hover:text-gray-600 transition-colors flex items-center"
|
||||
title={t('common.copy')}
|
||||
>
|
||||
{copied ? <Check size={14} className="text-green-500" /> : <Copy size={14} />}
|
||||
<DropdownIcon size={12} className="ml-1" />
|
||||
</button>
|
||||
|
||||
{showCopyDropdown && (
|
||||
<div className="absolute top-full left-0 mt-1 bg-white shadow-lg rounded-md border border-gray-200 py-1 z-10 min-w-[140px]">
|
||||
<button
|
||||
onClick={handleCopyId}
|
||||
className="w-full px-3 py-2 text-left text-sm text-gray-700 hover:bg-gray-100 flex items-center"
|
||||
>
|
||||
<Copy size={12} className="mr-2" />
|
||||
{t('common.copyId')}
|
||||
</button>
|
||||
<button
|
||||
onClick={handleCopyUrl}
|
||||
className="w-full px-3 py-2 text-left text-sm text-gray-700 hover:bg-gray-100 flex items-center"
|
||||
>
|
||||
<Link size={12} className="mr-2" />
|
||||
{t('common.copyUrl')}
|
||||
</button>
|
||||
<button
|
||||
onClick={handleCopyJson}
|
||||
className="w-full px-3 py-2 text-left text-sm text-gray-700 hover:bg-gray-100 flex items-center"
|
||||
>
|
||||
<FileCode size={12} className="mr-2" />
|
||||
{t('common.copyJson')}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{group.description && (
|
||||
@@ -89,7 +180,7 @@ const GroupCard = ({
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="bg-blue-50 text-blue-700 px-3 py-1 rounded-full text-sm">
|
||||
<div className="bg-blue-50 text-blue-700 px-3 py-1 rounded-full text-sm btn-secondary">
|
||||
{t('groups.serverCount', { count: group.servers.length })}
|
||||
</div>
|
||||
<button
|
||||
@@ -113,18 +204,68 @@ const GroupCard = ({
|
||||
{groupServers.length === 0 ? (
|
||||
<p className="text-gray-500 italic">{t('groups.noServers')}</p>
|
||||
) : (
|
||||
<div className="flex flex-wrap gap-2 mt-2">
|
||||
{groupServers.map(server => (
|
||||
<div
|
||||
key={server.name}
|
||||
className="inline-flex items-center px-3 py-1 bg-gray-50 rounded"
|
||||
>
|
||||
<span className="font-medium text-gray-700 text-sm">{server.name}</span>
|
||||
<span className={`ml-2 inline-block h-2 w-2 rounded-full ${server.status === 'connected' ? 'bg-green-500' :
|
||||
server.status === 'connecting' ? 'bg-yellow-500' : 'bg-red-500'
|
||||
}`}></span>
|
||||
</div>
|
||||
))}
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{groupServers.map(server => {
|
||||
const serverConfig = getServerConfig(server.name);
|
||||
const hasToolRestrictions = serverConfig && serverConfig.tools !== 'all' && Array.isArray(serverConfig.tools);
|
||||
const toolCount = hasToolRestrictions && Array.isArray(serverConfig?.tools)
|
||||
? serverConfig.tools.length
|
||||
: (server.tools?.length || 0); // Show total tool count when all tools are selected
|
||||
|
||||
const isExpanded = expandedServer === server.name;
|
||||
|
||||
// Get tools list for display
|
||||
const getToolsList = () => {
|
||||
if (hasToolRestrictions && Array.isArray(serverConfig?.tools)) {
|
||||
return serverConfig.tools;
|
||||
} else if (server.tools && server.tools.length > 0) {
|
||||
return server.tools.map(tool => tool.name);
|
||||
}
|
||||
return [];
|
||||
};
|
||||
|
||||
const handleServerClick = () => {
|
||||
setExpandedServer(isExpanded ? null : server.name);
|
||||
};
|
||||
|
||||
return (
|
||||
<div key={server.name} className="relative">
|
||||
<div
|
||||
className="flex items-center space-x-2 bg-gray-50 rounded-lg px-3 py-2 cursor-pointer hover:bg-gray-100 transition-colors"
|
||||
onClick={handleServerClick}
|
||||
>
|
||||
<span className="font-medium text-gray-700 text-sm">{server.name}</span>
|
||||
<span className={`inline-block h-2 w-2 rounded-full ${server.status === 'connected' ? 'bg-green-500' :
|
||||
server.status === 'connecting' ? 'bg-yellow-500' : 'bg-red-500'
|
||||
}`}></span>
|
||||
{toolCount > 0 && (
|
||||
<span className="text-xs text-blue-600 bg-blue-100 px-2 py-0.5 rounded flex items-center gap-1">
|
||||
<Wrench size={12} />
|
||||
{toolCount}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{isExpanded && (
|
||||
<div className="absolute top-full left-0 mt-1 bg-white shadow-lg rounded-md border border-gray-200 p-3 z-10 min-w-[300px] max-w-[400px]">
|
||||
<div className="text-gray-600 text-xs mb-2">
|
||||
{hasToolRestrictions ? t('groups.selectedTools') : t('groups.allTools')}:
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{getToolsList().map((toolName, index) => (
|
||||
<span
|
||||
key={index}
|
||||
className="inline-block bg-gray-100 text-gray-700 px-2 py-1 rounded text-xs"
|
||||
>
|
||||
{toolName}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -48,25 +48,26 @@ const LogViewer: React.FC<LogViewerProps> = ({ logs, isLoading = false, error =
|
||||
// Get badge color based on log type
|
||||
const getLogTypeColor = (type: string) => {
|
||||
switch (type) {
|
||||
case 'error': return 'bg-red-400';
|
||||
case 'warn': return 'bg-yellow-400';
|
||||
case 'debug': return 'bg-purple-400';
|
||||
default: return 'bg-blue-400';
|
||||
case 'error': return 'bg-red-400/80 text-white';
|
||||
case 'warn': return 'bg-yellow-400/80 text-gray-900';
|
||||
case 'debug': return 'bg-purple-400/80 text-white';
|
||||
case 'info': return 'bg-blue-400/80 text-white';
|
||||
default: return 'bg-blue-400/80 text-white';
|
||||
}
|
||||
};
|
||||
|
||||
// Get badge color based on log source
|
||||
const getSourceColor = (source: string) => {
|
||||
switch (source) {
|
||||
case 'main': return 'bg-green-400';
|
||||
case 'child': return 'bg-orange-400';
|
||||
default: return 'bg-gray-400';
|
||||
case 'main': return 'bg-green-400/80 text-white';
|
||||
case 'child': return 'bg-orange-400/80 text-white';
|
||||
default: return 'bg-gray-400/80 text-white';
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full">
|
||||
<div className="bg-card p-3 rounded-t-md border-b flex flex-wrap items-center justify-between gap-2">
|
||||
<div className="bg-card p-3 rounded-t-md flex flex-wrap items-center justify-between gap-2">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<span className="font-semibold text-sm">{t('logs.filters')}:</span>
|
||||
|
||||
@@ -74,14 +75,14 @@ const LogViewer: React.FC<LogViewerProps> = ({ logs, isLoading = false, error =
|
||||
<input
|
||||
type="text"
|
||||
placeholder={t('logs.search')}
|
||||
className="px-2 py-1 text-sm border rounded"
|
||||
className="shadow appearance-none border border-gray-200 rounded py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline form-input"
|
||||
value={filter}
|
||||
onChange={(e) => setFilter(e.target.value)}
|
||||
/>
|
||||
|
||||
{/* Log type filters */}
|
||||
<div className="flex gap-1 items-center">
|
||||
{(['info', 'error', 'warn', 'debug'] as const).map(type => (
|
||||
{(['debug', 'info', 'error', 'warn'] as const).map(type => (
|
||||
<Badge
|
||||
key={type}
|
||||
variant={typeFilter.includes(type) ? 'default' : 'outline'}
|
||||
@@ -134,6 +135,7 @@ const LogViewer: React.FC<LogViewerProps> = ({ logs, isLoading = false, error =
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={onClear}
|
||||
className='btn-secondary'
|
||||
disabled={isLoading || logs.length === 0}
|
||||
>
|
||||
{t('logs.clearLogs')}
|
||||
@@ -164,7 +166,7 @@ const LogViewer: React.FC<LogViewerProps> = ({ logs, isLoading = false, error =
|
||||
filteredLogs.map((log, index) => (
|
||||
<div
|
||||
key={`${log.timestamp}-${index}`}
|
||||
className={`py-1 border-b border-gray-100 dark:border-gray-800 ${log.type === 'error' ? 'text-red-500' :
|
||||
className={`py-1 ${log.type === 'error' ? 'text-red-500' :
|
||||
log.type === 'warn' ? 'text-yellow-500' : ''
|
||||
}`}
|
||||
>
|
||||
|
||||
@@ -15,31 +15,31 @@ const MarketServerCard: React.FC<MarketServerCardProps> = ({ server, onClick })
|
||||
if (!server.tags || server.tags.length === 0) {
|
||||
return { tagsToShow: [], hasMore: false, moreCount: 0 };
|
||||
}
|
||||
|
||||
|
||||
// Estimate available width in the card (in characters)
|
||||
const estimatedAvailableWidth = 28; // Estimated number of characters that can fit in one line
|
||||
|
||||
|
||||
// Calculate the character space needed for tags and plus sign (including # and spacing)
|
||||
const calculateTagWidth = (tag: string) => tag.length + 3; // +3 for # and spacing
|
||||
|
||||
|
||||
// Loop to determine the maximum number of tags that can be displayed
|
||||
let totalWidth = 0;
|
||||
let i = 0;
|
||||
|
||||
|
||||
// First, sort tags by length to prioritize displaying shorter tags
|
||||
const sortedTags = [...server.tags].sort((a, b) => a.length - b.length);
|
||||
|
||||
|
||||
// Calculate how many tags can fit
|
||||
for (i = 0; i < sortedTags.length; i++) {
|
||||
const tagWidth = calculateTagWidth(sortedTags[i]);
|
||||
|
||||
|
||||
// If this tag would make the total width exceed available width, stop adding
|
||||
if (totalWidth + tagWidth > estimatedAvailableWidth) {
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
totalWidth += tagWidth;
|
||||
|
||||
|
||||
// If this is the last tag but there's still space, no need to show "more"
|
||||
if (i === sortedTags.length - 1) {
|
||||
return {
|
||||
@@ -49,16 +49,16 @@ const MarketServerCard: React.FC<MarketServerCardProps> = ({ server, onClick })
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// If there's not enough space to display any tags, show at least one
|
||||
if (i === 0 && sortedTags.length > 0) {
|
||||
i = 1;
|
||||
}
|
||||
|
||||
|
||||
// Calculate space needed for the "more" tag
|
||||
const moreCount = sortedTags.length - i;
|
||||
const moreTagWidth = 3 + String(moreCount).length + t('market.moreTags').length;
|
||||
|
||||
|
||||
// If there's enough remaining space to display the "more" tag
|
||||
if (totalWidth + moreTagWidth <= estimatedAvailableWidth || i < 1) {
|
||||
return {
|
||||
@@ -67,7 +67,7 @@ const MarketServerCard: React.FC<MarketServerCardProps> = ({ server, onClick })
|
||||
moreCount
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
// If there's not enough space for even the "more" tag, reduce one tag to make room
|
||||
return {
|
||||
tagsToShow: sortedTags.slice(0, Math.max(1, i - 1)),
|
||||
@@ -79,27 +79,27 @@ const MarketServerCard: React.FC<MarketServerCardProps> = ({ server, onClick })
|
||||
const { tagsToShow, hasMore, moreCount } = getTagsToDisplay();
|
||||
|
||||
return (
|
||||
<div
|
||||
className="bg-white rounded-lg shadow-md p-5 hover:shadow-lg transition-shadow cursor-pointer flex flex-col h-full"
|
||||
<div
|
||||
className="bg-white rounded-lg shadow-md p-5 hover:shadow-lg transition-all duration-200 cursor-pointer flex flex-col h-full page-card"
|
||||
onClick={() => onClick(server)}
|
||||
>
|
||||
<div className="flex justify-between items-start mb-3">
|
||||
<h3 className="text-lg font-semibold text-gray-900 line-clamp-1 mr-2">{server.display_name}</h3>
|
||||
{server.is_official && (
|
||||
<span className="bg-blue-100 text-blue-800 text-xs font-medium px-2.5 py-0.5 rounded flex-shrink-0">
|
||||
<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>
|
||||
|
||||
|
||||
{/* Categories */}
|
||||
<div className="flex flex-wrap gap-1 mb-2 min-h-[28px]">
|
||||
{server.categories?.length > 0 ? (
|
||||
server.categories.map((category, index) => (
|
||||
<span
|
||||
<span
|
||||
key={index}
|
||||
className="bg-gray-100 text-gray-800 text-xs px-2 py-1 rounded whitespace-nowrap"
|
||||
className="bg-gray-100 text-gray-800 text-xs px-2 py-1.5 rounded whitespace-nowrap"
|
||||
>
|
||||
{category}
|
||||
</span>
|
||||
@@ -108,15 +108,15 @@ const MarketServerCard: React.FC<MarketServerCardProps> = ({ server, onClick })
|
||||
<span className="text-xs text-gray-400 py-1">-</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
||||
{/* Tags */}
|
||||
<div className="relative mb-3 min-h-[28px] overflow-x-auto">
|
||||
{server.tags?.length > 0 ? (
|
||||
<div className="flex gap-1 items-center whitespace-nowrap">
|
||||
{tagsToShow.map((tag, index) => (
|
||||
<span
|
||||
<span
|
||||
key={index}
|
||||
className="bg-green-50 text-green-700 text-xs px-2 py-1 rounded flex-shrink-0"
|
||||
className="bg-green-50 text-green-700 text-xs px-2 py-1 rounded flex-shrink-0 label-secondary"
|
||||
>
|
||||
#{tag}
|
||||
</span>
|
||||
@@ -131,8 +131,8 @@ const MarketServerCard: React.FC<MarketServerCardProps> = ({ server, onClick })
|
||||
<span className="text-xs text-gray-400 py-1">-</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between items-center mt-auto pt-2 text-xs text-gray-500 border-t border-gray-100">
|
||||
|
||||
<div className="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">
|
||||
|
||||
@@ -2,11 +2,14 @@ import React, { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { MarketServer, MarketServerInstallation } from '@/types';
|
||||
import ServerForm from './ServerForm';
|
||||
import { detectVariables } from '../utils/variableDetection';
|
||||
|
||||
import { ServerConfig } from '@/types';
|
||||
|
||||
interface MarketServerDetailProps {
|
||||
server: MarketServer;
|
||||
onBack: () => void;
|
||||
onInstall: (server: MarketServer) => void;
|
||||
onInstall: (server: MarketServer, config: ServerConfig) => void;
|
||||
installing?: boolean;
|
||||
isInstalled?: boolean;
|
||||
}
|
||||
@@ -21,6 +24,9 @@ const MarketServerDetail: React.FC<MarketServerDetailProps> = ({
|
||||
const { t } = useTranslation();
|
||||
const [modalVisible, setModalVisible] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [confirmationVisible, setConfirmationVisible] = useState(false);
|
||||
const [pendingPayload, setPendingPayload] = useState<any>(null);
|
||||
const [detectedVariables, setDetectedVariables] = useState<string[]>([]);
|
||||
|
||||
// Helper function to determine button state
|
||||
const getButtonProps = () => {
|
||||
@@ -38,7 +44,7 @@ const MarketServerDetail: React.FC<MarketServerDetailProps> = ({
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
className: "bg-blue-600 hover:bg-blue-700 px-4 py-2 rounded text-sm font-medium text-white",
|
||||
className: "bg-blue-600 hover:bg-blue-700 px-4 py-2 rounded text-sm font-medium text-white btn-primary",
|
||||
disabled: false,
|
||||
text: t('market.install')
|
||||
};
|
||||
@@ -48,6 +54,27 @@ const MarketServerDetail: React.FC<MarketServerDetailProps> = ({
|
||||
const toggleModal = () => {
|
||||
setModalVisible(!modalVisible);
|
||||
setError(null); // Clear any previous errors when toggling modal
|
||||
setConfirmationVisible(false);
|
||||
setPendingPayload(null);
|
||||
};
|
||||
|
||||
const handleConfirmInstall = async () => {
|
||||
if (pendingPayload) {
|
||||
await proceedWithInstall(pendingPayload);
|
||||
setConfirmationVisible(false);
|
||||
setPendingPayload(null);
|
||||
}
|
||||
};
|
||||
|
||||
const proceedWithInstall = async (payload: any) => {
|
||||
try {
|
||||
setError(null);
|
||||
onInstall(server, payload.config);
|
||||
setModalVisible(false);
|
||||
} catch (err) {
|
||||
console.error('Error installing server:', err);
|
||||
setError(t('errors.serverInstall'));
|
||||
}
|
||||
};
|
||||
|
||||
const handleInstall = () => {
|
||||
@@ -70,24 +97,32 @@ const MarketServerDetail: React.FC<MarketServerDetailProps> = ({
|
||||
} else if (server.installations.default) {
|
||||
return server.installations.default;
|
||||
}
|
||||
|
||||
|
||||
// If none of the preferred types are available, get the first available installation type
|
||||
const installTypes = Object.keys(server.installations);
|
||||
if (installTypes.length > 0) {
|
||||
return server.installations[installTypes[0]];
|
||||
}
|
||||
|
||||
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const handleSubmit = async (payload: any) => {
|
||||
try {
|
||||
setError(null);
|
||||
// Pass the server object to the parent component for installation
|
||||
onInstall(server);
|
||||
setModalVisible(false);
|
||||
// Check for variables in the payload
|
||||
const variables = detectVariables(payload);
|
||||
|
||||
if (variables.length > 0) {
|
||||
// Show confirmation dialog
|
||||
setDetectedVariables(variables);
|
||||
setPendingPayload(payload);
|
||||
setConfirmationVisible(true);
|
||||
} else {
|
||||
// Install directly if no variables found
|
||||
await proceedWithInstall(payload);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error installing server:', err);
|
||||
console.error('Error processing server installation:', err);
|
||||
setError(t('errors.serverInstall'));
|
||||
}
|
||||
};
|
||||
@@ -112,15 +147,15 @@ const MarketServerDetail: React.FC<MarketServerDetailProps> = ({
|
||||
<div className="flex justify-between items-start mb-4">
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold text-gray-900 flex items-center flex-wrap">
|
||||
{server.display_name}
|
||||
{server.display_name}
|
||||
<span className="text-sm font-normal text-gray-500 ml-2">({server.name})</span>
|
||||
<span className="text-sm font-normal text-gray-600 ml-4">
|
||||
{t('market.author')}: {server.author.name} • {t('market.license')}: {server.license} •
|
||||
{t('market.author')}: {server.author.name} • {t('market.license')}: {server.license} •
|
||||
<a
|
||||
href={server.repository.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-blue-600 hover:underline ml-1"
|
||||
className="text-blue-500 hover:underline ml-1"
|
||||
>
|
||||
{t('market.repository')}
|
||||
</a>
|
||||
@@ -130,7 +165,7 @@ const MarketServerDetail: React.FC<MarketServerDetailProps> = ({
|
||||
|
||||
<div className="flex items-center">
|
||||
{server.is_official && (
|
||||
<span className="bg-blue-100 text-blue-800 text-sm font-medium px-4 py-2 rounded mr-2 flex items-center">
|
||||
<span className="bg-blue-100 text-blue-800 text-sm font-normal px-4 py-2 rounded mr-2 flex items-center label-primary">
|
||||
{t('market.official')}
|
||||
</span>
|
||||
)}
|
||||
@@ -167,7 +202,7 @@ const MarketServerDetail: React.FC<MarketServerDetailProps> = ({
|
||||
<h3 className="text-lg font-semibold mb-3">{t('market.arguments')}</h3>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="min-w-full divide-y divide-gray-200">
|
||||
<thead className="bg-gray-50">
|
||||
<thead className="bg-gray-50 border-b border-gray-200">
|
||||
<tr>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider whitespace-nowrap">
|
||||
{t('market.argumentName')}
|
||||
@@ -196,7 +231,7 @@ const MarketServerDetail: React.FC<MarketServerDetailProps> = ({
|
||||
{arg.required ? (
|
||||
<span className="text-green-600">✓</span>
|
||||
) : (
|
||||
<span className="text-red-600">✗</span>
|
||||
<span className="text-gray-600">✗</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||
@@ -226,7 +261,7 @@ const MarketServerDetail: React.FC<MarketServerDetailProps> = ({
|
||||
element.classList.toggle('hidden');
|
||||
}
|
||||
}}
|
||||
className="text-sm text-blue-600 hover:underline focus:outline-none ml-2"
|
||||
className="text-sm text-blue-500 font-normal hover:underline focus:outline-none ml-2"
|
||||
>
|
||||
{t('market.viewSchema')}
|
||||
</button>
|
||||
@@ -279,19 +314,73 @@ const MarketServerDetail: React.FC<MarketServerDetailProps> = ({
|
||||
initialData={{
|
||||
name: server.name,
|
||||
status: 'disconnected',
|
||||
config: preferredInstallation
|
||||
config: preferredInstallation
|
||||
? {
|
||||
command: preferredInstallation.command || '',
|
||||
args: preferredInstallation.args || [],
|
||||
env: preferredInstallation.env || {}
|
||||
}
|
||||
command: preferredInstallation.command || '',
|
||||
args: preferredInstallation.args || [],
|
||||
env: preferredInstallation.env || {}
|
||||
}
|
||||
: undefined
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{confirmationVisible && (
|
||||
<div className="fixed inset-0 bg-black/50 z-[60] flex items-center justify-center p-4">
|
||||
<div className="bg-white rounded-lg p-6 w-full max-w-md">
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-4">
|
||||
{t('server.confirmVariables')}
|
||||
</h3>
|
||||
<p className="text-gray-600 mb-4">
|
||||
{t('server.variablesDetected')}
|
||||
</p>
|
||||
<div className="bg-yellow-50 border border-yellow-200 rounded p-3 mb-4">
|
||||
<div className="flex items-start">
|
||||
<div className="flex-shrink-0">
|
||||
<svg className="h-5 w-5 text-yellow-400" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fillRule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clipRule="evenodd" />
|
||||
</svg>
|
||||
</div>
|
||||
<div className="ml-3">
|
||||
<h4 className="text-sm font-medium text-yellow-800">
|
||||
{t('server.detectedVariables')}:
|
||||
</h4>
|
||||
<ul className="mt-1 text-sm text-yellow-700">
|
||||
{detectedVariables.map((variable, index) => (
|
||||
<li key={index} className="font-mono">
|
||||
${`{${variable}}`}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-gray-600 text-sm mb-6">
|
||||
{t('market.confirmVariablesMessage')}
|
||||
</p>
|
||||
<div className="flex justify-end space-x-3">
|
||||
<button
|
||||
onClick={() => {
|
||||
setConfirmationVisible(false)
|
||||
setPendingPayload(null)
|
||||
}}
|
||||
className="px-4 py-2 text-gray-600 border border-gray-300 rounded hover:bg-gray-50 btn-secondary"
|
||||
>
|
||||
{t('common.cancel')}
|
||||
</button>
|
||||
<button
|
||||
onClick={handleConfirmInstall}
|
||||
className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 btn-primary"
|
||||
>
|
||||
{t('market.confirmAndInstall')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default MarketServerDetail;
|
||||
export default MarketServerDetail;
|
||||
|
||||
95
frontend/src/components/PermissionChecker.tsx
Normal file
@@ -0,0 +1,95 @@
|
||||
import React from 'react';
|
||||
import { useAuth } from '../contexts/AuthContext';
|
||||
|
||||
interface PermissionCheckerProps {
|
||||
permissions: string | string[];
|
||||
fallback?: React.ReactNode;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Permission checker component for conditional rendering
|
||||
* @param permissions Required permissions, supports single permission string or permission array
|
||||
* @param fallback Content to show when permission is denied, defaults to null
|
||||
* @param children Content to show when permission is granted
|
||||
*/
|
||||
export const PermissionChecker: React.FC<PermissionCheckerProps> = ({
|
||||
permissions,
|
||||
fallback = null,
|
||||
children,
|
||||
}) => {
|
||||
const hasPermission = usePermissionCheck(permissions);
|
||||
|
||||
return hasPermission ? <>{children}</> : <>{fallback}</>;
|
||||
};
|
||||
|
||||
/**
|
||||
* Permission check hook
|
||||
* @param requiredPermissions Permissions to check
|
||||
* @returns Whether user has permission
|
||||
*/
|
||||
export const usePermissionCheck = (requiredPermissions: string | string[]): boolean => {
|
||||
const { auth } = useAuth();
|
||||
|
||||
if (!auth.isAuthenticated || !auth.user) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const userPermissions = auth.user.permissions || [];
|
||||
|
||||
if (requiredPermissions === 'x' && !userPermissions.includes('x')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If user has '*' permission, they have all permissions
|
||||
if (userPermissions.includes('*')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// If user is admin, they have all permissions by default
|
||||
if (auth.user.isAdmin) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Normalize required permissions to array
|
||||
const permissionsToCheck = Array.isArray(requiredPermissions)
|
||||
? requiredPermissions
|
||||
: [requiredPermissions];
|
||||
|
||||
// Check if user has any of the required permissions
|
||||
return permissionsToCheck.some(permission =>
|
||||
userPermissions.includes(permission)
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Permission check hook - requires all permissions
|
||||
* @param requiredPermissions Array of permissions to check
|
||||
* @returns Whether user has all permissions
|
||||
*/
|
||||
export const usePermissionCheckAll = (requiredPermissions: string[]): boolean => {
|
||||
const { auth } = useAuth();
|
||||
|
||||
if (!auth.isAuthenticated || !auth.user) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const userPermissions = auth.user.permissions || [];
|
||||
|
||||
// If user has '*' permission, they have all permissions
|
||||
if (userPermissions.includes('*')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// If user is admin, they have all permissions by default
|
||||
if (auth.user.isAdmin) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if user has all required permissions
|
||||
return requiredPermissions.every(permission =>
|
||||
userPermissions.includes(permission)
|
||||
);
|
||||
};
|
||||
|
||||
export default PermissionChecker;
|
||||
@@ -7,20 +7,20 @@ interface ProtectedRouteProps {
|
||||
redirectPath?: string;
|
||||
}
|
||||
|
||||
const ProtectedRoute: React.FC<ProtectedRouteProps> = ({
|
||||
redirectPath = '/login'
|
||||
const ProtectedRoute: React.FC<ProtectedRouteProps> = ({
|
||||
redirectPath = '/login'
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const { auth } = useAuth();
|
||||
|
||||
|
||||
if (auth.loading) {
|
||||
return <div className="flex items-center justify-center h-screen">{t('app.loading')}</div>;
|
||||
}
|
||||
|
||||
|
||||
if (!auth.isAuthenticated) {
|
||||
return <Navigate to={redirectPath} replace />;
|
||||
}
|
||||
|
||||
|
||||
return <Outlet />;
|
||||
};
|
||||
|
||||
|
||||
@@ -11,10 +11,11 @@ interface ServerCardProps {
|
||||
server: Server
|
||||
onRemove: (serverName: string) => void
|
||||
onEdit: (server: Server) => void
|
||||
onToggle?: (server: Server, enabled: boolean) => void
|
||||
onToggle?: (server: Server, enabled: boolean) => Promise<boolean>
|
||||
onRefresh?: () => void
|
||||
}
|
||||
|
||||
const ServerCard = ({ server, onRemove, onEdit, onToggle }: ServerCardProps) => {
|
||||
const ServerCard = ({ server, onRemove, onEdit, onToggle, onRefresh }: ServerCardProps) => {
|
||||
const { t } = useTranslation()
|
||||
const { showToast } = useToast()
|
||||
const [isExpanded, setIsExpanded] = useState(false)
|
||||
@@ -50,7 +51,7 @@ const ServerCard = ({ server, onRemove, onEdit, onToggle }: ServerCardProps) =>
|
||||
const handleToggle = async (e: React.MouseEvent) => {
|
||||
e.stopPropagation()
|
||||
if (isToggling || !onToggle) return
|
||||
|
||||
|
||||
setIsToggling(true)
|
||||
try {
|
||||
await onToggle(server, !(server.enabled !== false))
|
||||
@@ -102,9 +103,32 @@ const ServerCard = ({ server, onRemove, onEdit, onToggle }: ServerCardProps) =>
|
||||
setShowDeleteDialog(false)
|
||||
}
|
||||
|
||||
const handleToolToggle = async (toolName: string, enabled: boolean) => {
|
||||
try {
|
||||
const { toggleTool } = await import('@/services/toolService')
|
||||
const result = await toggleTool(server.name, toolName, enabled)
|
||||
|
||||
if (result.success) {
|
||||
showToast(
|
||||
t(enabled ? 'tool.enableSuccess' : 'tool.disableSuccess', { name: toolName }),
|
||||
'success'
|
||||
)
|
||||
// Trigger refresh to update the tool's state in the UI
|
||||
if (onRefresh) {
|
||||
onRefresh()
|
||||
}
|
||||
} else {
|
||||
showToast(result.error || t('tool.toggleFailed'), 'error')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error toggling tool:', error)
|
||||
showToast(t('tool.toggleFailed'), 'error')
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={`bg-white shadow rounded-lg p-6 mb-6 ${server.enabled === false ? 'opacity-60' : ''}`}>
|
||||
<div className={`bg-white shadow rounded-lg p-6 mb-6 page-card transition-all duration-200 ${server.enabled === false ? 'opacity-60' : ''}`}>
|
||||
<div
|
||||
className="flex justify-between items-center cursor-pointer"
|
||||
onClick={() => setIsExpanded(!isExpanded)}
|
||||
@@ -112,26 +136,34 @@ const ServerCard = ({ server, onRemove, onEdit, onToggle }: ServerCardProps) =>
|
||||
<div className="flex items-center space-x-3">
|
||||
<h2 className={`text-xl font-semibold ${server.enabled === false ? 'text-gray-600' : 'text-gray-900'}`}>{server.name}</h2>
|
||||
<StatusBadge status={server.status} />
|
||||
|
||||
|
||||
{/* Tool count display */}
|
||||
<div className="flex items-center px-2 py-1 bg-blue-50 text-blue-700 rounded-full text-sm btn-primary">
|
||||
<svg className="w-4 h-4 mr-1" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fillRule="evenodd" d="M11.3 1.046A1 1 0 0112 2v5h4a1 1 0 01.82 1.573l-7 10A1 1 0 018 18v-5H4a1 1 0 01-.82-1.573l7-10a1 1 0 011.12-.38z" clipRule="evenodd" />
|
||||
</svg>
|
||||
<span>{server.tools?.length || 0} {t('server.tools')}</span>
|
||||
</div>
|
||||
|
||||
{server.error && (
|
||||
<div className="relative">
|
||||
<div
|
||||
className="cursor-pointer"
|
||||
<div
|
||||
className="cursor-pointer"
|
||||
onClick={handleErrorIconClick}
|
||||
aria-label={t('server.viewErrorDetails')}
|
||||
>
|
||||
<AlertCircle className="text-red-500 hover:text-red-600" size={18} />
|
||||
</div>
|
||||
|
||||
|
||||
{showErrorPopover && (
|
||||
<div
|
||||
<div
|
||||
ref={errorPopoverRef}
|
||||
className="absolute z-10 mt-2 bg-white border border-gray-200 rounded-md shadow-lg p-0 w-120"
|
||||
style={{
|
||||
left: '-231px',
|
||||
top: '24px',
|
||||
maxHeight: '300px',
|
||||
overflowY: 'auto',
|
||||
style={{
|
||||
left: '-231px',
|
||||
top: '24px',
|
||||
maxHeight: '300px',
|
||||
overflowY: 'auto',
|
||||
width: '480px',
|
||||
transform: 'translateX(50%)'
|
||||
}}
|
||||
@@ -140,15 +172,15 @@ const ServerCard = ({ server, onRemove, onEdit, onToggle }: ServerCardProps) =>
|
||||
<div className="flex justify-between items-center sticky top-0 bg-white py-2 px-4 border-b border-gray-200 z-20 shadow-sm">
|
||||
<div className="flex items-center space-x-2">
|
||||
<h4 className="text-sm font-medium text-red-600">{t('server.errorDetails')}</h4>
|
||||
<button
|
||||
<button
|
||||
onClick={copyToClipboard}
|
||||
className="p-1 text-gray-400 hover:text-gray-600 transition-colors"
|
||||
className="p-1 text-gray-400 hover:text-gray-600 transition-colors btn-secondary"
|
||||
title={t('common.copy')}
|
||||
>
|
||||
{copied ? <Check size={14} className="text-green-500" /> : <Copy size={14} />}
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
setShowErrorPopover(false)
|
||||
@@ -169,37 +201,36 @@ const ServerCard = ({ server, onRemove, onEdit, onToggle }: ServerCardProps) =>
|
||||
<div className="flex space-x-2">
|
||||
<button
|
||||
onClick={handleEdit}
|
||||
className="px-3 py-1 bg-blue-100 text-blue-800 rounded hover:bg-blue-200 text-sm"
|
||||
className="px-3 py-1 bg-blue-100 text-blue-800 rounded hover:bg-blue-200 text-sm btn-primary"
|
||||
>
|
||||
{t('server.edit')}
|
||||
</button>
|
||||
<div className="flex items-center">
|
||||
<button
|
||||
onClick={handleToggle}
|
||||
className={`px-3 py-1 text-sm rounded transition-colors ${
|
||||
isToggling
|
||||
? 'bg-gray-200 text-gray-500'
|
||||
: server.enabled !== false
|
||||
? 'bg-green-100 text-green-800 hover:bg-green-200'
|
||||
: 'bg-gray-100 text-gray-800 hover:bg-gray-200'
|
||||
}`}
|
||||
className={`px-3 py-1 text-sm rounded transition-colors ${isToggling
|
||||
? 'bg-gray-200 text-gray-500'
|
||||
: server.enabled !== false
|
||||
? 'bg-green-100 text-green-800 hover:bg-green-200 btn-secondary'
|
||||
: 'bg-gray-100 text-gray-800 hover:bg-gray-200 btn-primary'
|
||||
}`}
|
||||
disabled={isToggling}
|
||||
>
|
||||
{isToggling
|
||||
{isToggling
|
||||
? t('common.processing')
|
||||
: server.enabled !== false
|
||||
? t('server.disable')
|
||||
: server.enabled !== false
|
||||
? t('server.disable')
|
||||
: t('server.enable')
|
||||
}
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleRemove}
|
||||
className="px-3 py-1 bg-red-100 text-red-800 rounded hover:bg-red-200 text-sm"
|
||||
className="px-3 py-1 bg-red-100 text-red-800 rounded hover:bg-red-200 text-sm btn-danger"
|
||||
>
|
||||
{t('server.delete')}
|
||||
</button>
|
||||
<button className="text-gray-400 hover:text-gray-600">
|
||||
<button className="text-gray-400 hover:text-gray-600 btn-secondary">
|
||||
{isExpanded ? <ChevronDown size={18} /> : <ChevronRight size={18} />}
|
||||
</button>
|
||||
</div>
|
||||
@@ -207,10 +238,10 @@ const ServerCard = ({ server, onRemove, onEdit, onToggle }: ServerCardProps) =>
|
||||
|
||||
{isExpanded && server.tools && (
|
||||
<div className="mt-6">
|
||||
<h3 className={`text-lg font-medium ${server.enabled === false ? 'text-gray-600' : 'text-gray-900'} mb-4`}>{t('server.tools')}</h3>
|
||||
<h6 className={`font-medium ${server.enabled === false ? 'text-gray-600' : 'text-gray-900'} mb-4`}>{t('server.tools')}</h6>
|
||||
<div className="space-y-4">
|
||||
{server.tools.map((tool, index) => (
|
||||
<ToolCard key={index} tool={tool} />
|
||||
<ToolCard key={index} server={server.name} tool={tool} onToggle={handleToolToggle} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -26,7 +26,7 @@ const ServerForm = ({ onSubmit, onCancel, initialData = null, modalTitle, formEr
|
||||
}
|
||||
};
|
||||
|
||||
const [serverType, setServerType] = useState<'stdio' | 'sse' | 'streamable-http'>(getInitialServerType());
|
||||
const [serverType, setServerType] = useState<'stdio' | 'sse' | 'streamable-http' | 'openapi'>(getInitialServerType());
|
||||
|
||||
const [formData, setFormData] = useState<ServerFormData>({
|
||||
name: (initialData && initialData.name) || '',
|
||||
@@ -40,7 +40,39 @@ const ServerForm = ({ onSubmit, onCancel, initialData = null, modalTitle, formEr
|
||||
: '',
|
||||
args: (initialData && initialData.config && initialData.config.args) || [],
|
||||
type: getInitialServerType(), // Initialize the type field
|
||||
env: []
|
||||
env: [],
|
||||
headers: [],
|
||||
options: {
|
||||
timeout: (initialData && initialData.config && initialData.config.options && initialData.config.options.timeout) || 60000,
|
||||
resetTimeoutOnProgress: (initialData && initialData.config && initialData.config.options && initialData.config.options.resetTimeoutOnProgress) || false,
|
||||
maxTotalTimeout: (initialData && initialData.config && initialData.config.options && initialData.config.options.maxTotalTimeout) || undefined,
|
||||
},
|
||||
// OpenAPI configuration initialization
|
||||
openapi: initialData && initialData.config && initialData.config.openapi ? {
|
||||
url: initialData.config.openapi.url || '',
|
||||
schema: initialData.config.openapi.schema ? JSON.stringify(initialData.config.openapi.schema, null, 2) : '',
|
||||
inputMode: initialData.config.openapi.url ? 'url' : (initialData.config.openapi.schema ? 'schema' : 'url'),
|
||||
version: initialData.config.openapi.version || '3.1.0',
|
||||
securityType: initialData.config.openapi.security?.type || 'none',
|
||||
// API Key initialization
|
||||
apiKeyName: initialData.config.openapi.security?.apiKey?.name || '',
|
||||
apiKeyIn: initialData.config.openapi.security?.apiKey?.in || 'header',
|
||||
apiKeyValue: initialData.config.openapi.security?.apiKey?.value || '',
|
||||
// HTTP auth initialization
|
||||
httpScheme: initialData.config.openapi.security?.http?.scheme || 'bearer',
|
||||
httpCredentials: initialData.config.openapi.security?.http?.credentials || '',
|
||||
// OAuth2 initialization
|
||||
oauth2Token: initialData.config.openapi.security?.oauth2?.token || '',
|
||||
// OpenID Connect initialization
|
||||
openIdConnectUrl: initialData.config.openapi.security?.openIdConnect?.url || '',
|
||||
openIdConnectToken: initialData.config.openapi.security?.openIdConnect?.token || ''
|
||||
} : {
|
||||
inputMode: 'url',
|
||||
url: '',
|
||||
schema: '',
|
||||
version: '3.1.0',
|
||||
securityType: 'none'
|
||||
}
|
||||
})
|
||||
|
||||
const [envVars, setEnvVars] = useState<EnvVar[]>(
|
||||
@@ -49,6 +81,13 @@ const ServerForm = ({ onSubmit, onCancel, initialData = null, modalTitle, formEr
|
||||
: [],
|
||||
)
|
||||
|
||||
const [headerVars, setHeaderVars] = useState<EnvVar[]>(
|
||||
initialData && initialData.config && initialData.config.headers
|
||||
? Object.entries(initialData.config.headers).map(([key, value]) => ({ key, value }))
|
||||
: [],
|
||||
)
|
||||
|
||||
const [isRequestOptionsExpanded, setIsRequestOptionsExpanded] = useState<boolean>(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const isEdit = !!initialData
|
||||
|
||||
@@ -59,11 +98,11 @@ const ServerForm = ({ onSubmit, onCancel, initialData = null, modalTitle, formEr
|
||||
|
||||
// Transform space-separated arguments string into array
|
||||
const handleArgsChange = (value: string) => {
|
||||
let args = value.split(' ').filter((arg) => arg.trim() !== '')
|
||||
const args = value.split(' ').filter((arg) => arg.trim() !== '')
|
||||
setFormData({ ...formData, arguments: value, args })
|
||||
}
|
||||
|
||||
const updateServerType = (type: 'stdio' | 'sse' | 'streamable-http') => {
|
||||
const updateServerType = (type: 'stdio' | 'sse' | 'streamable-http' | 'openapi') => {
|
||||
setServerType(type);
|
||||
setFormData(prev => ({ ...prev, type }));
|
||||
}
|
||||
@@ -84,6 +123,33 @@ const ServerForm = ({ onSubmit, onCancel, initialData = null, modalTitle, formEr
|
||||
setEnvVars(newEnvVars)
|
||||
}
|
||||
|
||||
const handleHeaderVarChange = (index: number, field: 'key' | 'value', value: string) => {
|
||||
const newHeaderVars = [...headerVars]
|
||||
newHeaderVars[index][field] = value
|
||||
setHeaderVars(newHeaderVars)
|
||||
}
|
||||
|
||||
const addHeaderVar = () => {
|
||||
setHeaderVars([...headerVars, { key: '', value: '' }])
|
||||
}
|
||||
|
||||
const removeHeaderVar = (index: number) => {
|
||||
const newHeaderVars = [...headerVars]
|
||||
newHeaderVars.splice(index, 1)
|
||||
setHeaderVars(newHeaderVars)
|
||||
}
|
||||
|
||||
// Handle options changes
|
||||
const handleOptionsChange = (field: 'timeout' | 'resetTimeoutOnProgress' | 'maxTotalTimeout', value: number | boolean | undefined) => {
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
options: {
|
||||
...prev.options,
|
||||
[field]: value
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
// Submit handler for server configuration
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
@@ -97,18 +163,94 @@ const ServerForm = ({ onSubmit, onCancel, initialData = null, modalTitle, formEr
|
||||
}
|
||||
})
|
||||
|
||||
const headers: Record<string, string> = {}
|
||||
headerVars.forEach(({ key, value }) => {
|
||||
if (key.trim()) {
|
||||
headers[key.trim()] = value
|
||||
}
|
||||
})
|
||||
|
||||
// Prepare options object, only include defined values
|
||||
const options: any = {}
|
||||
if (formData.options?.timeout && formData.options.timeout !== 60000) {
|
||||
options.timeout = formData.options.timeout
|
||||
}
|
||||
if (formData.options?.resetTimeoutOnProgress) {
|
||||
options.resetTimeoutOnProgress = formData.options.resetTimeoutOnProgress
|
||||
}
|
||||
if (formData.options?.maxTotalTimeout) {
|
||||
options.maxTotalTimeout = formData.options.maxTotalTimeout
|
||||
}
|
||||
|
||||
const payload = {
|
||||
name: formData.name,
|
||||
config: {
|
||||
type: serverType, // Always include the type
|
||||
...(serverType === 'sse' || serverType === 'streamable-http'
|
||||
? { url: formData.url }
|
||||
: {
|
||||
command: formData.command,
|
||||
args: formData.args,
|
||||
env: Object.keys(env).length > 0 ? env : undefined,
|
||||
...(serverType === 'openapi'
|
||||
? {
|
||||
openapi: (() => {
|
||||
const openapi: any = {
|
||||
version: formData.openapi?.version || '3.1.0'
|
||||
};
|
||||
|
||||
// Add URL or schema based on input mode
|
||||
if (formData.openapi?.inputMode === 'url') {
|
||||
openapi.url = formData.openapi?.url || '';
|
||||
} else if (formData.openapi?.inputMode === 'schema' && formData.openapi?.schema) {
|
||||
try {
|
||||
openapi.schema = JSON.parse(formData.openapi.schema);
|
||||
} catch (e) {
|
||||
throw new Error('Invalid JSON schema format');
|
||||
}
|
||||
}
|
||||
|
||||
// Add security configuration if provided
|
||||
if (formData.openapi?.securityType && formData.openapi.securityType !== 'none') {
|
||||
openapi.security = {
|
||||
type: formData.openapi.securityType,
|
||||
...(formData.openapi.securityType === 'apiKey' && {
|
||||
apiKey: {
|
||||
name: formData.openapi.apiKeyName || '',
|
||||
in: formData.openapi.apiKeyIn || 'header',
|
||||
value: formData.openapi.apiKeyValue || ''
|
||||
}
|
||||
}),
|
||||
...(formData.openapi.securityType === 'http' && {
|
||||
http: {
|
||||
scheme: formData.openapi.httpScheme || 'bearer',
|
||||
credentials: formData.openapi.httpCredentials || ''
|
||||
}
|
||||
}),
|
||||
...(formData.openapi.securityType === 'oauth2' && {
|
||||
oauth2: {
|
||||
token: formData.openapi.oauth2Token || ''
|
||||
}
|
||||
}),
|
||||
...(formData.openapi.securityType === 'openIdConnect' && {
|
||||
openIdConnect: {
|
||||
url: formData.openapi.openIdConnectUrl || '',
|
||||
token: formData.openapi.openIdConnectToken || ''
|
||||
}
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
return openapi;
|
||||
})(),
|
||||
...(Object.keys(headers).length > 0 ? { headers } : {})
|
||||
}
|
||||
)
|
||||
: serverType === 'sse' || serverType === 'streamable-http'
|
||||
? {
|
||||
url: formData.url,
|
||||
...(Object.keys(headers).length > 0 ? { headers } : {})
|
||||
}
|
||||
: {
|
||||
command: formData.command,
|
||||
args: formData.args,
|
||||
env: Object.keys(env).length > 0 ? env : undefined,
|
||||
}
|
||||
),
|
||||
...(Object.keys(options).length > 0 ? { options } : {})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -144,7 +286,7 @@ const ServerForm = ({ onSubmit, onCancel, initialData = null, modalTitle, formEr
|
||||
id="name"
|
||||
value={formData.name}
|
||||
onChange={handleInputChange}
|
||||
className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
|
||||
className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline form-input"
|
||||
placeholder="e.g.: time-mcp"
|
||||
required
|
||||
disabled={isEdit}
|
||||
@@ -190,25 +332,394 @@ const ServerForm = ({ onSubmit, onCancel, initialData = null, modalTitle, formEr
|
||||
/>
|
||||
<label htmlFor="streamable-http">Streamable HTTP</label>
|
||||
</div>
|
||||
<div>
|
||||
<input
|
||||
type="radio"
|
||||
id="openapi"
|
||||
name="serverType"
|
||||
value="openapi"
|
||||
checked={serverType === 'openapi'}
|
||||
onChange={() => updateServerType('openapi')}
|
||||
className="mr-1"
|
||||
/>
|
||||
<label htmlFor="openapi">OpenAPI</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{serverType === 'sse' || serverType === 'streamable-http' ? (
|
||||
<div className="mb-4">
|
||||
<label className="block text-gray-700 text-sm font-bold mb-2" htmlFor="url">
|
||||
{t('server.url')}
|
||||
</label>
|
||||
<input
|
||||
type="url"
|
||||
name="url"
|
||||
id="url"
|
||||
value={formData.url}
|
||||
onChange={handleInputChange}
|
||||
className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
|
||||
placeholder={serverType === 'streamable-http' ? "e.g.: http://localhost:3000/mcp" : "e.g.: http://localhost:3000/sse"}
|
||||
required={serverType === 'sse' || serverType === 'streamable-http'}
|
||||
/>
|
||||
</div>
|
||||
{serverType === 'openapi' ? (
|
||||
<>
|
||||
{/* Input Mode Selection */}
|
||||
<div className="mb-4">
|
||||
<label className="block text-gray-700 text-sm font-bold mb-2">
|
||||
{t('server.openapi.inputMode')}
|
||||
</label>
|
||||
<div className="flex space-x-4">
|
||||
<div>
|
||||
<input
|
||||
type="radio"
|
||||
id="input-mode-url"
|
||||
name="inputMode"
|
||||
value="url"
|
||||
checked={formData.openapi?.inputMode === 'url'}
|
||||
onChange={() => setFormData(prev => ({
|
||||
...prev,
|
||||
openapi: { ...prev.openapi!, inputMode: 'url' }
|
||||
}))}
|
||||
className="mr-1"
|
||||
/>
|
||||
<label htmlFor="input-mode-url">{t('server.openapi.inputModeUrl')}</label>
|
||||
</div>
|
||||
<div>
|
||||
<input
|
||||
type="radio"
|
||||
id="input-mode-schema"
|
||||
name="inputMode"
|
||||
value="schema"
|
||||
checked={formData.openapi?.inputMode === 'schema'}
|
||||
onChange={() => setFormData(prev => ({
|
||||
...prev,
|
||||
openapi: { ...prev.openapi!, inputMode: 'schema' }
|
||||
}))}
|
||||
className="mr-1"
|
||||
/>
|
||||
<label htmlFor="input-mode-schema">{t('server.openapi.inputModeSchema')}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* URL Input */}
|
||||
{formData.openapi?.inputMode === 'url' && (
|
||||
<div className="mb-4">
|
||||
<label className="block text-gray-700 text-sm font-bold mb-2" htmlFor="openapi-url">
|
||||
{t('server.openapi.specUrl')}
|
||||
</label>
|
||||
<input
|
||||
type="url"
|
||||
name="openapi-url"
|
||||
id="openapi-url"
|
||||
value={formData.openapi?.url || ''}
|
||||
onChange={(e) => setFormData(prev => ({
|
||||
...prev,
|
||||
openapi: { ...prev.openapi!, url: e.target.value }
|
||||
}))}
|
||||
className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline form-input"
|
||||
placeholder="e.g.: https://api.example.com/openapi.json"
|
||||
required={serverType === 'openapi' && formData.openapi?.inputMode === 'url'}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Schema Input */}
|
||||
{formData.openapi?.inputMode === 'schema' && (
|
||||
<div className="mb-4">
|
||||
<label className="block text-gray-700 text-sm font-bold mb-2" htmlFor="openapi-schema">
|
||||
{t('server.openapi.schema')}
|
||||
</label>
|
||||
<textarea
|
||||
name="openapi-schema"
|
||||
id="openapi-schema"
|
||||
rows={10}
|
||||
value={formData.openapi?.schema || ''}
|
||||
onChange={(e) => setFormData(prev => ({
|
||||
...prev,
|
||||
openapi: { ...prev.openapi!, schema: e.target.value }
|
||||
}))}
|
||||
className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline font-mono text-sm"
|
||||
placeholder={`{
|
||||
"openapi": "3.1.0",
|
||||
"info": {
|
||||
"title": "API",
|
||||
"version": "1.0.0"
|
||||
},
|
||||
"servers": [
|
||||
{
|
||||
"url": "https://api.example.com"
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
...
|
||||
}
|
||||
}`}
|
||||
required={serverType === 'openapi' && formData.openapi?.inputMode === 'schema'}
|
||||
/>
|
||||
<p className="text-xs text-gray-500 mt-1">{t('server.openapi.schemaHelp')}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Security Configuration */}
|
||||
<div className="mb-4">
|
||||
<label className="block text-gray-700 text-sm font-bold mb-2">
|
||||
{t('server.openapi.security')}
|
||||
</label>
|
||||
<select
|
||||
value={formData.openapi?.securityType || 'none'}
|
||||
onChange={(e) => setFormData(prev => ({
|
||||
...prev,
|
||||
openapi: {
|
||||
...prev.openapi,
|
||||
securityType: e.target.value as any,
|
||||
url: prev.openapi?.url || ''
|
||||
}
|
||||
}))}
|
||||
className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline form-input"
|
||||
>
|
||||
<option value="none">{t('server.openapi.securityNone')}</option>
|
||||
<option value="apiKey">{t('server.openapi.securityApiKey')}</option>
|
||||
<option value="http">{t('server.openapi.securityHttp')}</option>
|
||||
<option value="oauth2">{t('server.openapi.securityOAuth2')}</option>
|
||||
<option value="openIdConnect">{t('server.openapi.securityOpenIdConnect')}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* API Key Configuration */}
|
||||
{formData.openapi?.securityType === 'apiKey' && (
|
||||
<div className="mb-4 p-4 border border-gray-200 rounded bg-gray-50">
|
||||
<h4 className="text-sm font-medium text-gray-700 mb-3">{t('server.openapi.apiKeyConfig')}</h4>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-3">
|
||||
<div>
|
||||
<label className="block text-xs text-gray-600 mb-1">{t('server.openapi.apiKeyName')}</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.openapi?.apiKeyName || ''}
|
||||
onChange={(e) => setFormData(prev => ({
|
||||
...prev,
|
||||
openapi: { ...prev.openapi, apiKeyName: e.target.value, url: prev.openapi?.url || '' }
|
||||
}))}
|
||||
className="w-full border rounded px-2 py-1 text-sm form-input focus:outline-none"
|
||||
placeholder="Authorization"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs text-gray-600 mb-1">{t('server.openapi.apiKeyIn')}</label>
|
||||
<select
|
||||
value={formData.openapi?.apiKeyIn || 'header'}
|
||||
onChange={(e) => setFormData(prev => ({
|
||||
...prev,
|
||||
openapi: { ...prev.openapi, apiKeyIn: e.target.value as any, url: prev.openapi?.url || '' }
|
||||
}))}
|
||||
className="w-full border rounded px-2 py-1 text-sm focus:outline-none form-input"
|
||||
>
|
||||
<option value="header">Header</option>
|
||||
<option value="query">Query</option>
|
||||
<option value="cookie">Cookie</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs text-gray-600 mb-1">{t('server.openapi.apiKeyValue')}</label>
|
||||
<input
|
||||
type="password"
|
||||
value={formData.openapi?.apiKeyValue || ''}
|
||||
onChange={(e) => setFormData(prev => ({
|
||||
...prev,
|
||||
openapi: { ...prev.openapi, apiKeyValue: e.target.value, url: prev.openapi?.url || '' }
|
||||
}))}
|
||||
className="w-full border rounded px-2 py-1 text-sm focus:outline-none form-input"
|
||||
placeholder="your-api-key"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* HTTP Authentication Configuration */}
|
||||
{formData.openapi?.securityType === 'http' && (
|
||||
<div className="mb-4 p-4 border border-gray-200 rounded bg-gray-50">
|
||||
<h4 className="text-sm font-medium text-gray-700 mb-3">{t('server.openapi.httpAuthConfig')}</h4>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||
<div>
|
||||
<label className="block text-xs text-gray-600 mb-1">{t('server.openapi.httpScheme')}</label>
|
||||
<select
|
||||
value={formData.openapi?.httpScheme || 'bearer'}
|
||||
onChange={(e) => setFormData(prev => ({
|
||||
...prev,
|
||||
openapi: { ...prev.openapi, httpScheme: e.target.value as any, url: prev.openapi?.url || '' }
|
||||
}))}
|
||||
className="w-full border rounded px-2 py-1 text-sm focus:outline-none form-input"
|
||||
>
|
||||
<option value="basic">Basic</option>
|
||||
<option value="bearer">Bearer</option>
|
||||
<option value="digest">Digest</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs text-gray-600 mb-1">{t('server.openapi.httpCredentials')}</label>
|
||||
<input
|
||||
type="password"
|
||||
value={formData.openapi?.httpCredentials || ''}
|
||||
onChange={(e) => setFormData(prev => ({
|
||||
...prev,
|
||||
openapi: { ...prev.openapi, httpCredentials: e.target.value, url: prev.openapi?.url || '' }
|
||||
}))}
|
||||
className="w-full border rounded px-2 py-1 text-sm focus:outline-none form-input"
|
||||
placeholder={formData.openapi?.httpScheme === 'basic' ? 'base64-encoded-credentials' : 'bearer-token'}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* OAuth2 Configuration */}
|
||||
{formData.openapi?.securityType === 'oauth2' && (
|
||||
<div className="mb-4 p-4 border border-gray-200 rounded bg-gray-50">
|
||||
<h4 className="text-sm font-medium text-gray-700 mb-3">{t('server.openapi.oauth2Config')}</h4>
|
||||
<div className="grid grid-cols-1 gap-3">
|
||||
<div>
|
||||
<label className="block text-xs text-gray-600 mb-1">{t('server.openapi.oauth2Token')}</label>
|
||||
<input
|
||||
type="password"
|
||||
value={formData.openapi?.oauth2Token || ''}
|
||||
onChange={(e) => setFormData(prev => ({
|
||||
...prev,
|
||||
openapi: { ...prev.openapi, oauth2Token: e.target.value, url: prev.openapi?.url || '' }
|
||||
}))}
|
||||
className="w-full border rounded px-2 py-1 text-sm focus:outline-none form-input"
|
||||
placeholder="access-token"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* OpenID Connect Configuration */}
|
||||
{formData.openapi?.securityType === 'openIdConnect' && (
|
||||
<div className="mb-4 p-4 border border-gray-200 rounded bg-gray-50">
|
||||
<h4 className="text-sm font-medium text-gray-700 mb-3">{t('server.openapi.openIdConnectConfig')}</h4>
|
||||
<div className="grid grid-cols-1 gap-3">
|
||||
<div>
|
||||
<label className="block text-xs text-gray-600 mb-1">{t('server.openapi.openIdConnectUrl')}</label>
|
||||
<input
|
||||
type="url"
|
||||
value={formData.openapi?.openIdConnectUrl || ''}
|
||||
onChange={(e) => setFormData(prev => ({
|
||||
...prev,
|
||||
openapi: { ...prev.openapi, openIdConnectUrl: e.target.value, url: prev.openapi?.url || '' }
|
||||
}))}
|
||||
className="w-full border rounded px-2 py-1 text-sm focus:outline-none form-input"
|
||||
placeholder="https://example.com/.well-known/openid_configuration"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs text-gray-600 mb-1">{t('server.openapi.openIdConnectToken')}</label>
|
||||
<input
|
||||
type="password"
|
||||
value={formData.openapi?.openIdConnectToken || ''}
|
||||
onChange={(e) => setFormData(prev => ({
|
||||
...prev,
|
||||
openapi: { ...prev.openapi, openIdConnectToken: e.target.value, url: prev.openapi?.url || '' }
|
||||
}))}
|
||||
className="w-full border rounded px-2 py-1 text-sm focus:outline-none form-input"
|
||||
placeholder="id-token"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mb-4">
|
||||
<div className="flex justify-between items-center mb-2">
|
||||
<label className="block text-gray-700 text-sm font-bold">
|
||||
{t('server.headers')}
|
||||
</label>
|
||||
<button
|
||||
type="button"
|
||||
onClick={addHeaderVar}
|
||||
className="bg-gray-200 hover:bg-gray-300 text-gray-700 font-medium py-1 px-2 rounded text-sm flex items-center justify-center min-w-[30px] min-h-[30px] btn-primary"
|
||||
>
|
||||
+
|
||||
</button>
|
||||
</div>
|
||||
{headerVars.map((headerVar, index) => (
|
||||
<div key={index} className="flex items-center mb-2">
|
||||
<div className="flex items-center space-x-2 flex-grow">
|
||||
<input
|
||||
type="text"
|
||||
value={headerVar.key}
|
||||
onChange={(e) => handleHeaderVarChange(index, 'key', e.target.value)}
|
||||
className="shadow appearance-none border rounded py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline w-1/2 form-input"
|
||||
placeholder="Authorization"
|
||||
/>
|
||||
<span className="flex items-center">:</span>
|
||||
<input
|
||||
type="text"
|
||||
value={headerVar.value}
|
||||
onChange={(e) => handleHeaderVarChange(index, 'value', e.target.value)}
|
||||
className="shadow appearance-none border rounded py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline w-1/2 form-input"
|
||||
placeholder="Bearer token..."
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => removeHeaderVar(index)}
|
||||
className="bg-gray-200 hover:bg-gray-300 text-gray-700 font-medium py-1 px-2 rounded text-sm flex items-center justify-center min-w-[30px] min-h-[30px] ml-2 btn-danger"
|
||||
>
|
||||
-
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
) : serverType === 'sse' || serverType === 'streamable-http' ? (
|
||||
<>
|
||||
<div className="mb-4">
|
||||
<label className="block text-gray-700 text-sm font-bold mb-2" htmlFor="url">
|
||||
{t('server.url')}
|
||||
</label>
|
||||
<input
|
||||
type="url"
|
||||
name="url"
|
||||
id="url"
|
||||
value={formData.url}
|
||||
onChange={handleInputChange}
|
||||
className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline form-input"
|
||||
placeholder={serverType === 'streamable-http' ? "e.g.: http://localhost:3000/mcp" : "e.g.: http://localhost:3000/sse"}
|
||||
required={serverType === 'sse' || serverType === 'streamable-http'}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mb-4">
|
||||
<div className="flex justify-between items-center mb-2">
|
||||
<label className="block text-gray-700 text-sm font-bold">
|
||||
{t('server.headers')}
|
||||
</label>
|
||||
<button
|
||||
type="button"
|
||||
onClick={addHeaderVar}
|
||||
className="bg-gray-200 hover:bg-gray-300 text-gray-700 font-medium py-1 px-2 rounded text-sm flex items-center justify-center min-w-[30px] min-h-[30px] btn-primary"
|
||||
>
|
||||
+
|
||||
</button>
|
||||
</div>
|
||||
{headerVars.map((headerVar, index) => (
|
||||
<div key={index} className="flex items-center mb-2">
|
||||
<div className="flex items-center space-x-2 flex-grow">
|
||||
<input
|
||||
type="text"
|
||||
value={headerVar.key}
|
||||
onChange={(e) => handleHeaderVarChange(index, 'key', e.target.value)}
|
||||
className="shadow appearance-none border rounded py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline w-1/2 form-input"
|
||||
placeholder="Authorization"
|
||||
/>
|
||||
<span className="flex items-center">:</span>
|
||||
<input
|
||||
type="text"
|
||||
value={headerVar.value}
|
||||
onChange={(e) => handleHeaderVarChange(index, 'value', e.target.value)}
|
||||
className="shadow appearance-none border rounded py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline w-1/2 form-input"
|
||||
placeholder="Bearer token..."
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => removeHeaderVar(index)}
|
||||
className="bg-gray-200 hover:bg-gray-300 text-gray-700 font-medium py-1 px-2 rounded text-sm flex items-center justify-center min-w-[30px] min-h-[30px] ml-2 btn-danger"
|
||||
>
|
||||
-
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div className="mb-4">
|
||||
@@ -221,7 +732,7 @@ const ServerForm = ({ onSubmit, onCancel, initialData = null, modalTitle, formEr
|
||||
id="command"
|
||||
value={formData.command}
|
||||
onChange={handleInputChange}
|
||||
className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
|
||||
className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline form-input"
|
||||
placeholder="e.g.: npx"
|
||||
required={serverType === 'stdio'}
|
||||
/>
|
||||
@@ -236,7 +747,7 @@ const ServerForm = ({ onSubmit, onCancel, initialData = null, modalTitle, formEr
|
||||
id="arguments"
|
||||
value={formData.arguments}
|
||||
onChange={(e) => handleArgsChange(e.target.value)}
|
||||
className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
|
||||
className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline form-input"
|
||||
placeholder="e.g.: -y time-mcp"
|
||||
required={serverType === 'stdio'}
|
||||
/>
|
||||
@@ -250,9 +761,9 @@ const ServerForm = ({ onSubmit, onCancel, initialData = null, modalTitle, formEr
|
||||
<button
|
||||
type="button"
|
||||
onClick={addEnvVar}
|
||||
className="bg-gray-200 hover:bg-gray-300 text-gray-700 font-medium py-1 px-2 rounded text-sm flex items-center"
|
||||
className="bg-gray-200 hover:bg-gray-300 text-gray-700 font-medium py-1 px-2 rounded text-sm flex items-center justify-center min-w-[30px] min-h-[30px] btn-primary"
|
||||
>
|
||||
+ {t('server.add')}
|
||||
+
|
||||
</button>
|
||||
</div>
|
||||
{envVars.map((envVar, index) => (
|
||||
@@ -262,7 +773,7 @@ const ServerForm = ({ onSubmit, onCancel, initialData = null, modalTitle, formEr
|
||||
type="text"
|
||||
value={envVar.key}
|
||||
onChange={(e) => handleEnvVarChange(index, 'key', e.target.value)}
|
||||
className="shadow appearance-none border rounded py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline w-1/2"
|
||||
className="shadow appearance-none border rounded py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline w-1/2 form-input"
|
||||
placeholder={t('server.key')}
|
||||
/>
|
||||
<span className="flex items-center">:</span>
|
||||
@@ -270,16 +781,16 @@ const ServerForm = ({ onSubmit, onCancel, initialData = null, modalTitle, formEr
|
||||
type="text"
|
||||
value={envVar.value}
|
||||
onChange={(e) => handleEnvVarChange(index, 'value', e.target.value)}
|
||||
className="shadow appearance-none border rounded py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline w-1/2"
|
||||
className="shadow appearance-none border rounded py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline w-1/2 form-input"
|
||||
placeholder={t('server.value')}
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => removeEnvVar(index)}
|
||||
className="bg-gray-200 hover:bg-gray-300 text-gray-700 font-medium py-1 px-2 rounded text-sm flex items-center justify-center min-w-[56px] ml-2"
|
||||
className="bg-gray-200 hover:bg-gray-300 text-gray-700 font-medium py-1 px-2 rounded text-sm flex items-center justify-center min-w-[30px] min-h-[30px] ml-2 btn-danger"
|
||||
>
|
||||
- {t('server.remove')}
|
||||
-
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
@@ -287,17 +798,88 @@ const ServerForm = ({ onSubmit, onCancel, initialData = null, modalTitle, formEr
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Request Options Configuration */}
|
||||
{serverType !== 'openapi' && (
|
||||
<div className="mb-4">
|
||||
<div
|
||||
className="flex items-center justify-between cursor-pointer bg-gray-50 hover:bg-gray-100 p-3 rounded border border-gray-200"
|
||||
onClick={() => setIsRequestOptionsExpanded(!isRequestOptionsExpanded)}
|
||||
>
|
||||
<label className="text-gray-700 text-sm font-bold">
|
||||
{t('server.requestOptions')}
|
||||
</label>
|
||||
<span className="text-gray-500 text-sm">
|
||||
{isRequestOptionsExpanded ? '▼' : '▶'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{isRequestOptionsExpanded && (
|
||||
<div className="border border-gray-200 rounded-b p-4 bg-gray-50 border-t-0">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-gray-600 text-sm font-medium mb-1" htmlFor="timeout">
|
||||
{t('server.timeout')}
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
id="timeout"
|
||||
value={formData.options?.timeout || 60000}
|
||||
onChange={(e) => handleOptionsChange('timeout', parseInt(e.target.value) || 60000)}
|
||||
className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline form-input"
|
||||
placeholder="30000"
|
||||
min="1000"
|
||||
max="300000"
|
||||
/>
|
||||
<p className="text-xs text-gray-500 mt-1">{t('server.timeoutDescription')}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-gray-600 text-sm font-medium mb-1" htmlFor="maxTotalTimeout">
|
||||
{t('server.maxTotalTimeout')}
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
id="maxTotalTimeout"
|
||||
value={formData.options?.maxTotalTimeout || ''}
|
||||
onChange={(e) => handleOptionsChange('maxTotalTimeout', e.target.value ? parseInt(e.target.value) : undefined)}
|
||||
className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline form-input"
|
||||
placeholder="Optional"
|
||||
min="1000"
|
||||
/>
|
||||
<p className="text-xs text-gray-500 mt-1">{t('server.maxTotalTimeoutDescription')}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-3">
|
||||
<label className="flex items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={formData.options?.resetTimeoutOnProgress || false}
|
||||
onChange={(e) => handleOptionsChange('resetTimeoutOnProgress', e.target.checked)}
|
||||
className="mr-2"
|
||||
/>
|
||||
<span className="text-gray-600 text-sm">{t('server.resetTimeoutOnProgress')}</span>
|
||||
</label>
|
||||
<p className="text-xs text-gray-500 mt-1 ml-6">
|
||||
{t('server.resetTimeoutOnProgressDescription')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex justify-end mt-6">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onCancel}
|
||||
className="bg-gray-300 hover:bg-gray-400 text-gray-800 font-medium py-2 px-4 rounded mr-2"
|
||||
className="bg-gray-300 hover:bg-gray-400 text-gray-800 font-medium py-2 px-4 rounded mr-2 btn-secondary"
|
||||
>
|
||||
{t('server.cancel')}
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
className="bg-blue-500 hover:bg-blue-600 text-white font-medium py-2 px-4 rounded"
|
||||
className="bg-blue-500 hover:bg-blue-600 text-white font-medium py-2 px-4 rounded btn-primary"
|
||||
>
|
||||
{isEdit ? t('server.save') : t('server.add')}
|
||||
</button>
|
||||
|
||||
317
frontend/src/components/ServerToolConfig.tsx
Normal file
@@ -0,0 +1,317 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { IGroupServerConfig, Server, Tool } from '@/types';
|
||||
import { cn } from '@/utils/cn';
|
||||
|
||||
interface ServerToolConfigProps {
|
||||
servers: Server[];
|
||||
value: string[] | IGroupServerConfig[];
|
||||
onChange: (value: IGroupServerConfig[]) => void;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const ServerToolConfig: React.FC<ServerToolConfigProps> = ({
|
||||
servers,
|
||||
value,
|
||||
onChange,
|
||||
className
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const [expandedServers, setExpandedServers] = useState<Set<string>>(new Set());
|
||||
|
||||
// Normalize current value to IGroupServerConfig[] format
|
||||
const normalizedValue: IGroupServerConfig[] = React.useMemo(() => {
|
||||
return value.map(item => {
|
||||
if (typeof item === 'string') {
|
||||
return { name: item, tools: 'all' as const };
|
||||
}
|
||||
return { ...item, tools: item.tools || 'all' as const };
|
||||
});
|
||||
}, [value]);
|
||||
|
||||
// Get available servers (enabled only)
|
||||
const availableServers = React.useMemo(() =>
|
||||
servers.filter(server => server.enabled !== false),
|
||||
[servers]
|
||||
);
|
||||
|
||||
// Clean up expanded servers when servers are removed from configuration
|
||||
// But keep servers that were explicitly expanded even if they have no configuration
|
||||
React.useEffect(() => {
|
||||
const configuredServerNames = new Set(normalizedValue.map(config => config.name));
|
||||
const availableServerNames = new Set(availableServers.map(server => server.name));
|
||||
|
||||
setExpandedServers(prev => {
|
||||
const newSet = new Set<string>();
|
||||
prev.forEach(serverName => {
|
||||
// Keep expanded if server is configured OR if server exists and user manually expanded it
|
||||
if (configuredServerNames.has(serverName) || availableServerNames.has(serverName)) {
|
||||
newSet.add(serverName);
|
||||
}
|
||||
});
|
||||
return newSet;
|
||||
});
|
||||
}, [normalizedValue, availableServers]);
|
||||
|
||||
const toggleServer = (serverName: string) => {
|
||||
const existingIndex = normalizedValue.findIndex(config => config.name === serverName);
|
||||
|
||||
if (existingIndex >= 0) {
|
||||
// Remove server - this will also remove all its tools
|
||||
const newValue = normalizedValue.filter(config => config.name !== serverName);
|
||||
onChange(newValue);
|
||||
// Don't auto-collapse the server when it's unchecked - let user control expansion manually
|
||||
} else {
|
||||
// Add server with all tools by default
|
||||
const newValue = [...normalizedValue, { name: serverName, tools: 'all' as const }];
|
||||
onChange(newValue);
|
||||
// Don't auto-expand the server when it's checked - let user control expansion manually
|
||||
}
|
||||
};
|
||||
|
||||
const toggleServerExpanded = (serverName: string) => {
|
||||
setExpandedServers(prev => {
|
||||
const newSet = new Set(prev);
|
||||
if (newSet.has(serverName)) {
|
||||
newSet.delete(serverName);
|
||||
} else {
|
||||
newSet.add(serverName);
|
||||
}
|
||||
return newSet;
|
||||
});
|
||||
};
|
||||
|
||||
const updateServerTools = (serverName: string, tools: string[] | 'all', keepExpanded = false) => {
|
||||
if (Array.isArray(tools) && tools.length === 0) {
|
||||
// If no tools are selected, remove the server entirely
|
||||
const newValue = normalizedValue.filter(config => config.name !== serverName);
|
||||
onChange(newValue);
|
||||
// Only collapse the server if not explicitly asked to keep it expanded
|
||||
if (!keepExpanded) {
|
||||
setExpandedServers(prev => {
|
||||
const newSet = new Set(prev);
|
||||
newSet.delete(serverName);
|
||||
return newSet;
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// Update server tools or add server if it doesn't exist
|
||||
const existingServerIndex = normalizedValue.findIndex(config => config.name === serverName);
|
||||
|
||||
if (existingServerIndex >= 0) {
|
||||
// Update existing server
|
||||
const newValue = normalizedValue.map(config =>
|
||||
config.name === serverName ? { ...config, tools } : config
|
||||
);
|
||||
onChange(newValue);
|
||||
} else {
|
||||
// Add new server with specified tools
|
||||
const newValue = [...normalizedValue, { name: serverName, tools }];
|
||||
onChange(newValue);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const toggleTool = (serverName: string, toolName: string) => {
|
||||
const server = availableServers.find(s => s.name === serverName);
|
||||
if (!server) return;
|
||||
|
||||
const allToolNames = server.tools?.map(tool => tool.name.replace(`${serverName}-`, '')) || [];
|
||||
const serverConfig = normalizedValue.find(config => config.name === serverName);
|
||||
|
||||
if (!serverConfig) {
|
||||
// Server not selected yet, add it with only this tool
|
||||
const newValue = [...normalizedValue, { name: serverName, tools: [toolName] }];
|
||||
onChange(newValue);
|
||||
// Don't auto-expand - let user control expansion manually
|
||||
return;
|
||||
}
|
||||
|
||||
if (serverConfig.tools === 'all') {
|
||||
// Switch from 'all' to specific tools, excluding the toggled tool
|
||||
const newTools = allToolNames.filter(name => name !== toolName);
|
||||
updateServerTools(serverName, newTools);
|
||||
// If all tools are deselected, the server will be removed and collapsed in updateServerTools
|
||||
} else if (Array.isArray(serverConfig.tools)) {
|
||||
const currentTools = serverConfig.tools;
|
||||
if (currentTools.includes(toolName)) {
|
||||
// Remove tool
|
||||
const newTools = currentTools.filter(name => name !== toolName);
|
||||
updateServerTools(serverName, newTools);
|
||||
// If all tools are deselected, the server will be removed and collapsed in updateServerTools
|
||||
} else {
|
||||
// Add tool
|
||||
const newTools = [...currentTools, toolName];
|
||||
|
||||
// If all tools are selected, switch to 'all'
|
||||
if (newTools.length === allToolNames.length) {
|
||||
updateServerTools(serverName, 'all');
|
||||
} else {
|
||||
updateServerTools(serverName, newTools);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const isServerSelected = (serverName: string) => {
|
||||
const serverConfig = normalizedValue.find(config => config.name === serverName);
|
||||
if (!serverConfig) return false;
|
||||
|
||||
// Server is considered "fully selected" if tools is 'all'
|
||||
return serverConfig.tools === 'all';
|
||||
};
|
||||
|
||||
const isServerPartiallySelected = (serverName: string) => {
|
||||
const serverConfig = normalizedValue.find(config => config.name === serverName);
|
||||
if (!serverConfig) return false;
|
||||
|
||||
// Server is partially selected if it has specific tools selected (not 'all' and not empty)
|
||||
return Array.isArray(serverConfig.tools) && serverConfig.tools.length > 0;
|
||||
};
|
||||
|
||||
const isToolSelected = (serverName: string, toolName: string) => {
|
||||
const serverConfig = normalizedValue.find(config => config.name === serverName);
|
||||
if (!serverConfig) return false;
|
||||
|
||||
if (serverConfig.tools === 'all') return true;
|
||||
if (Array.isArray(serverConfig.tools)) {
|
||||
return serverConfig.tools.includes(toolName);
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const getServerTools = (serverName: string): Tool[] => {
|
||||
const server = availableServers.find(s => s.name === serverName);
|
||||
return server?.tools || [];
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={cn("space-y-4", className)}>
|
||||
<div className="space-y-3">
|
||||
{availableServers.map(server => {
|
||||
const isSelected = isServerSelected(server.name);
|
||||
const isPartiallySelected = isServerPartiallySelected(server.name);
|
||||
const isExpanded = expandedServers.has(server.name);
|
||||
const serverTools = getServerTools(server.name);
|
||||
const serverConfig = normalizedValue.find(config => config.name === server.name);
|
||||
|
||||
return (
|
||||
<div key={server.name} className="border border-gray-200 rounded-lg hover:border-gray-300 hover:bg-gray-50 transition-colors">
|
||||
<div
|
||||
className="flex items-center justify-between p-3 cursor-pointer rounded-lg transition-colors"
|
||||
onClick={() => toggleServerExpanded(server.name)}
|
||||
>
|
||||
<div
|
||||
className="flex items-center space-x-3"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
toggleServer(server.name);
|
||||
}}
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={isSelected || isPartiallySelected}
|
||||
onChange={() => toggleServer(server.name)}
|
||||
className="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500"
|
||||
/>
|
||||
<span className="font-medium text-gray-900 cursor-pointer select-none">
|
||||
{server.name}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-3">
|
||||
{serverConfig && serverConfig.tools !== 'all' && Array.isArray(serverConfig.tools) && (
|
||||
<span className="text-sm text-green-600">
|
||||
({t('groups.toolsSelected')} {serverConfig.tools.length}/{serverTools.length})
|
||||
</span>
|
||||
)}
|
||||
{serverConfig && serverConfig.tools === 'all' && (
|
||||
<span className="text-sm text-green-600">
|
||||
({t('groups.allTools')} {serverTools.length}/{serverTools.length})
|
||||
</span>
|
||||
)}
|
||||
|
||||
{serverTools.length > 0 && (
|
||||
<button
|
||||
type="button"
|
||||
className="p-1 text-gray-400 hover:text-gray-600 transition-colors"
|
||||
>
|
||||
<svg
|
||||
className={cn("w-5 h-5 transition-transform", isExpanded && "rotate-180")}
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
||||
</svg>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isExpanded && serverTools.length > 0 && (
|
||||
<div className="border-t border-gray-200 bg-gray-50 p-3">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<span className="text-sm font-medium text-gray-700">
|
||||
{t('groups.toolSelection')}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
const isAllSelected = serverConfig?.tools === 'all';
|
||||
if (isAllSelected || (Array.isArray(serverConfig?.tools) && serverConfig.tools.length === serverTools.length)) {
|
||||
// If all tools are selected, deselect all (remove server) but keep expanded
|
||||
updateServerTools(server.name, [], true);
|
||||
} else {
|
||||
// Select all tools (add server if not present)
|
||||
updateServerTools(server.name, 'all');
|
||||
// Don't auto-expand - let user control expansion manually
|
||||
}
|
||||
}}
|
||||
className="text-sm text-blue-600 hover:text-blue-800 transition-colors"
|
||||
>
|
||||
{(serverConfig?.tools === 'all' ||
|
||||
(Array.isArray(serverConfig?.tools) && serverConfig.tools.length === serverTools.length))
|
||||
? t('groups.selectNone')
|
||||
: t('groups.selectAll')}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 gap-2 max-h-32 overflow-y-auto">
|
||||
{serverTools.map(tool => {
|
||||
const toolName = tool.name.replace(`${server.name}-`, '');
|
||||
const isToolChecked = isToolSelected(server.name, toolName);
|
||||
|
||||
return (
|
||||
<label key={tool.name} className="flex items-center space-x-2 text-sm">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={isToolChecked}
|
||||
onChange={() => toggleTool(server.name, toolName)}
|
||||
className="w-3 h-3 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500"
|
||||
/>
|
||||
<span className="text-gray-700">
|
||||
{toolName}
|
||||
</span>
|
||||
{tool.description && (
|
||||
<span className="text-gray-400 text-xs truncate">
|
||||
{tool.description}
|
||||
</span>
|
||||
)}
|
||||
</label>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{availableServers.length === 0 && (
|
||||
<p className="text-gray-500 text-sm">{t('groups.noServerOptions')}</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
27
frontend/src/components/icons/LanguageIcon.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export const LanguageIcon: React.FC<React.SVGProps<SVGSVGElement>> = (props) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<svg
|
||||
role="img"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
{...props}
|
||||
>
|
||||
<title>{t('common.language')}</title>
|
||||
<circle cx="12" cy="12" r="10" />
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M2 12h20" />
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M12 2a15.3 15.3 0 014 10 15.3 15.3 0 01-4 10 15.3 15.3 0 01-4-10 15.3 15.3 0 014-10z" />
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export default LanguageIcon;
|
||||
@@ -1,6 +1,46 @@
|
||||
import { ChevronDown, ChevronRight, Edit, Trash, Copy, Check, User, Settings, LogOut, Info } from 'lucide-react'
|
||||
import {
|
||||
ChevronDown,
|
||||
ChevronRight,
|
||||
Edit,
|
||||
Trash,
|
||||
Copy,
|
||||
Check,
|
||||
User,
|
||||
Settings,
|
||||
LogOut,
|
||||
Info,
|
||||
Play,
|
||||
Loader,
|
||||
CheckCircle,
|
||||
XCircle,
|
||||
AlertCircle,
|
||||
Link,
|
||||
FileCode,
|
||||
ChevronDown as DropdownIcon,
|
||||
Wrench
|
||||
} from 'lucide-react'
|
||||
|
||||
export { ChevronDown, ChevronRight, Edit, Trash, Copy, Check, User, Settings, LogOut, Info }
|
||||
export {
|
||||
ChevronDown,
|
||||
ChevronRight,
|
||||
Edit,
|
||||
Trash,
|
||||
Copy,
|
||||
Check,
|
||||
User,
|
||||
Settings,
|
||||
LogOut,
|
||||
Info,
|
||||
Play,
|
||||
Loader,
|
||||
CheckCircle,
|
||||
XCircle,
|
||||
AlertCircle,
|
||||
Link,
|
||||
FileCode,
|
||||
DropdownIcon,
|
||||
Wrench
|
||||
}
|
||||
|
||||
const LucideIcons = {
|
||||
ChevronDown,
|
||||
@@ -12,7 +52,15 @@ const LucideIcons = {
|
||||
User,
|
||||
Settings,
|
||||
LogOut,
|
||||
Info
|
||||
Info,
|
||||
Play,
|
||||
Loader,
|
||||
CheckCircle,
|
||||
XCircle,
|
||||
AlertCircle,
|
||||
Link,
|
||||
FileCode,
|
||||
DropdownIcon
|
||||
}
|
||||
|
||||
export default LucideIcons
|
||||
42
frontend/src/components/index.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
// Permission components unified export
|
||||
export { PermissionChecker, usePermissionCheck, usePermissionCheckAll } from './PermissionChecker';
|
||||
export { PERMISSIONS } from '../constants/permissions';
|
||||
|
||||
// Convenient permission check Hook
|
||||
export { useAuth } from '../contexts/AuthContext';
|
||||
|
||||
// Permission utility functions
|
||||
export const hasPermission = (
|
||||
userPermissions: string[] = [],
|
||||
requiredPermissions: string | string[],
|
||||
): boolean => {
|
||||
if (requiredPermissions === 'x' && !userPermissions.includes('x')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If user has '*' permission, it means they have all permissions
|
||||
if (userPermissions.includes('*')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Normalize required permissions to array
|
||||
const permissionsToCheck = Array.isArray(requiredPermissions)
|
||||
? requiredPermissions
|
||||
: [requiredPermissions];
|
||||
|
||||
// Check if user has any of the required permissions
|
||||
return permissionsToCheck.some((permission) => userPermissions.includes(permission));
|
||||
};
|
||||
|
||||
export const hasAllPermissions = (
|
||||
userPermissions: string[] = [],
|
||||
requiredPermissions: string[],
|
||||
): boolean => {
|
||||
// If user has '*' permission, it means they have all permissions
|
||||
if (userPermissions.includes('*')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if user has all required permissions
|
||||
return requiredPermissions.every((permission) => userPermissions.includes(permission));
|
||||
};
|
||||
@@ -1,27 +1,19 @@
|
||||
import React, { useState } from 'react';
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useAuth } from '@/contexts/AuthContext';
|
||||
import ThemeSwitch from '@/components/ui/ThemeSwitch';
|
||||
import LanguageSwitch from '@/components/ui/LanguageSwitch';
|
||||
import GitHubIcon from '@/components/icons/GitHubIcon';
|
||||
import SponsorIcon from '@/components/icons/SponsorIcon';
|
||||
import WeChatIcon from '@/components/icons/WeChatIcon';
|
||||
import DiscordIcon from '@/components/icons/DiscordIcon';
|
||||
import SponsorDialog from '@/components/ui/SponsorDialog';
|
||||
import WeChatDialog from '@/components/ui/WeChatDialog';
|
||||
|
||||
interface HeaderProps {
|
||||
onToggleSidebar: () => void;
|
||||
}
|
||||
|
||||
const Header: React.FC<HeaderProps> = ({ onToggleSidebar }) => {
|
||||
const { t, i18n } = useTranslation();
|
||||
const { auth } = useAuth();
|
||||
const [sponsorDialogOpen, setSponsorDialogOpen] = useState(false);
|
||||
const [wechatDialogOpen, setWechatDialogOpen] = useState(false);
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<header className="bg-white dark:bg-gray-800 shadow-sm z-10">
|
||||
<div className="flex justify-between items-center px-4 py-3">
|
||||
<div className="flex justify-between items-center px-3 py-3">
|
||||
<div className="flex items-center">
|
||||
{/* 侧边栏切换按钮 */}
|
||||
<button
|
||||
@@ -38,53 +30,27 @@ const Header: React.FC<HeaderProps> = ({ onToggleSidebar }) => {
|
||||
<h1 className="ml-4 text-xl font-bold text-gray-900 dark:text-white">{t('app.title')}</h1>
|
||||
</div>
|
||||
|
||||
{/* Theme Switch and Version */}
|
||||
<div className="flex items-center space-x-4">
|
||||
<span className="text-sm text-gray-500 dark:text-gray-400">
|
||||
{/* Theme Switch and Language Switcher and Version */}
|
||||
<div className="flex items-center space-x-1">
|
||||
<span className="text-sm text-gray-500 dark:text-gray-400 mr-2">
|
||||
{import.meta.env.PACKAGE_VERSION === 'dev'
|
||||
? import.meta.env.PACKAGE_VERSION
|
||||
: `v${import.meta.env.PACKAGE_VERSION}`}
|
||||
</span>
|
||||
|
||||
<a
|
||||
href="https://github.com/samanhappy/mcphub"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-gray-500 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200"
|
||||
className="p-2 rounded-md text-gray-500 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-700"
|
||||
aria-label="GitHub Repository"
|
||||
>
|
||||
<GitHubIcon className="h-5 w-5" />
|
||||
</a>
|
||||
{i18n.language === 'zh' ? (
|
||||
<button
|
||||
onClick={() => setWechatDialogOpen(true)}
|
||||
className="text-gray-500 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200 focus:outline-none"
|
||||
aria-label={t('wechat.label')}
|
||||
>
|
||||
<WeChatIcon className="h-5 w-5" />
|
||||
</button>
|
||||
) : (
|
||||
<a
|
||||
href="https://discord.gg/qMKNsn5Q"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-gray-500 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200"
|
||||
aria-label={t('discord.label')}
|
||||
>
|
||||
<DiscordIcon className="h-5 w-5" />
|
||||
</a>
|
||||
)}
|
||||
<button
|
||||
onClick={() => setSponsorDialogOpen(true)}
|
||||
className="text-gray-500 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200 focus:outline-none"
|
||||
aria-label={t('sponsor.label')}
|
||||
>
|
||||
<SponsorIcon className="h-5 w-5" />
|
||||
</button>
|
||||
<ThemeSwitch />
|
||||
<LanguageSwitch />
|
||||
</div>
|
||||
</div>
|
||||
<SponsorDialog open={sponsorDialogOpen} onOpenChange={setSponsorDialogOpen} />
|
||||
<WeChatDialog open={wechatDialogOpen} onOpenChange={setWechatDialogOpen} />
|
||||
</header>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { NavLink, useLocation } from 'react-router-dom';
|
||||
import { NavLink } from 'react-router-dom';
|
||||
import { useAuth } from '@/contexts/AuthContext';
|
||||
import { usePermissionCheck } from '../PermissionChecker';
|
||||
import UserProfileMenu from '@/components/ui/UserProfileMenu';
|
||||
|
||||
interface SidebarProps {
|
||||
@@ -15,11 +17,11 @@ interface MenuItem {
|
||||
|
||||
const Sidebar: React.FC<SidebarProps> = ({ collapsed }) => {
|
||||
const { t } = useTranslation();
|
||||
const location = useLocation();
|
||||
|
||||
const { auth } = useAuth();
|
||||
|
||||
// Application version from package.json (accessed via Vite environment variables)
|
||||
const appVersion = import.meta.env.PACKAGE_VERSION as string;
|
||||
|
||||
|
||||
// Menu item configuration
|
||||
const menuItems: MenuItem[] = [
|
||||
{
|
||||
@@ -50,6 +52,15 @@ const Sidebar: React.FC<SidebarProps> = ({ collapsed }) => {
|
||||
</svg>
|
||||
),
|
||||
},
|
||||
...(auth.user?.isAdmin && usePermissionCheck('x') ? [{
|
||||
path: '/users',
|
||||
label: t('nav.users'),
|
||||
icon: (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path d="M9 6a3 3 0 11-6 0 3 3 0 016 0zM17 6a3 3 0 11-6 0 3 3 0 016 0zM12.93 17c.046-.327.07-.66.07-1a6.97 6.97 0 00-1.5-4.33A5 5 0 0119 16v1h-6.07zM6 11a5 5 0 015 5v1H1v-1a5 5 0 015-5z" />
|
||||
</svg>
|
||||
),
|
||||
}] : []),
|
||||
{
|
||||
path: '/market',
|
||||
label: t('nav.market'),
|
||||
@@ -71,10 +82,9 @@ const Sidebar: React.FC<SidebarProps> = ({ collapsed }) => {
|
||||
];
|
||||
|
||||
return (
|
||||
<aside
|
||||
className={`bg-white dark:bg-gray-800 shadow-sm transition-all duration-300 ease-in-out flex flex-col h-full relative ${
|
||||
collapsed ? 'w-16' : 'w-64'
|
||||
}`}
|
||||
<aside
|
||||
className={`bg-white dark:bg-gray-800 shadow-sm transition-all duration-300 ease-in-out flex flex-col h-full relative ${collapsed ? 'w-16' : 'w-64'
|
||||
}`}
|
||||
>
|
||||
{/* Scrollable navigation area */}
|
||||
<div className="overflow-y-auto flex-grow">
|
||||
@@ -83,12 +93,11 @@ const Sidebar: React.FC<SidebarProps> = ({ collapsed }) => {
|
||||
<NavLink
|
||||
key={item.path}
|
||||
to={item.path}
|
||||
className={({ isActive }) =>
|
||||
`flex items-center px-3 py-2 rounded-md transition-colors ${
|
||||
isActive
|
||||
? 'bg-blue-100 dark:bg-blue-900 text-blue-800 dark:text-blue-200'
|
||||
: 'text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700'
|
||||
}`
|
||||
className={({ isActive }) =>
|
||||
`flex items-center px-2.5 py-2 rounded-lg transition-colors duration-200
|
||||
${isActive
|
||||
? 'bg-blue-50 text-blue-700'
|
||||
: 'text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-100'}`
|
||||
}
|
||||
end={item.path === '/'}
|
||||
>
|
||||
@@ -98,7 +107,7 @@ const Sidebar: React.FC<SidebarProps> = ({ collapsed }) => {
|
||||
))}
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
|
||||
{/* User profile menu fixed at the bottom */}
|
||||
<div className="p-3 bg-white dark:bg-gray-800">
|
||||
<UserProfileMenu collapsed={collapsed} version={appVersion} />
|
||||
|
||||
@@ -93,7 +93,7 @@ const AboutDialog: React.FC<AboutDialogProps> = ({ isOpen, onClose, version }) =
|
||||
<button
|
||||
onClick={checkForUpdates}
|
||||
disabled={isChecking}
|
||||
className={`mt-4 inline-flex items-center px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm text-sm font-medium
|
||||
className={`mt-4 inline-flex items-center px-4 py-2 border border-gray-200 dark:border-gray-600 rounded-md shadow-sm text-sm font-medium btn-secondary
|
||||
${isChecking
|
||||
? 'text-gray-400 dark:text-gray-500 bg-gray-100 dark:bg-gray-800'
|
||||
: 'text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-700 hover:bg-gray-50 dark:hover:bg-gray-600'
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ServerStatus } from '@/types';
|
||||
import { cn } from '../../utils/cn';
|
||||
|
||||
type BadgeVariant = 'default' | 'secondary' | 'outline' | 'destructive';
|
||||
@@ -19,11 +18,11 @@ const badgeVariants = {
|
||||
destructive: 'bg-red-500 text-white hover:bg-red-600',
|
||||
};
|
||||
|
||||
export function Badge({
|
||||
children,
|
||||
variant = 'default',
|
||||
className,
|
||||
onClick
|
||||
export function Badge({
|
||||
children,
|
||||
variant = 'default',
|
||||
className,
|
||||
onClick
|
||||
}: BadgeProps) {
|
||||
return (
|
||||
<span
|
||||
@@ -43,11 +42,11 @@ export function Badge({
|
||||
// For backward compatibility with existing code
|
||||
export const StatusBadge = ({ status }: { status: 'connected' | 'disconnected' | 'connecting' }) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
|
||||
const colors = {
|
||||
connecting: 'bg-yellow-100 text-yellow-800',
|
||||
connected: 'bg-green-100 text-green-800',
|
||||
disconnected: 'bg-red-100 text-red-800',
|
||||
connecting: 'status-badge-connecting',
|
||||
connected: 'status-badge-online',
|
||||
disconnected: 'status-badge-offline',
|
||||
};
|
||||
|
||||
// Map status to translation keys
|
||||
|
||||
142
frontend/src/components/ui/ConfirmDialog.tsx
Normal file
@@ -0,0 +1,142 @@
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
interface ConfirmDialogProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
onConfirm: () => void;
|
||||
title?: string;
|
||||
message: string;
|
||||
confirmText?: string;
|
||||
cancelText?: string;
|
||||
variant?: 'danger' | 'warning' | 'info';
|
||||
}
|
||||
|
||||
const ConfirmDialog: React.FC<ConfirmDialogProps> = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
onConfirm,
|
||||
title,
|
||||
message,
|
||||
confirmText,
|
||||
cancelText,
|
||||
variant = 'warning'
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
const getVariantStyles = () => {
|
||||
switch (variant) {
|
||||
case 'danger':
|
||||
return {
|
||||
icon: (
|
||||
<svg className="w-6 h-6 text-red-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L3.732 16.5c-.77.833.192 2.5 1.732 2.5z" />
|
||||
</svg>
|
||||
),
|
||||
confirmClass: 'bg-red-600 hover:bg-red-700 text-white',
|
||||
};
|
||||
case 'warning':
|
||||
return {
|
||||
icon: (
|
||||
<svg className="w-6 h-6 text-yellow-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L3.732 16.5c-.77.833.192 2.5 1.732 2.5z" />
|
||||
</svg>
|
||||
),
|
||||
confirmClass: 'bg-yellow-600 hover:bg-yellow-700 text-white',
|
||||
};
|
||||
case 'info':
|
||||
return {
|
||||
icon: (
|
||||
<svg className="w-6 h-6 text-blue-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<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>
|
||||
),
|
||||
confirmClass: 'bg-blue-600 hover:bg-blue-700 text-white',
|
||||
};
|
||||
default:
|
||||
return {
|
||||
icon: null,
|
||||
confirmClass: 'bg-blue-600 hover:bg-blue-700 text-white',
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const { icon, confirmClass } = getVariantStyles();
|
||||
|
||||
const handleBackdropClick = (e: React.MouseEvent) => {
|
||||
if (e.target === e.currentTarget) {
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||
if (e.key === 'Escape') {
|
||||
onClose();
|
||||
} else if (e.key === 'Enter') {
|
||||
onConfirm();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className="fixed inset-0 bg-black/50 z-[100] flex items-center justify-center p-4"
|
||||
onClick={handleBackdropClick}
|
||||
onKeyDown={handleKeyDown}
|
||||
tabIndex={-1}
|
||||
>
|
||||
<div
|
||||
className="bg-white rounded-lg shadow-xl max-w-md w-full transform transition-all duration-200 ease-out"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="confirm-dialog-title"
|
||||
aria-describedby="confirm-dialog-message"
|
||||
>
|
||||
<div className="p-6">
|
||||
<div className="flex items-start space-x-3">
|
||||
{icon && (
|
||||
<div className="flex-shrink-0">
|
||||
{icon}
|
||||
</div>
|
||||
)}
|
||||
<div className="flex-1">
|
||||
{title && (
|
||||
<h3
|
||||
id="confirm-dialog-title"
|
||||
className="text-lg font-medium text-gray-900 mb-2"
|
||||
>
|
||||
{title}
|
||||
</h3>
|
||||
)}
|
||||
<p
|
||||
id="confirm-dialog-message"
|
||||
className="text-gray-600 leading-relaxed"
|
||||
>
|
||||
{message}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end space-x-3 mt-6">
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="px-4 py-2 text-gray-600 hover:text-gray-800 hover:bg-gray-100 rounded-md transition-colors duration-150 btn-secondary"
|
||||
autoFocus
|
||||
>
|
||||
{cancelText || t('common.cancel')}
|
||||
</button>
|
||||
<button
|
||||
onClick={onConfirm}
|
||||
className={`px-4 py-2 rounded-md transition-colors duration-150 focus:outline-none focus:ring-2 focus:ring-offset-2 ${confirmClass} ${variant === 'danger' ? 'btn-danger' : variant === 'warning' ? 'btn-warning' : 'btn-primary'}`}
|
||||
>
|
||||
{confirmText || t('common.confirm')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ConfirmDialog;
|
||||
@@ -6,35 +6,42 @@ interface DeleteDialogProps {
|
||||
onConfirm: () => void
|
||||
serverName: string
|
||||
isGroup?: boolean
|
||||
isUser?: boolean
|
||||
}
|
||||
|
||||
const DeleteDialog = ({ isOpen, onClose, onConfirm, serverName, isGroup = false }: DeleteDialogProps) => {
|
||||
const DeleteDialog = ({ isOpen, onClose, onConfirm, serverName, isGroup = false, isUser = false }: DeleteDialogProps) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
if (!isOpen) return null
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-30 z-50 flex items-center justify-center p-4">
|
||||
<div className="fixed inset-0 bg-black/50 bg-opacity-30 z-50 flex items-center justify-center p-4">
|
||||
<div className="bg-white rounded-lg shadow-lg max-w-md w-full">
|
||||
<div className="p-6">
|
||||
<h3 className="text-lg font-medium text-gray-900 mb-3">
|
||||
{isGroup ? t('groups.confirmDelete') : t('server.confirmDelete')}
|
||||
{isUser
|
||||
? t('users.confirmDelete')
|
||||
: isGroup
|
||||
? t('groups.confirmDelete')
|
||||
: t('server.confirmDelete')}
|
||||
</h3>
|
||||
<p className="text-gray-500 mb-6">
|
||||
{isGroup
|
||||
? t('groups.deleteWarning', { name: serverName })
|
||||
: t('server.deleteWarning', { name: serverName })}
|
||||
{isUser
|
||||
? t('users.deleteWarning', { username: serverName })
|
||||
: isGroup
|
||||
? t('groups.deleteWarning', { name: serverName })
|
||||
: t('server.deleteWarning', { name: serverName })}
|
||||
</p>
|
||||
<div className="flex justify-end space-x-3">
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="px-4 py-2 text-gray-600 hover:text-gray-800"
|
||||
className="px-4 py-2 text-gray-600 hover:text-gray-800 btn-secondary"
|
||||
>
|
||||
{t('common.cancel')}
|
||||
</button>
|
||||
<button
|
||||
onClick={onConfirm}
|
||||
className="px-4 py-2 bg-red-500 text-white rounded hover:bg-red-600"
|
||||
className="px-4 py-2 bg-red-500 text-white rounded hover:bg-red-600 btn-danger"
|
||||
>
|
||||
{t('common.delete')}
|
||||
</button>
|
||||
|
||||
724
frontend/src/components/ui/DynamicForm.tsx
Normal file
@@ -0,0 +1,724 @@
|
||||
import React, { useState, useEffect, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ToolInputSchema } from '@/types';
|
||||
|
||||
interface JsonSchema {
|
||||
type: string;
|
||||
properties?: Record<string, JsonSchema>;
|
||||
required?: string[];
|
||||
items?: JsonSchema;
|
||||
enum?: any[];
|
||||
description?: string;
|
||||
default?: any;
|
||||
}
|
||||
|
||||
interface DynamicFormProps {
|
||||
schema: ToolInputSchema;
|
||||
onSubmit: (values: Record<string, any>) => void;
|
||||
onCancel: () => void;
|
||||
loading?: boolean;
|
||||
storageKey?: string; // Optional key for localStorage persistence
|
||||
title?: string; // Optional title to display instead of default parameters title
|
||||
}
|
||||
|
||||
const DynamicForm: React.FC<DynamicFormProps> = ({ schema, onSubmit, onCancel, loading = false, storageKey, title }) => {
|
||||
const { t } = useTranslation();
|
||||
const [formValues, setFormValues] = useState<Record<string, any>>({});
|
||||
const [errors, setErrors] = useState<Record<string, string>>({});
|
||||
const [isJsonMode, setIsJsonMode] = useState<boolean>(false);
|
||||
const [jsonText, setJsonText] = useState<string>('');
|
||||
const [jsonError, setJsonError] = useState<string>('');
|
||||
|
||||
// Convert ToolInputSchema to JsonSchema - memoized to prevent infinite re-renders
|
||||
const jsonSchema = useMemo(() => {
|
||||
const convertToJsonSchema = (schema: ToolInputSchema): JsonSchema => {
|
||||
const convertProperty = (prop: unknown): JsonSchema => {
|
||||
if (typeof prop === 'object' && prop !== null) {
|
||||
const obj = prop as any;
|
||||
return {
|
||||
type: obj.type || 'string',
|
||||
description: obj.description,
|
||||
enum: obj.enum,
|
||||
default: obj.default,
|
||||
properties: obj.properties ? Object.fromEntries(
|
||||
Object.entries(obj.properties).map(([key, value]) => [key, convertProperty(value)])
|
||||
) : undefined,
|
||||
required: obj.required,
|
||||
items: obj.items ? convertProperty(obj.items) : undefined,
|
||||
};
|
||||
}
|
||||
return { type: 'string' };
|
||||
};
|
||||
|
||||
return {
|
||||
type: schema.type,
|
||||
properties: schema.properties ? Object.fromEntries(
|
||||
Object.entries(schema.properties).map(([key, value]) => [key, convertProperty(value)])
|
||||
) : undefined,
|
||||
required: schema.required,
|
||||
};
|
||||
};
|
||||
|
||||
return convertToJsonSchema(schema);
|
||||
}, [schema]);
|
||||
|
||||
// Initialize form values with defaults or from localStorage
|
||||
useEffect(() => {
|
||||
const initializeValues = (schema: JsonSchema, path: string = ''): Record<string, any> => {
|
||||
const values: Record<string, any> = {};
|
||||
|
||||
if (schema.type === 'object' && schema.properties) {
|
||||
Object.entries(schema.properties).forEach(([key, propSchema]) => {
|
||||
const fullPath = path ? `${path}.${key}` : key;
|
||||
if (propSchema.default !== undefined) {
|
||||
values[key] = propSchema.default;
|
||||
} else if (propSchema.type === 'string') {
|
||||
values[key] = '';
|
||||
} else if (propSchema.type === 'number' || propSchema.type === 'integer') {
|
||||
values[key] = 0;
|
||||
} else if (propSchema.type === 'boolean') {
|
||||
values[key] = false;
|
||||
} else if (propSchema.type === 'array') {
|
||||
values[key] = [];
|
||||
} else if (propSchema.type === 'object') {
|
||||
// For objects with properties, recursively initialize
|
||||
if (propSchema.properties) {
|
||||
values[key] = initializeValues(propSchema, fullPath);
|
||||
} else {
|
||||
// For objects without properties, initialize as empty object
|
||||
values[key] = {};
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return values;
|
||||
};
|
||||
|
||||
let initialValues = initializeValues(jsonSchema);
|
||||
|
||||
// Try to load saved form data from localStorage
|
||||
if (storageKey) {
|
||||
try {
|
||||
const savedData = localStorage.getItem(storageKey);
|
||||
if (savedData) {
|
||||
const parsedData = JSON.parse(savedData);
|
||||
// Merge saved data with initial values, preserving structure
|
||||
initialValues = { ...initialValues, ...parsedData };
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Failed to load saved form data:', error);
|
||||
}
|
||||
}
|
||||
|
||||
setFormValues(initialValues);
|
||||
}, [jsonSchema, storageKey]);
|
||||
|
||||
// Sync JSON text with form values when switching modes
|
||||
useEffect(() => {
|
||||
if (isJsonMode && Object.keys(formValues).length > 0) {
|
||||
setJsonText(JSON.stringify(formValues, null, 2));
|
||||
setJsonError('');
|
||||
}
|
||||
}, [isJsonMode, formValues]);
|
||||
|
||||
const handleJsonTextChange = (text: string) => {
|
||||
setJsonText(text);
|
||||
setJsonError('');
|
||||
|
||||
try {
|
||||
const parsedJson = JSON.parse(text);
|
||||
setFormValues(parsedJson);
|
||||
|
||||
// Save to localStorage if storageKey is provided
|
||||
if (storageKey) {
|
||||
try {
|
||||
localStorage.setItem(storageKey, JSON.stringify(parsedJson));
|
||||
} catch (error) {
|
||||
console.warn('Failed to save form data to localStorage:', error);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
setJsonError(t('tool.invalidJsonFormat'));
|
||||
}
|
||||
};
|
||||
|
||||
const switchToJsonMode = () => {
|
||||
setJsonText(JSON.stringify(formValues, null, 2));
|
||||
setJsonError('');
|
||||
setIsJsonMode(true);
|
||||
};
|
||||
|
||||
const switchToFormMode = () => {
|
||||
// Validate JSON before switching
|
||||
if (jsonText.trim()) {
|
||||
try {
|
||||
const parsedJson = JSON.parse(jsonText);
|
||||
setFormValues(parsedJson);
|
||||
setJsonError('');
|
||||
setIsJsonMode(false);
|
||||
} catch (error) {
|
||||
setJsonError(t('tool.fixJsonBeforeSwitching'));
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
setIsJsonMode(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleInputChange = (path: string, value: any) => {
|
||||
setFormValues(prev => {
|
||||
const newValues = { ...prev };
|
||||
const keys = path.split('.');
|
||||
let current = newValues;
|
||||
|
||||
for (let i = 0; i < keys.length - 1; i++) {
|
||||
if (!current[keys[i]]) {
|
||||
current[keys[i]] = {};
|
||||
}
|
||||
current = current[keys[i]];
|
||||
}
|
||||
|
||||
current[keys[keys.length - 1]] = value;
|
||||
|
||||
// Save to localStorage if storageKey is provided
|
||||
if (storageKey) {
|
||||
try {
|
||||
localStorage.setItem(storageKey, JSON.stringify(newValues));
|
||||
} catch (error) {
|
||||
console.warn('Failed to save form data to localStorage:', error);
|
||||
}
|
||||
}
|
||||
|
||||
return newValues;
|
||||
});
|
||||
|
||||
// Clear error for this field
|
||||
if (errors[path]) {
|
||||
setErrors(prev => {
|
||||
const newErrors = { ...prev };
|
||||
delete newErrors[path];
|
||||
return newErrors;
|
||||
});
|
||||
}
|
||||
};
|
||||
const validateForm = (): boolean => {
|
||||
const newErrors: Record<string, string> = {};
|
||||
|
||||
const validateObject = (schema: JsonSchema, values: any, path: string = '') => {
|
||||
if (schema.type === 'object' && schema.properties) {
|
||||
Object.entries(schema.properties).forEach(([key, propSchema]) => {
|
||||
const fullPath = path ? `${path}.${key}` : key;
|
||||
const value = getNestedValue(values, fullPath);
|
||||
|
||||
// Check required fields
|
||||
if (schema.required?.includes(key) && (value === undefined || value === null || value === '')) {
|
||||
newErrors[fullPath] = `${key} is required`;
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate type
|
||||
if (value !== undefined && value !== null && value !== '') {
|
||||
if (propSchema.type === 'string' && typeof value !== 'string') {
|
||||
newErrors[fullPath] = `${key} must be a string`;
|
||||
} else if (propSchema.type === 'number' && typeof value !== 'number') {
|
||||
newErrors[fullPath] = `${key} must be a number`;
|
||||
} else if (propSchema.type === 'integer' && (!Number.isInteger(value) || typeof value !== 'number')) {
|
||||
newErrors[fullPath] = `${key} must be an integer`;
|
||||
} else if (propSchema.type === 'boolean' && typeof value !== 'boolean') {
|
||||
newErrors[fullPath] = `${key} must be a boolean`;
|
||||
} else if (propSchema.type === 'array' && Array.isArray(value)) {
|
||||
// Validate array items
|
||||
if (propSchema.items) {
|
||||
value.forEach((item: any, index: number) => {
|
||||
if (propSchema.items?.type === 'object' && propSchema.items.properties) {
|
||||
validateObject(propSchema.items, item, `${fullPath}.${index}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
} else if (propSchema.type === 'object' && typeof value === 'object') {
|
||||
validateObject(propSchema, value, fullPath);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
validateObject(jsonSchema, formValues);
|
||||
setErrors(newErrors);
|
||||
return Object.keys(newErrors).length === 0;
|
||||
};
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (validateForm()) {
|
||||
onSubmit(formValues);
|
||||
}
|
||||
};
|
||||
|
||||
const getNestedValue = (obj: any, path: string): any => {
|
||||
return path.split('.').reduce((current, key) => current?.[key], obj);
|
||||
};
|
||||
|
||||
const renderObjectField = (key: string, schema: JsonSchema, currentValue: any, onChange: (value: any) => void): React.ReactNode => {
|
||||
const value = currentValue?.[key];
|
||||
|
||||
if (schema.type === 'string') {
|
||||
if (schema.enum) {
|
||||
return (
|
||||
<select
|
||||
value={value || ''}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
className="w-full border rounded-md px-2 py-1 text-sm border-gray-300 focus:outline-none focus:ring-1 focus:ring-blue-500"
|
||||
>
|
||||
<option value="">{t('tool.selectOption')}</option>
|
||||
{schema.enum.map((option: any, idx: number) => (
|
||||
<option key={idx} value={option}>
|
||||
{option}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<input
|
||||
type="text"
|
||||
value={value || ''}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
className="w-full border rounded-md px-2 py-1 text-sm border-gray-300 focus:outline-none focus:ring-1 focus:ring-blue-500 form-input"
|
||||
placeholder={schema.description || t('tool.enterKey', { key })}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (schema.type === 'number' || schema.type === 'integer') {
|
||||
return (
|
||||
<input
|
||||
type="number"
|
||||
step={schema.type === 'integer' ? '1' : 'any'}
|
||||
value={value || ''}
|
||||
onChange={(e) => {
|
||||
const val = e.target.value === '' ? '' : schema.type === 'integer' ? parseInt(e.target.value) : parseFloat(e.target.value);
|
||||
onChange(val);
|
||||
}}
|
||||
className="w-full border rounded-md px-2 py-1 text-sm border-gray-300 focus:outline-none focus:ring-1 focus:ring-blue-500 form-input"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (schema.type === 'boolean') {
|
||||
return (
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={value || false}
|
||||
onChange={(e) => onChange(e.target.checked)}
|
||||
className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// Default to text input
|
||||
return (
|
||||
<input
|
||||
type="text"
|
||||
value={value || ''}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
className="w-full border rounded-md px-2 py-1 text-sm border-gray-300 focus:outline-none focus:ring-1 focus:ring-blue-500 form-input"
|
||||
placeholder={schema.description || t('tool.enterKey', { key })}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const renderField = (key: string, propSchema: JsonSchema, path: string = ''): React.ReactNode => {
|
||||
const fullPath = path ? `${path}.${key}` : key;
|
||||
const value = getNestedValue(formValues, fullPath);
|
||||
const error = errors[fullPath]; // Handle array type
|
||||
if (propSchema.type === 'array') {
|
||||
const arrayValue = getNestedValue(formValues, fullPath) || [];
|
||||
|
||||
return (
|
||||
<div key={fullPath} className="mb-6">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
{key}
|
||||
{(path ? getNestedValue(jsonSchema, path)?.required?.includes(key) : jsonSchema.required?.includes(key)) && <span className="text-status-red ml-1">*</span>}
|
||||
</label>
|
||||
{propSchema.description && (
|
||||
<p className="text-xs text-gray-500 mb-2">{propSchema.description}</p>
|
||||
)}
|
||||
|
||||
<div className="border border-gray-200 rounded-md p-3 bg-gray-50">
|
||||
{arrayValue.map((item: any, index: number) => (
|
||||
<div key={index} className="mb-3 p-3 bg-white border rounded-md">
|
||||
<div className="flex justify-between items-center mb-2">
|
||||
<span className="text-sm font-medium text-gray-600">{t('tool.item', { index: index + 1 })}</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
const newArray = [...arrayValue];
|
||||
newArray.splice(index, 1);
|
||||
handleInputChange(fullPath, newArray);
|
||||
}}
|
||||
className="text-status-red hover:text-red-700 text-sm"
|
||||
>
|
||||
{t('common.remove')}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{propSchema.items?.type === 'string' && propSchema.items.enum ? (
|
||||
<select
|
||||
value={item || ''}
|
||||
onChange={(e) => {
|
||||
const newArray = [...arrayValue];
|
||||
newArray[index] = e.target.value;
|
||||
handleInputChange(fullPath, newArray);
|
||||
}}
|
||||
className="w-full border rounded-md px-3 py-2 border-gray-300 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
>
|
||||
<option value="">{t('tool.selectOption')}</option>
|
||||
{propSchema.items.enum.map((option: any, idx: number) => (
|
||||
<option key={idx} value={option}>
|
||||
{option}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
) : propSchema.items?.type === 'object' && propSchema.items.properties ? (
|
||||
<div className="space-y-3">
|
||||
{Object.entries(propSchema.items.properties).map(([objKey, objSchema]) => (
|
||||
<div key={objKey}>
|
||||
<label className="block text-xs font-medium text-gray-600 mb-1">
|
||||
{objKey}
|
||||
{propSchema.items?.required?.includes(objKey) && <span className="text-status-red ml-1">*</span>}
|
||||
</label>
|
||||
{renderObjectField(objKey, objSchema as JsonSchema, item, (newValue) => {
|
||||
const newArray = [...arrayValue];
|
||||
newArray[index] = { ...newArray[index], [objKey]: newValue };
|
||||
handleInputChange(fullPath, newArray);
|
||||
})}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<input
|
||||
type="text"
|
||||
value={item || ''}
|
||||
onChange={(e) => {
|
||||
const newArray = [...arrayValue];
|
||||
newArray[index] = e.target.value;
|
||||
handleInputChange(fullPath, newArray);
|
||||
}}
|
||||
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"
|
||||
placeholder={t('tool.enterValue', { type: propSchema.items?.type || 'value' })}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
const newItem = propSchema.items?.type === 'object' ? {} : '';
|
||||
handleInputChange(fullPath, [...arrayValue, newItem]);
|
||||
}}
|
||||
className="w-full mt-2 px-3 py-2 text-sm text-blue-600 border border-blue-300 rounded-md hover:bg-blue-50"
|
||||
>
|
||||
{t('tool.addItem', { key })}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{error && <p className="text-status-red text-xs mt-1">{error}</p>}
|
||||
</div>
|
||||
);
|
||||
} // Handle object type
|
||||
if (propSchema.type === 'object') {
|
||||
if (propSchema.properties) {
|
||||
// Object with defined properties - render as nested form
|
||||
return (
|
||||
<div key={fullPath} className="mb-6">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
{key}
|
||||
{(path ? getNestedValue(jsonSchema, path)?.required?.includes(key) : jsonSchema.required?.includes(key)) && <span className="text-status-red ml-1">*</span>}
|
||||
</label>
|
||||
{propSchema.description && (
|
||||
<p className="text-xs text-gray-500 mb-2">{propSchema.description}</p>
|
||||
)}
|
||||
|
||||
<div className="border border-gray-200 rounded-md p-4 bg-gray-50">
|
||||
{Object.entries(propSchema.properties).map(([objKey, objSchema]) => (
|
||||
renderField(objKey, objSchema as JsonSchema, fullPath)
|
||||
))}
|
||||
</div>
|
||||
|
||||
{error && <p className="text-status-red text-xs mt-1">{error}</p>}
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
// Object without defined properties - render as JSON textarea
|
||||
return (
|
||||
<div key={fullPath} className="mb-4">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
{key}
|
||||
{(path ? getNestedValue(jsonSchema, path)?.required?.includes(key) : jsonSchema.required?.includes(key)) && <span className="text-status-red ml-1">*</span>}
|
||||
<span className="text-xs text-gray-500 ml-1">(JSON object)</span>
|
||||
</label>
|
||||
{propSchema.description && (
|
||||
<p className="text-xs text-gray-500 mb-2">{propSchema.description}</p>
|
||||
)}
|
||||
<textarea
|
||||
value={typeof value === 'object' ? JSON.stringify(value, null, 2) : value || '{}'}
|
||||
onChange={(e) => {
|
||||
try {
|
||||
const parsedValue = JSON.parse(e.target.value);
|
||||
handleInputChange(fullPath, parsedValue);
|
||||
} catch (err) {
|
||||
// Keep the string value if it's not valid JSON yet
|
||||
handleInputChange(fullPath, e.target.value);
|
||||
}
|
||||
}}
|
||||
placeholder={`{\n "key": "value"\n}`}
|
||||
className={`w-full border rounded-md px-3 py-2 font-mono text-sm ${error ? 'border-red-500' : 'border-gray-300'} focus:outline-none focus:ring-2 focus:ring-blue-500`}
|
||||
rows={4}
|
||||
/>
|
||||
{error && <p className="text-status-red text-xs mt-1">{error}</p>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
} if (propSchema.type === 'string') {
|
||||
if (propSchema.enum) {
|
||||
return (
|
||||
<div key={fullPath} className="mb-4">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
{key}
|
||||
{(path ? false : jsonSchema.required?.includes(key)) && <span className="text-status-red ml-1">*</span>}
|
||||
</label>
|
||||
{propSchema.description && (
|
||||
<p className="text-xs text-gray-500 mb-2">{propSchema.description}</p>
|
||||
)}
|
||||
<select
|
||||
value={value || ''}
|
||||
onChange={(e) => handleInputChange(fullPath, e.target.value)}
|
||||
className={`w-full border rounded-md px-3 py-2 ${error ? 'border-red-500' : 'border-gray-300'} focus:outline-none focus:ring-2 focus:ring-blue-500`}
|
||||
>
|
||||
<option value="">{t('tool.selectOption')}</option>
|
||||
{propSchema.enum.map((option, idx) => (
|
||||
<option key={idx} value={option}>
|
||||
{option}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
{error && <p className="text-status-red text-xs mt-1">{error}</p>}
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<div key={fullPath} className="mb-4">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
{key}
|
||||
{(path ? false : jsonSchema.required?.includes(key)) && <span className="text-status-red ml-1">*</span>}
|
||||
</label>
|
||||
{propSchema.description && (
|
||||
<p className="text-xs text-gray-500 mb-2">{propSchema.description}</p>
|
||||
)}
|
||||
<input
|
||||
type="text"
|
||||
value={value || ''}
|
||||
onChange={(e) => handleInputChange(fullPath, e.target.value)}
|
||||
className={`w-full border rounded-md px-3 py-2 ${error ? 'border-red' : 'border-gray-200'} focus:outline-none form-input`}
|
||||
/>
|
||||
{error && <p className="text-status-red text-xs mt-1">{error}</p>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
} if (propSchema.type === 'number' || propSchema.type === 'integer') {
|
||||
return (
|
||||
<div key={fullPath} className="mb-4">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
{key}
|
||||
{(path ? false : jsonSchema.required?.includes(key)) && <span className="text-status-red ml-1">*</span>}
|
||||
</label>
|
||||
{propSchema.description && (
|
||||
<p className="text-xs text-gray-500 mb-2">{propSchema.description}</p>
|
||||
)}
|
||||
<input
|
||||
type="number"
|
||||
step={propSchema.type === 'integer' ? '1' : 'any'}
|
||||
value={value || ''}
|
||||
onChange={(e) => {
|
||||
const val = e.target.value === '' ? '' : propSchema.type === 'integer' ? parseInt(e.target.value) : parseFloat(e.target.value);
|
||||
handleInputChange(fullPath, val);
|
||||
}}
|
||||
className={`w-full border rounded-md px-3 py-2 form-input ${error ? 'border-red-500' : 'border-gray-300'} focus:outline-none focus:ring-2 focus:ring-blue-500`}
|
||||
/>
|
||||
{error && <p className="text-status-red text-xs mt-1">{error}</p>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (propSchema.type === 'boolean') {
|
||||
return (
|
||||
<div key={fullPath} className="mb-4">
|
||||
<div className="flex items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={value || false}
|
||||
onChange={(e) => handleInputChange(fullPath, e.target.checked)}
|
||||
className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"
|
||||
/>
|
||||
<label className="ml-2 block text-sm text-gray-700">
|
||||
{key}
|
||||
{(path ? false : jsonSchema.required?.includes(key)) && <span className="text-status-red ml-1">*</span>}
|
||||
</label>
|
||||
</div>
|
||||
{propSchema.description && (
|
||||
<p className="text-xs text-gray-500 mt-1">{propSchema.description}</p>
|
||||
)}
|
||||
{error && <p className="text-status-red text-xs mt-1">{error}</p>}
|
||||
</div>
|
||||
);
|
||||
} // For other types, show as text input with description
|
||||
return (
|
||||
<div key={fullPath} className="mb-4">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
{key}
|
||||
{(path ? false : jsonSchema.required?.includes(key)) && <span className="text-status-red ml-1">*</span>}
|
||||
<span className="text-xs text-gray-500 ml-1">({propSchema.type})</span>
|
||||
</label>
|
||||
{propSchema.description && (
|
||||
<p className="text-xs text-gray-500 mb-2">{propSchema.description}</p>
|
||||
)}
|
||||
<input
|
||||
type="text"
|
||||
value={value || ''}
|
||||
onChange={(e) => handleInputChange(fullPath, e.target.value)}
|
||||
placeholder={t('tool.enterValue', { type: propSchema.type })}
|
||||
className={`w-full border rounded-md px-3 py-2 ${error ? 'border-red-500' : 'border-gray-300'} focus:outline-none focus:ring-2 focus:ring-blue-500 form-input`}
|
||||
/>
|
||||
{error && <p className="text-status-red text-xs mt-1">{error}</p>}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
if (!jsonSchema.properties) {
|
||||
return (
|
||||
<div className="p-4 bg-gray-50 rounded-md">
|
||||
<p className="text-sm text-gray-600">{t('tool.noParameters')}</p>
|
||||
<div className="flex justify-end space-x-2 mt-4">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onCancel}
|
||||
className="px-4 py-2 text-sm text-gray-600 bg-gray-100 rounded-md hover:bg-gray-200"
|
||||
>
|
||||
{t('tool.cancel')}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => onSubmit({})}
|
||||
disabled={loading}
|
||||
className="px-4 py-2 text-sm text-white bg-blue-600 rounded-md hover:bg-blue-700 disabled:opacity-50"
|
||||
>
|
||||
{loading ? t('tool.running') : t('tool.runTool')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{/* Mode Toggle */}
|
||||
<div className="flex justify-between items-center pb-3">
|
||||
<h6 className="text-md font-medium text-gray-900">{title}</h6>
|
||||
<div className="flex space-x-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={switchToFormMode}
|
||||
className={`px-3 py-1 text-sm rounded-md transition-colors ${!isJsonMode
|
||||
? 'bg-blue-100 text-blue-800 rounded hover:bg-blue-200 text-sm btn-primary'
|
||||
: 'text-sm text-gray-600 bg-gray-200 rounded hover:bg-gray-300 btn-secondary'
|
||||
}`}
|
||||
>
|
||||
{t('tool.formMode')}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={switchToJsonMode}
|
||||
className={`px-3 py-1 text-sm rounded-md transition-colors ${isJsonMode
|
||||
? 'px-4 py-1 bg-blue-100 text-blue-800 rounded hover:bg-blue-200 text-sm btn-primary'
|
||||
: 'text-sm text-gray-600 bg-gray-200 rounded hover:bg-gray-300 btn-secondary'
|
||||
}`}
|
||||
>
|
||||
{t('tool.jsonMode')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* JSON Mode */}
|
||||
{isJsonMode ? (
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
{t('tool.jsonConfiguration')}
|
||||
</label>
|
||||
<textarea
|
||||
value={jsonText}
|
||||
onChange={(e) => handleJsonTextChange(e.target.value)}
|
||||
placeholder={`{\n "key": "value"\n}`}
|
||||
className={`w-full h-64 border rounded-md px-3 py-2 font-mono text-sm resize-y form-input ${jsonError ? 'border-red-500' : 'border-gray-300'
|
||||
} focus:outline-none focus:ring-2 focus:ring-blue-500`}
|
||||
/>
|
||||
{jsonError && <p className="text-status-red text-xs mt-1">{jsonError}</p>}
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end space-x-2 pt-4">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onCancel}
|
||||
className="px-4 py-1 text-sm text-gray-600 bg-gray-200 rounded hover:bg-gray-300 btn-secondary"
|
||||
>
|
||||
{t('tool.cancel')}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
try {
|
||||
const parsedJson = JSON.parse(jsonText);
|
||||
onSubmit(parsedJson);
|
||||
} catch (error) {
|
||||
setJsonError(t('tool.invalidJsonFormat'));
|
||||
}
|
||||
}}
|
||||
disabled={loading || !!jsonError}
|
||||
className="px-4 py-1 bg-blue-100 text-blue-800 rounded hover:bg-blue-200 text-sm btn-primary"
|
||||
>
|
||||
{loading ? t('tool.running') : t('tool.runTool')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
/* Form Mode */
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
{Object.entries(jsonSchema.properties || {}).map(([key, propSchema]) =>
|
||||
renderField(key, propSchema)
|
||||
)}
|
||||
|
||||
<div className="flex justify-end space-x-2 pt-4">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onCancel}
|
||||
className="px-4 py-1 text-sm text-gray-600 bg-gray-200 rounded hover:bg-gray-300 btn-secondary"
|
||||
>
|
||||
{t('tool.cancel')}
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className="px-4 py-1 bg-blue-100 text-blue-800 rounded hover:bg-blue-200 text-sm btn-primary"
|
||||
>
|
||||
{loading ? t('tool.running') : t('tool.runTool')}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DynamicForm;
|
||||
83
frontend/src/components/ui/LanguageSwitch.tsx
Normal file
@@ -0,0 +1,83 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import LanguageIcon from '@/components/icons/LanguageIcon';
|
||||
|
||||
const LanguageSwitch: React.FC = () => {
|
||||
const { i18n } = useTranslation();
|
||||
const [languageDropdownOpen, setLanguageDropdownOpen] = useState(false);
|
||||
const [currentLanguage, setCurrentLanguage] = useState(i18n.language);
|
||||
|
||||
// Available languages
|
||||
const availableLanguages = [
|
||||
{ code: 'en', label: 'English' },
|
||||
{ code: 'zh', label: '中文' }
|
||||
];
|
||||
|
||||
// Update current language when it changes
|
||||
useEffect(() => {
|
||||
setCurrentLanguage(i18n.language);
|
||||
}, [i18n.language]);
|
||||
|
||||
// Close dropdown when clicking outside
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
const target = event.target as HTMLElement;
|
||||
if (!target.closest('.language-dropdown')) {
|
||||
setLanguageDropdownOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (languageDropdownOpen) {
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
}
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', handleClickOutside);
|
||||
};
|
||||
}, [languageDropdownOpen]);
|
||||
|
||||
const handleLanguageChange = (lang: string) => {
|
||||
localStorage.setItem('i18nextLng', lang);
|
||||
setLanguageDropdownOpen(false);
|
||||
window.location.reload();
|
||||
};
|
||||
|
||||
// Always show dropdown for language selection
|
||||
const handleLanguageToggle = () => {
|
||||
setLanguageDropdownOpen(!languageDropdownOpen);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="relative language-dropdown">
|
||||
<button
|
||||
onClick={handleLanguageToggle}
|
||||
className="p-2 rounded-md text-gray-500 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-700 focus:outline-none"
|
||||
aria-label="Language Switcher"
|
||||
>
|
||||
<LanguageIcon className="h-5 w-5" />
|
||||
</button>
|
||||
|
||||
{/* Show dropdown when opened */}
|
||||
{languageDropdownOpen && (
|
||||
<div className="absolute right-0 mt-2 w-24 bg-white dark:bg-gray-800 rounded-md shadow-lg border border-gray-200 dark:border-gray-700 z-50">
|
||||
<div>
|
||||
{availableLanguages.map((lang) => (
|
||||
<button
|
||||
key={lang.code}
|
||||
onClick={() => handleLanguageChange(lang.code)}
|
||||
className={`flex items-center w-full px-4 py-2 text-sm hover:bg-gray-100 dark:hover:bg-gray-700 ${currentLanguage.startsWith(lang.code)
|
||||
? 'bg-blue-50 text-blue-700'
|
||||
: 'text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-100'
|
||||
}`}
|
||||
>
|
||||
{lang.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default LanguageSwitch;
|
||||
@@ -6,34 +6,33 @@ interface PaginationProps {
|
||||
onPageChange: (page: number) => void;
|
||||
}
|
||||
|
||||
const Pagination: React.FC<PaginationProps> = ({
|
||||
currentPage,
|
||||
totalPages,
|
||||
onPageChange
|
||||
const Pagination: React.FC<PaginationProps> = ({
|
||||
currentPage,
|
||||
totalPages,
|
||||
onPageChange
|
||||
}) => {
|
||||
// Generate page buttons
|
||||
const getPageButtons = () => {
|
||||
const buttons = [];
|
||||
const maxDisplayedPages = 5; // Maximum number of page buttons to display
|
||||
|
||||
|
||||
// Always display first page
|
||||
buttons.push(
|
||||
<button
|
||||
key="first"
|
||||
onClick={() => onPageChange(1)}
|
||||
className={`px-3 py-1 mx-1 rounded ${
|
||||
currentPage === 1
|
||||
? 'bg-blue-500 text-white'
|
||||
: 'bg-gray-200 hover:bg-gray-300 text-gray-700'
|
||||
}`}
|
||||
className={`px-3 py-1 mx-1 rounded ${currentPage === 1
|
||||
? 'bg-blue-500 text-white btn-primary'
|
||||
: 'bg-gray-200 hover:bg-gray-300 text-gray-700 btn-secondary'
|
||||
}`}
|
||||
>
|
||||
1
|
||||
</button>
|
||||
);
|
||||
|
||||
|
||||
// Start range
|
||||
let startPage = Math.max(2, currentPage - Math.floor(maxDisplayedPages / 2));
|
||||
|
||||
const startPage = Math.max(2, currentPage - Math.floor(maxDisplayedPages / 2));
|
||||
|
||||
// If we're showing ellipsis after first page
|
||||
if (startPage > 2) {
|
||||
buttons.push(
|
||||
@@ -42,24 +41,23 @@ const Pagination: React.FC<PaginationProps> = ({
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
// Middle pages
|
||||
for (let i = startPage; i <= Math.min(totalPages - 1, startPage + maxDisplayedPages - 3); i++) {
|
||||
buttons.push(
|
||||
<button
|
||||
key={i}
|
||||
onClick={() => onPageChange(i)}
|
||||
className={`px-3 py-1 mx-1 rounded ${
|
||||
currentPage === i
|
||||
? 'bg-blue-500 text-white'
|
||||
: 'bg-gray-200 hover:bg-gray-300 text-gray-700'
|
||||
}`}
|
||||
className={`px-3 py-1 mx-1 rounded ${currentPage === i
|
||||
? 'bg-blue-500 text-white btn-primary'
|
||||
: 'bg-gray-200 hover:bg-gray-300 text-gray-700 btn-secondary'
|
||||
}`}
|
||||
>
|
||||
{i}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
// If we're showing ellipsis before last page
|
||||
if (startPage + maxDisplayedPages - 3 < totalPages - 1) {
|
||||
buttons.push(
|
||||
@@ -68,24 +66,23 @@ const Pagination: React.FC<PaginationProps> = ({
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
// Always display last page if there's more than one page
|
||||
if (totalPages > 1) {
|
||||
buttons.push(
|
||||
<button
|
||||
key="last"
|
||||
onClick={() => onPageChange(totalPages)}
|
||||
className={`px-3 py-1 mx-1 rounded ${
|
||||
currentPage === totalPages
|
||||
? 'bg-blue-500 text-white'
|
||||
: 'bg-gray-200 hover:bg-gray-300 text-gray-700'
|
||||
}`}
|
||||
className={`px-3 py-1 mx-1 rounded ${currentPage === totalPages
|
||||
? 'bg-blue-500 text-white btn-primary'
|
||||
: 'bg-gray-200 hover:bg-gray-300 text-gray-700 btn-secondary'
|
||||
}`}
|
||||
>
|
||||
{totalPages}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
return buttons;
|
||||
};
|
||||
|
||||
@@ -99,25 +96,23 @@ const Pagination: React.FC<PaginationProps> = ({
|
||||
<button
|
||||
onClick={() => onPageChange(Math.max(1, currentPage - 1))}
|
||||
disabled={currentPage === 1}
|
||||
className={`px-3 py-1 rounded mr-2 ${
|
||||
currentPage === 1
|
||||
? 'bg-gray-100 text-gray-400 cursor-not-allowed'
|
||||
: 'bg-gray-200 hover:bg-gray-300 text-gray-700'
|
||||
}`}
|
||||
className={`px-3 py-1 rounded mr-2 ${currentPage === 1
|
||||
? 'bg-gray-100 text-gray-400 cursor-not-allowed'
|
||||
: 'bg-gray-200 hover:bg-gray-300 text-gray-700 btn-secondary'
|
||||
}`}
|
||||
>
|
||||
« Prev
|
||||
</button>
|
||||
|
||||
|
||||
<div className="flex">{getPageButtons()}</div>
|
||||
|
||||
|
||||
<button
|
||||
onClick={() => onPageChange(Math.min(totalPages, currentPage + 1))}
|
||||
disabled={currentPage === totalPages}
|
||||
className={`px-3 py-1 rounded ml-2 ${
|
||||
currentPage === totalPages
|
||||
? 'bg-gray-100 text-gray-400 cursor-not-allowed'
|
||||
: 'bg-gray-200 hover:bg-gray-300 text-gray-700'
|
||||
}`}
|
||||
className={`px-3 py-1 rounded ml-2 ${currentPage === totalPages
|
||||
? 'bg-gray-100 text-gray-400 cursor-not-allowed'
|
||||
: 'bg-gray-200 hover:bg-gray-300 text-gray-700 btn-secondary'
|
||||
}`}
|
||||
>
|
||||
Next »
|
||||
</button>
|
||||
|
||||