内容
# MCP 代理服务
企业级 MCP(模型上下文协议)代理,连接 LLM 客户端与现有的 RESTful 微服务。配置任何 REST API 为 MCP 工具 - 无需更改您的微服务。
## 目录
- [功能特性](#功能特性)
- [前提条件](#前提条件)
- [快速入门](#快速入门)
- [环境设置](#环境设置)
- [配置](#配置)
- [CLI 使用](#CLI-使用)
- [传输模式](#传输模式)
- [监控](#监控)
- [安全性](#安全性)
- [测试](#测试)
- [项目结构](#项目结构)
- [故障排除](#故障排除)
- [脚本参考](#脚本参考)
- [技术栈](#技术栈)
- [相关文档](#相关文档)
- [贡献](#贡献)
- [许可证](#许可证)
## 功能特性
- **协议转换** — 自动将 MCP 工具调用转换为 REST API 请求
- **动态工具注册** — 通过 YAML 配置定义 REST 端点为 MCP 工具
- **OpenAPI 导入** — 从 OpenAPI/Swagger 规范生成工具定义
- **传输无关** — 支持 stdio(适用于 Claude Desktop、CLI)和 HTTP(流式 HTTP,支持 SSE)
- **断路器** — 具有自动服务恢复的容错能力(opossum)
- **速率限制** — 固定窗口、滑动窗口和令牌桶策略
- **访问控制** — 服务和工具级别的基于角色的权限
- **身份验证** — API 密钥、Bearer 令牌、基本身份验证和 OAuth2 客户端凭证
- **请求验证** — 类型检查、模式匹配、格式验证(电子邮件、URL、UUID)
- **请求取消** — 使用信号传播到上游服务中止进行中的请求
- **响应转换** — JSON、文本和 XML 响应格式转换
- **监控** — Prometheus 指标、健康检查、就绪探测
- **重试和抖动回退** — 防止故障恢复时的 thundering herd 问题
- **优雅关闭** — SIGINT/SIGTERM 时的干净连接排空
## 前提条件
- **Node.js** >= 18.0.0
- **npm** >= 8.0.0
- **Docker** (可选,用于容器化部署)
- **Redis** (可选,用于跨多个实例的分布式速率限制)
## 快速入门
```bash
# 安装依赖
npm install
# 复制环境文件
cp .env.example .env
# 构建
npm run build
# 使用 stdio 传输(默认,适用于 Claude Desktop)
npm run start:stdio
# 使用 HTTP 传输
npm run start:http
```
## 环境设置
### 开发环境
```bash
# .env
NODE_ENV=development
LOG_LEVEL=debug
# 服务器
MCP_SERVER_PORT=3000
HEALTH_CHECK_PORT=8080
# Redis(可选 - 如果未配置,则使用内存速率限制)
REDIS_HOST=localhost
REDIS_PORT=6379
# 监控
METRICS_ENABLED=true
```
在开发模式下运行并启用热重载:
```bash
npm run dev
```
### 生产环境
```bash
# .env
NODE_ENV=production
LOG_LEVEL=info
# 服务器
MCP_SERVER_PORT=3000
HEALTH_CHECK_PORT=8080
# Redis(分布式速率限制必填)
REDIS_HOST=redis.internal
REDIS_PORT=6379
REDIS_PASSWORD=your-secure-password
# MCP 服务器身份验证(保护 /mcp 端点)
MCP_SERVER_AUTH_ENABLED=true
MCP_SERVER_AUTH_TYPE=bearer
MCP_SERVER_AUTH_TOKEN_ENV=MCP_AUTH_TOKEN
MCP_AUTH_TOKEN=your-secret-token-here
# 服务身份验证令牌
USER_SERVICE_TOKEN=your_user_service_token
PAYMENT_API_KEY=your_payment_api_key
ANALYTICS_CLIENT_SECRET=your_oauth_client_secret
# OAuth2(如果使用 oauth2 身份验证策略)
OAUTH2_CLIENT_ID=your_client_id
OAUTH2_CLIENT_SECRET=your_client_secret
OAUTH2_TOKEN_URL=https://auth.example.com/oauth/token
# 监控
METRICS_ENABLED=true
PROMETHEUS_PORT=9090
```
使用 Docker 部署:
```bash
docker build -t mcp-proxy .
docker run -p 3000:3000 -p 8080:8080 \
-v ./config:/app/config:ro \
--env-file .env \
mcp-proxy
```
或使用 Docker Compose(包括 Redis):
```bash
docker-compose up
```
## 配置
服务配置在 YAML 中定义。代理默认读取 `config/services.yaml`。
### 指定自定义配置文件
使用 `--config`(或 `-c`)标志加载不同的配置文件:
```bash
# 使用自定义配置文件启用 stdio
node dist/server/unified-server.js --transport stdio --config ./config/my-services.yaml
# 使用自定义配置文件启用 HTTP
node dist/server/unified-server.js --transport http --port 3000 --config /absolute/path/to/services.yaml
```
> **重要:** 配置路径相对于**进程的工作目录**解析,而不是相对于项目根目录。当代理由外部工具(Claude Desktop、LangChain 等)启动时,工作目录可能与项目根目录不同。使用**绝对路径**以避免 `CONFIGURATION_ERROR` 失败。
### 最简示例(无身份验证)
```yaml
version: "1.0"
settings:
log_level: "info"
metrics_enabled: true
health_check_port: 8080
services:
- name: "jsonplaceholder"
description: "JSONPlaceholder - 免费的 REST API"
base_url: "https://jsonplaceholder.typicode.com"
tools:
- name: "get_post"
description: "按 ID 获取博客帖子"
method: "GET"
path: "/posts/{id}"
parameters:
- name: "id"
type: "number"
required: true
description: "帖子 ID(1-100)"
in: "path"
- name: "list_posts"
description: "列出所有博客帖子"
method: "GET"
path: "/posts"
- name: "get_user"
description: "按 ID 获取用户"
method: "GET"
path: "/users/{id}"
parameters:
- name: "id"
type: "number"
required: true
description: "用户 ID(1-10)"
in: "path"
```
### 完整企业示例
```yaml
version: "1.0"
settings:
log_level: "info"
metrics_enabled: true
health_check_port: 8080
mcp_server_port: 3000
services:
- name: "user_service"
description: "用户管理服务"
base_url: "https://api.example.com/v1/users"
auth:
type: "bearer"
config:
token_env: "USER_SERVICE_TOKEN"
circuit_breaker:
enabled: true
timeout: 3000
error_threshold: 50
reset_timeout: 30000
volume_threshold: 10
rate_limit:
enabled: true
max_requests: 100
window: 60000
strategy: "sliding_window"
access_control:
enabled: true
allowed_roles: ["admin", "user"]
required_permissions: ["read:users"]
retry:
enabled: true
max_attempts: 3
backoff_ms: 1000
timeout: 5000
tools:
- name: "create_user"
description: "创建新用户账户"
method: "POST"
path: "/"
access_control:
enabled: true
required_permissions: ["write:users"]
parameters:
- name: "username"
type: "string"
required: true
description: "新账户的用户名"
in: "body"
validation:
min_length: 3
max_length: 50
pattern: "^[a-zA-Z0-9_]+$"
- name: "email"
type: "string"
required: true
description: "电子邮件地址"
in: "body"
validation:
format: "email"
request_body:
content_type: "application/json"
template: |
{
"username": "{{username}}",
"email": "{{email}}",
"created_at": "{{$timestamp}}"
}
response:
success_codes: [201]
error_codes: [400, 409]
transform: "json"
- name: "payment_service"
description: "支付处理服务"
base_url: "https://api.payments.example.com/v2"
auth:
type: "api_key"
config:
key_env: "PAYMENT_API_KEY"
header_name: "X-Payment-API-Key"
circuit_breaker:
enabled: true
timeout: 5000
error_threshold: 30
reset_timeout: 60000
volume_threshold: 5
rate_limit:
enabled: true
max_requests: 50
window: 60000
strategy: "token_bucket"
tools:
- name: "create_payment"
description: "处理支付交易"
method: "POST"
path: "/payments"
parameters:
- name: "amount"
type: "number"
required: true
description: "以美分表示的支付金额"
in: "body"
validation:
minimum: 1
- name: "currency"
type: "string"
required: true
description: "货币代码"
in: "body"
validation:
enum: ["USD", "EUR", "GBP"]
- name: "analytics_service"
description: "分析和报告服务"
base_url: "https://analytics.example.com/api"
auth:
type: "oauth2"
config:
client_id: "analytics_client"
client_secret_env: "ANALYTICS_CLIENT_SECRET"
token_url: "https://auth.example.com/oauth/token"
scope: "analytics:read analytics:write"
tools:
- name: "get_report"
description: "生成分析报告"
method: "GET"
path: "/reports/{report_id}"
parameters:
- name: "report_id"
type: "string"
required: true
description: "报告标识符"
in: "path"
- name: "format"
type: "string"
required: false
description: "报告格式"
in: "query"
validation:
enum: ["json", "csv", "pdf"]
```
### 凭证管理
所有密钥都通过环境变量进行管理 — 永远不会存储在配置文件中。`.env` 文件被 gitignore。在生产环境中使用机密管理器(Vault、AWS 机密管理器等)。
### JSON 模板注入防护
请求正文模板使用 `JSON.stringify().slice(1,-1)` 转义用户提供的数值,然后再进行替换。这可以防止注入额外的 JSON 键或格式错误的负载:
```
输入: hello", "admin": true, "x": "y
结果: {"message": "hello\", \"admin\": true, \"x\": \"y"}
^--- 注入被中和,视为字符串
```
### 防御时序攻击的令牌比较
身份验证令牌比较使用 `crypto.timingSafeEqual` 和缓冲区填充来防止时序攻击。在比较之前,两个缓冲区都被填充为相等的长度,从而消除了基于长度的时序泄漏。
### 加密会话 ID
HTTP 传输会话使用 `crypto.randomUUID()` (基于 CSPRNG)而不是可预测的 `Math.random()`。这可以防止会话预测攻击。
### 输入验证
- **配置**: Zod 模式验证所有配置(类型、范围、必填字段)在加载时
- **运行时**:每个工具的参数验证检查类型、模式、格式和范围,然后再进行任何上游调用
### 速率限制
保护上游服务免受滥用。支持每个服务和每个用户使用 Redis 进行分布式强制执行。
## 测试
```bash
# 单元测试
npm test
# 单元测试并生成覆盖率报告
npm run test:coverage
# 监视模式
npm run test:watch
# 端到端测试(stdio 传输 + 取消)
npm run test:e2e
# 端到端测试:HTTP 传输
npm run test:http
# 端到端测试:HTTP 传输并启用身份验证
npm run test:http:auth
# 所有测试(单元测试 + 端到端测试)
npm run test:all
# 类型检查
npm run typecheck
```
### 测试覆盖率
| 区域 | 测试 |
|------|-------|
| 配置模式验证 | Zod 模式正确性 |
| 模板注入 | JSON 转义和注入中和 |
| 健康检查器 | Ping、断路器状态、多服务 |
| REST 客户端 | Ping、超时、错误处理 |
| 访问控制 | 角色/权限检查、拒绝/允许 |
| 请求验证器 | 类型、模式、格式、范围验证 |
| 防御时序比较 | 长度正确性,无早期返回 |
| 端到端 stdio | stdin/stdout 上的完整工具调用往返 |
| 端到端 HTTP | 带有会话管理的 HTTP 传输 |
| 端到端取消 | 请求中止传播 |
## 项目结构
```
mcp-proxy/
├── src/
│ ├── core/
│ │ └── mcp-engine.ts # 传输无关的 MCP 协议引擎
│ ├── transport/
│ │ ├── index.ts # 传输导出
│ │ ├── types.ts # ITransport 接口、配置、状态
│ │ ├── factory.ts # 传输工厂(createTransport)
│ │ ├── stdio-transport.ts # stdio 传输实现
│ │ └── http-transport.ts # HTTP 传输(Streamable HTTP + SSE)
│ ├── server/
│ │ ├── unified-server.ts # 主入口:CLI 参数解析 + 启动
│ │ └── tool-registry.ts # 动态工具注册
│ ├── config/
│ │ ├── loader.ts # YAML/JSON 配置加载器
│ │ ├── schema.ts # Zod 验证模式
│ │ └── openapi-parser.ts # OpenAPI/Swagger → 工具配置解析器
│ ├── rest/
│ │ ├── client.ts # 带有断路器 + 重试的 Axios HTTP 客户端
│ │ └── transformer.ts # 响应转换(JSON、文本、XML)
│ ├── auth/
│ │ ├── manager.ts # 身份验证策略管理器
│ │ ├── types.ts # 身份验证类型
│ │ └── strategies/
│ │ ├── api-key.ts
│ │ ├── bearer.ts
│ │ ├── basic.ts
│ │ └── oauth2.ts
│ ├── middleware/
│ │ ├── rate-limiter.ts # 速率限制(固定/滑动/令牌桶)
│ │ ├── access-control.ts # RBAC 实现
│ │ └── validator.ts # 请求参数验证
│ ├── monitoring/
│ │ ├── metrics.ts # Prometheus 风格的指标收集器
│ │ ├── health.ts # 健康检查器(ping + 断路器)
│ │ └── http-server.ts # 监控 HTTP 服务器(/health, /metrics)
│ ├── types/
│ │ └── config.ts # ProxyConfig, ServiceConfig, ToolConfig 类型
│ ├── utils/
│ │ ├── errors.ts # 自定义错误类
│ │ └── logger.ts # Winston 结构化日志记录
│ └── cli/
│ ├── index.ts # CLI 入口点(commander)
│ └── commands/
│ ├── generate.ts # 从 OpenAPI 生成配置
│ ├── validate.ts # 验证配置
│ └── serve.ts # 启动服务器
├── config/
│ ├── services.yaml # 活动服务配置
│ ├── example-services.yaml # 功能齐全的示例
│ └── openapi/
│ └── example-user-api.yaml # 示例 OpenAPI 规范
├── tests/
│ ├── unit/ # 单元测试(Jest)
│ └── e2e/ # 端到端测试
├── Dockerfile # 多阶段生产构建
├── docker-compose.yaml # Docker Compose(代理 + Redis)
├── package.json
├── tsconfig.json
└── jest.config.js
```
## 故障排除
### 断路器保持打开
**症状**:所有服务请求立即失败,提示“服务暂时不可用”。
**原因**:错误率在 `volume_threshold` 请求内超过了 `error_threshold` 百分比。
**解决方法**:
1. 检查上游服务是否健康:`curl <service_base_url>/health`
2. 查看日志以查找根本原因:`LOG_LEVEL=debug npm run start:http`
3. 如果测试请求成功,断路器将在 `reset_timeout` 毫秒后自动关闭
4. 要强制重置,请重启代理
### 速率限制超出
**症状**:在请求到达上游服务之前,请求返回速率限制错误。
**原因**:在 `window` 毫秒内,请求次数超过了 `max_requests` 次。
**解决方法**:
1. 在配置文件中增加 `max_requests` 或 `window`
2. 如果使用 `per_user` 键控,请检查单个用户是否占比过大
3. 对于分布式部署,请确保 Redis 已连接 — 如果没有 Redis,每个实例都会独立跟踪限制
### Redis 连接失败
**症状**:启动时关于 Redis 连接的警告,或速率限制行为不一致。
**原因**:Redis 无法访问或凭证错误。
**解决方法**:
1. 验证 Redis 是否正在运行:`redis-cli ping`
2. 在 `.env` 中检查 `REDIS_HOST`、`REDIS_PORT`、`REDIS_PASSWORD`
3. 如果 Redis 无法访问,代理将恢复为内存速率限制 — 这对于单实例部署是安全的
### 配置验证错误
**症状**:服务器因 Zod 验证错误而无法启动。
**解决方法**:
1. 运行 `npm run cli -- validate -c ./config/services.yaml` 以获取详细错误输出
2. 常见问题:缺少 `base_url`、无效 `method`(必须是大写 GET/POST/PUT/DELETE/PATCH)、`type` 必须是 string/number/boolean/object/array 之一
### 上游服务身份验证失败
**症状**:上游服务返回 401/403,尽管已配置身份验证。
**解决方法**:
1. 验证 `token_env` / `key_env` 中指定的环境变量是否设置:`echo $USER_SERVICE_TOKEN`
2. 对于 OAuth2,请检查 `token_url` 是否可达且客户端凭证是否有效
3. 启用调试日志以查看传出的身份验证标头(令牌被遮蔽):`LOG_LEVEL=debug`
### stdio 传输无法连接
**症状**:Claude Desktop 显示 MCP 服务器已断开连接。
**解决方法**:
1. 确保项目已构建:`npm run build`
2. 验证 `claude_desktop_config.json` 中的路径指向 `dist/server/unified-server.js`
3. 手动测试:`echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"test","version":"1.0.0"}}}' | node dist/server/unified-server.js`
4. 检查是否有其他输出(console.log、调试打印)污染 stdout — stdio 传输需要干净的 JSON-RPC stdin/stdout
### 请求超时
**症状**:工具调用在配置的超时时间后失败,提示“请求超时”。
**解决方法**:
1. 在服务级别或工具级别增加 `timeout`
2. 如果启用了断路器,请确保 `circuit_breaker.timeout` >= 服务 `timeout`
3. 检查到上游服务的网络连接
## 脚本参考
| 脚本 | 描述 |
|--------|-------------|
| `npm run build` | 编译 TypeScript 到 `dist/` |
| `npm run dev` | 开发模式,热重载 |
| `npm run start` | 启动服务器(旧入口点) |
| `npm run start:unified` | 启动统一服务器(自动检测传输) |
| `npm run start:stdio` | 启动 stdio 传输 |
| `npm run start:http` | 启动 HTTP 传输,端口 3000 |
| `npm run test` | 运行单元测试 |
| `npm run test:all` | 运行单元测试 + 端到端测试 |
| `npm run test:e2e` | 运行端到端测试(stdio + 取消) |
| `npm run test:http` | 运行 HTTP 传输端到端测试 |
| `npm run test:http:auth` | 运行 HTTP 身份验证端到端测试 |
| `npm run test:coverage` | 单元测试,生成覆盖率报告 |
| `npm run cli -- <cmd>` | 运行 CLI 命令(生成、验证、服务) |
| `npm run typecheck` | TypeScript 类型检查 |
| `npm run lint` | ESLint |
| `npm run lint:fix` | ESLint 自动修复 |
| `npm run format` | Prettier 格式化 |
## 技术栈
| 组件 | 技术 |
|-----------|------------|
| 运行时 | Node.js >= 18 |
| 语言 | TypeScript 5.x |
| MCP SDK | `@modelcontextprotocol/sdk` |
| HTTP 客户端 | Axios |
| 断路器 | Opossum |
| 速率限制 | rate-limiter-flexible + 可选 Redis(ioredis) |
| 验证 | Zod |
| 日志记录 | Winston(结构化 JSON) |
| 指标 | prom-client(Prometheus) |
| HTTP 服务器 | Express |
| OpenAPI 解析 | @apidevtools/swagger-parser |
| CLI | Commander |
| 测试 | Jest + ts-jest(ESM) |
| 容器化 | Docker(多阶段,node:20-alpine) |
## 相关文档
- [架构概述](./ARCHITECTURE.md) — 系统设计、请求流程图、组件详情
- [测试指南](./docs/TEST_GUIDE.md) — 端到端测试演练
## 贡献
1. 分叉仓库
2. 创建功能分支:`git checkout -b feature/my-feature`
3. 进行更改并添加测试
4. 运行完整测试套件:`npm run test:all`
5. 运行类型检查:`npm run typecheck`
6. 运行 linting:`npm run lint`
7. 提交并附上描述信息
8. 打开拉取请求
请确保所有测试通过且类型检查成功后再提交。
## 许可证
MIT
MCP 配置
以下是此 MCP Server 的配置信息,可直接复制到 Cursor 或其他 MCP 客户端中使用。
mcp.json
连接信息
猜你喜欢
暂无推荐服务器