提交qiming-mcp-proxy

This commit is contained in:
Codex
2026-06-01 13:03:20 +08:00
parent 9e9486b7c2
commit afb3d9f4e6
394 changed files with 124494 additions and 0 deletions

View File

@@ -0,0 +1,98 @@
[package]
name = "mcp-stdio-proxy"
# 主入口版本,与 workspace 对外发布版本一致
version = "0.1.68"
edition = "2024"
authors = ["nuwax-ai"]
description = "MCP (Model Context Protocol) proxy server and CLI tool for protocol conversion and remote service access"
license = "MIT OR Apache-2.0"
repository = "https://github.com/nuwax-ai/mcp-proxy"
keywords = ["mcp", "proxy", "protocol", "sse", "cli"]
categories = ["web-programming", "command-line-utilities", "network-programming"]
readme = "README.md"
exclude = [
"logs/",
"tmp/",
"*.log",
"target/",
"benches/",
]
[features]
default = []
[dependencies]
# 共享类型和工具
mcp-common = { version = "0.1.28", path = "../mcp-common", features = ["otlp"] }
# 国际化支持
rust-i18n = { workspace = true }
# 新增:协议特定的代理库
mcp-streamable-proxy = { version = "0.1.28", path = "../mcp-streamable-proxy" }
mcp-sse-proxy = { version = "0.1.28", path = "../mcp-sse-proxy" }
axum = { workspace = true }
tower = { workspace = true }
tower-http = { workspace = true }
tokio = { workspace = true, features = ["macros", "net", "rt", "rt-multi-thread", "signal", "io-util"] }
tokio-util = { workspace = true }
tracing = { workspace = true }
tracing-subscriber = { workspace = true }
tracing-appender = { workspace = true }
tracing-opentelemetry = { workspace = true }
opentelemetry = { workspace = true }
opentelemetry-jaeger = { workspace = true }
opentelemetry-semantic-conventions = { workspace = true }
opentelemetry_sdk = { workspace = true }
hostname = { workspace = true }
log = { workspace = true }
anyhow = { workspace = true }
thiserror = { workspace = true }
once_cell = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
serde_yaml = { workspace = true }
reqwest = { workspace = true }
http = { workspace = true }
clap = { workspace = true }
uuid = { workspace = true }
dashmap = { workspace = true }
arc-swap = { workspace = true }
futures = { workspace = true }
chrono = { workspace = true }
tokio-stream = { workspace = true }
backtrace = { workspace = true }
tracing-futures = { workspace = true }
rand = { workspace = true }
# 执行js/ts/python代码,通过 uv/deno 命令方式执行
run_code_rmcp = { workspace = true }
urlencoding = { workspace = true }
base64 = { workspace = true }
async-trait.workspace = true
rustls = { version = "0.23", default-features = false, features = ["ring"] }
moka = { workspace = true, features = ["future"] }
# 进程组管理(跨平台子进程清理)
# 添加 process-group 特性用于 Unix 平台的进程组管理
# 添加 job-object 特性用于 Windows 平台的进程树管理
process-wrap = { version = "9.0", features = ["tokio1", "process-group", "job-object"] }
[target.'cfg(windows)'.dependencies]
windows = { version = "0.62", features = ["Win32_System_Threading"] }
[[bin]]
name = "mcp-proxy"
path = "src/main.rs"
[dev-dependencies]
criterion = "0.6"
env_logger = "0.11"
futures-util = "0.3.31"
[[bench]]
name = "run_code_bench"
harness = false
[[bench]]
name = "run_code_advanced_bench"
harness = false

View File

@@ -0,0 +1,22 @@
MIT License
Copyright (c) 2024 nuwax-ai
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1,388 @@
# mcp-stdio-proxy
**[English](README.md)** | **[简体中文](README_zh-CN.md)**
---
# mcp-stdio-proxy
MCP (Model Context Protocol) client proxy tool that converts remote MCP services (SSE/Streamable HTTP) to local stdio interface.
> **Package Name**: `mcp-stdio-proxy`
> **Command Name**: `mcp-proxy` (shorter)
## Core Features
`mcp-proxy` is a lightweight client proxy tool that solves one core problem:
**Enabling stdio-only MCP clients to access remote SSE/HTTP MCP services.**
### How It Works
```
Remote MCP Service (SSE/HTTP) ←→ mcp-proxy ←→ Local Application (stdio)
```
- **Input**: Remote MCP service URL (supports SSE or Streamable HTTP protocols)
- **Output**: Local stdio interface (standard input/output)
- **Purpose**: Protocol conversion + transparent proxy
## Features
- 🔄 **Protocol Conversion**: Auto-detect and convert SSE/Streamable HTTP → stdio
- 🌐 **Remote Access**: Enable local applications to access remote MCP services
- 🔍 **Auto Protocol Detection**: Intelligently identify service protocol types
- 🔐 **Authentication Support**: Custom Authorization header and HTTP headers
-**Lightweight & Efficient**: No extra configuration needed, works out of the box
## Installation
### Install from crates.io (Recommended)
```bash
cargo install mcp-stdio-proxy
```
### Build from Source
```bash
git clone https://github.com/nuwax-ai/mcp-proxy.git
cd mcp-proxy/mcp-proxy
cargo build --release
# Binary located at: target/release/mcp-proxy
```
## Quick Start
### Basic Usage
```bash
# Convert remote SSE service to stdio
mcp-proxy convert https://example.com/mcp/sse
# Or use simplified syntax (backward compatible)
mcp-proxy https://example.com/mcp/sse
```
### Complete Example with Authentication
```bash
# Use Bearer token authentication
mcp-proxy convert https://api.example.com/mcp/sse \
--auth "Bearer your-api-token"
# Add custom headers
mcp-proxy convert https://api.example.com/mcp/sse \
-H "Authorization=Bearer token" \
-H "X-Custom-Header=value"
```
### Use with MCP Clients
```bash
# Pipe mcp-proxy output to your MCP client
mcp-proxy convert https://remote-server.com/mcp \
--auth "Bearer token" | \
your-mcp-client
# Or use in MCP client configuration
# Example (Claude Desktop configuration):
{
"mcpServers": {
"remote-service": {
"command": "mcp-proxy",
"args": [
"convert",
"https://remote-server.com/mcp/sse",
"--auth",
"Bearer your-token"
]
}
}
}
```
## Command Details
### 1. `convert` - Protocol Conversion (Core Command)
Convert remote MCP service to local stdio interface.
```bash
mcp-proxy convert <URL> [options]
```
**Options:**
| Option | Short | Description | Default |
|--------|-------|-------------|---------|
| `--auth <TOKEN>` | `-a` | Authentication header (e.g., "Bearer token") | - |
| `--header <KEY=VALUE>` | `-H` | Custom HTTP headers (can be used multiple times) | - |
| `--timeout <SECONDS>` | - | Connection timeout in seconds | 30 |
| `--retries <NUM>` | - | Number of retries | 3 |
| `--verbose` | `-v` | Verbose output (show debug info) | false |
| `--quiet` | `-q` | Quiet mode (errors only) | false |
**Examples:**
```bash
# Basic conversion
mcp-proxy convert https://api.example.com/mcp/sse
# With authentication and timeout
mcp-proxy convert https://api.example.com/mcp/sse \
--auth "Bearer sk-1234567890" \
--timeout 60 \
--retries 5
# Add multiple custom headers
mcp-proxy convert https://api.example.com/mcp \
-H "Authorization=Bearer token" \
-H "X-API-Key=your-key" \
-H "X-Request-ID=abc123"
# Verbose mode (view connection process)
mcp-proxy convert https://api.example.com/mcp/sse --verbose
```
### 2. `check` - Service Status Check
Check if remote MCP service is available, verify connectivity and protocol support.
```bash
mcp-proxy check <URL> [options]
```
**Options:**
| Option | Short | Description | Default |
|--------|-------|-------------|---------|
| `--auth <TOKEN>` | `-a` | Authentication header | - |
| `--timeout <SECONDS>` | - | Timeout in seconds | 10 |
**Examples:**
```bash
# Check service status
mcp-proxy check https://api.example.com/mcp/sse
# With authentication
mcp-proxy check https://api.example.com/mcp/sse \
--auth "Bearer token" \
--timeout 5
```
**Exit Codes:**
- `0`: Service is healthy
- `Non-zero`: Service unavailable or check failed
### 3. `detect` - Protocol Detection
Automatically detect the protocol type used by remote MCP service.
```bash
mcp-proxy detect <URL> [options]
```
**Options:**
| Option | Short | Description |
|--------|-------|-------------|
| `--auth <TOKEN>` | `-a` | Authentication header |
| `--quiet` | `-q` | Quiet mode (output protocol type only) |
**Output:**
- `SSE` - Server-Sent Events protocol
- `Streamable HTTP` - Streamable HTTP protocol
- `Stdio` - Standard input/output protocol (not applicable for remote services)
**Examples:**
```bash
# Detect protocol type
mcp-proxy detect https://api.example.com/mcp/sse
# Use in scripts
PROTOCOL=$(mcp-proxy detect https://api.example.com/mcp --quiet)
if [ "$PROTOCOL" = "SSE" ]; then
echo "Detected SSE protocol"
fi
```
## Use Cases
### Case 1: Claude Desktop with Remote MCP Service
Claude Desktop only supports stdio protocol MCP services. Use `mcp-proxy` to access remote services.
**Configuration Example** (`~/Library/Application Support/Claude/config.json`):
```json
{
"mcpServers": {
"remote-database": {
"command": "mcp-proxy",
"args": [
"convert",
"https://your-server.com/mcp/database",
"--auth",
"Bearer your-token-here"
]
},
"remote-search": {
"command": "mcp-proxy",
"args": ["https://search-api.com/mcp/sse"]
}
}
}
```
### Case 2: Health Check in CI/CD Pipeline
```bash
#!/bin/bash
# Check MCP service status before deployment
echo "Checking MCP service..."
if mcp-proxy check https://api.example.com/mcp --timeout 5; then
echo "✅ MCP service is healthy, continuing deployment"
# Execute deployment script
./deploy.sh
else
echo "❌ MCP service unavailable, aborting deployment"
exit 1
fi
```
### Case 3: Cross-Network Enterprise Internal MCP Service
```bash
# Access internal MCP service via VPN or jump host
mcp-proxy convert https://internal-mcp.company.com/api/sse \
--auth "Bearer ${MCP_TOKEN}" \
--timeout 120 | \
local-mcp-client
```
### Case 4: Development and Testing
```bash
# Quick test remote MCP service
mcp-proxy convert https://test-api.com/mcp/sse --verbose
# View detailed connection and communication logs
RUST_LOG=debug mcp-proxy convert https://api.com/mcp/sse -v
```
## Supported Protocols
`mcp-proxy` can connect to remote MCP services using the following protocols:
| Protocol | Description | Status |
|----------|-------------|--------|
| **SSE** | Server-Sent Events, unidirectional real-time push | ✅ Fully Supported |
| **Streamable HTTP** | Bidirectional streaming HTTP communication | ✅ Fully Supported |
**Output Protocol**: Always **stdio** (standard input/output)
## Environment Variables
| Variable | Description | Example |
|----------|-------------|---------|
| `RUST_LOG` | Log level | `RUST_LOG=debug mcp-proxy convert ...` |
| `HTTP_PROXY` | HTTP proxy | `HTTP_PROXY=http://proxy:8080` |
| `HTTPS_PROXY` | HTTPS proxy | `HTTPS_PROXY=http://proxy:8080` |
## FAQ
### Q: Why do I need mcp-proxy?
**A:** Many MCP clients (like Claude Desktop) only support local stdio protocol services. If your MCP service is deployed on a remote server using SSE or HTTP protocols, you need `mcp-proxy` as a protocol conversion bridge.
### Q: What's the difference between mcp-proxy and MCP server?
**A:**
- **MCP Server**: Backend service that provides specific functionality (database access, file operations, etc.)
- **mcp-proxy**: Pure client proxy tool that only does protocol conversion, provides no business functionality
### Q: Does it support bidirectional communication?
**A:** Yes! Whether using SSE or Streamable HTTP protocol, `mcp-proxy` supports full bidirectional communication (request/response).
### Q: How to debug connection issues?
**A:** Use `--verbose` option and `RUST_LOG` environment variable:
```bash
RUST_LOG=debug mcp-proxy convert https://api.com/mcp --verbose
```
### Q: Does it support self-signed SSL certificates?
**A:** Current version uses system default certificate verification. For self-signed certificate support, please submit an Issue.
## Troubleshooting
### Connection Timeout
```bash
# Increase timeout
mcp-proxy convert https://slow-api.com/mcp --timeout 120
```
### Authentication Failed
```bash
# Check token format, ensure "Bearer " prefix
mcp-proxy convert https://api.com/mcp --auth "Bearer your-token-here"
# Or use custom header
mcp-proxy convert https://api.com/mcp -H "Authorization=Bearer your-token"
```
### Protocol Detection Failed
```bash
# View detailed error message
mcp-proxy detect https://api.com/mcp --verbose
# Check service status
mcp-proxy check https://api.com/mcp
```
## System Requirements
- **Operating System**: Linux, macOS, Windows
- **Rust Version**: 1.70+ (only required for building from source)
- **Network**: Ability to access target MCP service
## License
This project is dual-licensed under MIT or Apache-2.0.
## Contributing
Issues and Pull Requests are welcome!
- **GitHub Repository**: https://github.com/nuwax-ai/mcp-proxy
- **Issue Tracker**: https://github.com/nuwax-ai/mcp-proxy/issues
- **Feature Discussions**: https://github.com/nuwax-ai/mcp-proxy/discussions
## Related Resources
- [MCP Official Documentation](https://modelcontextprotocol.io/)
- [rmcp - Rust MCP Implementation](https://crates.io/crates/rmcp)
- [MCP Servers List](https://github.com/modelcontextprotocol/servers)
## Changelog
### v0.1.18
- ✅ SSE and Streamable HTTP protocol conversion support
- ✅ Auto protocol detection
- ✅ Authentication and custom headers support
- ✅ Service status check command
- ✅ Protocol detection command
- ✅ OpenTelemetry integration with OTLP
- ✅ Background health checks
- ✅ Run code execution via external processes

View File

@@ -0,0 +1,388 @@
# mcp-stdio-proxy
**[English](README.md)** | **[简体中文](README_zh-CN.md)**
---
# mcp-stdio-proxy
MCP (Model Context Protocol) 客户端代理工具,将远程 MCP 服务SSE/Streamable HTTP转换为本地 stdio 接口。
> **包名**`mcp-stdio-proxy`
> **命令名**`mcp-proxy`(更简短)
## 核心功能
`mcp-proxy` 是一个轻量级的客户端代理工具,解决了一个核心问题:
**让只支持 stdio 协议的 MCP 客户端能够访问远程的 SSE/HTTP MCP 服务。**
### 工作原理
```
远程 MCP 服务 (SSE/HTTP) ←→ mcp-proxy ←→ 本地应用 (stdio)
```
- **输入**:远程 MCP 服务 URL支持 SSE 或 Streamable HTTP 协议)
- **输出**:本地 stdio 接口(标准输入/输出)
- **作用**:协议转换 + 透明代理
## 功能特性
- 🔄 **协议转换**:自动检测并转换 SSE/Streamable HTTP → stdio
- 🌐 **远程访问**:让本地应用能够访问远程 MCP 服务
- 🔍 **自动协议检测**:智能识别服务端协议类型
- 🔐 **认证支持**:支持自定义 Authorization header 和其他 HTTP headers
-**轻量高效**:无需额外配置,开箱即用
## 安装
### 从 crates.io 安装(推荐)
```bash
cargo install mcp-stdio-proxy
```
### 从源码构建
```bash
git clone https://github.com/nuwax-ai/mcp-proxy.git
cd mcp-proxy/mcp-proxy
cargo build --release
# 二进制文件位于: target/release/mcp-proxy
```
## 快速开始
### 基本用法
```bash
# 将远程 SSE 服务转换为 stdio
mcp-proxy convert https://example.com/mcp/sse
# 或使用简化语法(向后兼容)
mcp-proxy https://example.com/mcp/sse
```
### 带认证的完整示例
```bash
# 使用 Bearer token 认证
mcp-proxy convert https://api.example.com/mcp/sse \
--auth "Bearer your-api-token"
# 添加自定义 headers
mcp-proxy convert https://api.example.com/mcp/sse \
-H "Authorization=Bearer token" \
-H "X-Custom-Header=value"
```
### 配合 MCP 客户端使用
```bash
# 将 mcp-proxy 输出管道到你的 MCP 客户端
mcp-proxy convert https://remote-server.com/mcp \
--auth "Bearer token" | \
your-mcp-client
# 或在 MCP 客户端配置中使用
# 配置文件示例(如 Claude Desktop 配置):
{
"mcpServers": {
"remote-service": {
"command": "mcp-proxy",
"args": [
"convert",
"https://remote-server.com/mcp/sse",
"--auth",
"Bearer your-token"
]
}
}
}
```
## 命令详解
### 1. `convert` - 协议转换(核心命令)
将远程 MCP 服务转换为本地 stdio 接口。
```bash
mcp-proxy convert <URL> [选项]
```
**选项:**
| 选项 | 简写 | 说明 | 默认值 |
|------|------|------|--------|
| `--auth <TOKEN>` | `-a` | 认证 header如: "Bearer token" | - |
| `--header <KEY=VALUE>` | `-H` | 自定义 HTTP headers可多次使用 | - |
| `--timeout <SECONDS>` | - | 连接超时时间(秒) | 30 |
| `--retries <NUM>` | - | 重试次数 | 3 |
| `--verbose` | `-v` | 详细输出(显示调试信息) | false |
| `--quiet` | `-q` | 静默模式(只输出错误) | false |
**示例:**
```bash
# 基本转换
mcp-proxy convert https://api.example.com/mcp/sse
# 带认证和超时设置
mcp-proxy convert https://api.example.com/mcp/sse \
--auth "Bearer sk-1234567890" \
--timeout 60 \
--retries 5
# 添加多个自定义 headers
mcp-proxy convert https://api.example.com/mcp \
-H "Authorization=Bearer token" \
-H "X-API-Key=your-key" \
-H "X-Request-ID=abc123"
# 详细模式(查看连接过程)
mcp-proxy convert https://api.example.com/mcp/sse --verbose
```
### 2. `check` - 服务状态检查
检查远程 MCP 服务是否可用,验证连接性和协议支持。
```bash
mcp-proxy check <URL> [选项]
```
**选项:**
| 选项 | 简写 | 说明 | 默认值 |
|------|------|------|--------|
| `--auth <TOKEN>` | `-a` | 认证 header | - |
| `--timeout <SECONDS>` | - | 超时时间(秒) | 10 |
**示例:**
```bash
# 检查服务状态
mcp-proxy check https://api.example.com/mcp/sse
# 带认证检查
mcp-proxy check https://api.example.com/mcp/sse \
--auth "Bearer token" \
--timeout 5
```
**退出码:**
- `0`:服务正常
- `非 0`:服务不可用或检查失败
### 3. `detect` - 协议检测
自动检测远程 MCP 服务使用的协议类型。
```bash
mcp-proxy detect <URL> [选项]
```
**选项:**
| 选项 | 简写 | 说明 |
|------|------|------|
| `--auth <TOKEN>` | `-a` | 认证 header |
| `--quiet` | `-q` | 静默模式(只输出协议类型) |
**输出:**
- `SSE` - Server-Sent Events 协议
- `Streamable HTTP` - Streamable HTTP 协议
- `Stdio` - 标准输入输出协议(不适用于远程服务)
**示例:**
```bash
# 检测协议类型
mcp-proxy detect https://api.example.com/mcp/sse
# 在脚本中使用
PROTOCOL=$(mcp-proxy detect https://api.example.com/mcp --quiet)
if [ "$PROTOCOL" = "SSE" ]; then
echo "检测到 SSE 协议"
fi
```
## 使用场景
### 场景 1Claude Desktop 集成远程 MCP 服务
Claude Desktop 只支持 stdio 协议的 MCP 服务,使用 `mcp-proxy` 可以让它访问远程服务。
**配置文件示例** (`~/Library/Application Support/Claude/config.json`)
```json
{
"mcpServers": {
"remote-database": {
"command": "mcp-proxy",
"args": [
"convert",
"https://your-server.com/mcp/database",
"--auth",
"Bearer your-token-here"
]
},
"remote-search": {
"command": "mcp-proxy",
"args": ["https://search-api.com/mcp/sse"]
}
}
}
```
### 场景 2CI/CD 流水线中的健康检查
```bash
#!/bin/bash
# 部署前检查 MCP 服务状态
echo "检查 MCP 服务..."
if mcp-proxy check https://api.example.com/mcp --timeout 5; then
echo "✅ MCP 服务正常,继续部署"
# 执行部署脚本
./deploy.sh
else
echo "❌ MCP 服务不可用,中止部署"
exit 1
fi
```
### 场景 3跨网络访问企业内部 MCP 服务
```bash
# 通过 VPN 或跳板机访问内网 MCP 服务
mcp-proxy convert https://internal-mcp.company.com/api/sse \
--auth "Bearer ${MCP_TOKEN}" \
--timeout 120 | \
local-mcp-client
```
### 场景 4开发测试
```bash
# 快速测试远程 MCP 服务
mcp-proxy convert https://test-api.com/mcp/sse --verbose
# 查看详细的连接和通信日志
RUST_LOG=debug mcp-proxy convert https://api.com/mcp/sse -v
```
## 支持的协议
`mcp-proxy` 可以连接以下协议的远程 MCP 服务:
| 协议 | 说明 | 状态 |
|------|------|------|
| **SSE** | Server-Sent Events单向实时推送 | ✅ 完全支持 |
| **Streamable HTTP** | 双向流式 HTTP 通信 | ✅ 完全支持 |
**输出协议**:始终是 **stdio**(标准输入/输出)
## 环境变量
| 变量 | 说明 | 示例 |
|------|------|------|
| `RUST_LOG` | 日志级别 | `RUST_LOG=debug mcp-proxy convert ...` |
| `HTTP_PROXY` | HTTP 代理 | `HTTP_PROXY=http://proxy:8080` |
| `HTTPS_PROXY` | HTTPS 代理 | `HTTPS_PROXY=http://proxy:8080` |
## 常见问题
### Q: 为什么需要 mcp-proxy
**A:** 许多 MCP 客户端(如 Claude Desktop只支持本地 stdio 协议的服务。如果你的 MCP 服务部署在远程服务器上使用 SSE 或 HTTP 协议,就需要 `mcp-proxy` 作为协议转换桥梁。
### Q: mcp-proxy 和 MCP 服务器有什么区别?
**A:**
- **MCP 服务器**:提供具体功能(数据库访问、文件操作等)的后端服务
- **mcp-proxy**:纯粹的客户端代理工具,只做协议转换,不提供任何业务功能
### Q: 支持双向通信吗?
**A:** 是的!无论是 SSE 还是 Streamable HTTP 协议,`mcp-proxy` 都支持完整的双向通信(请求/响应)。
### Q: 如何调试连接问题?
**A:** 使用 `--verbose` 选项和 `RUST_LOG` 环境变量:
```bash
RUST_LOG=debug mcp-proxy convert https://api.com/mcp --verbose
```
### Q: 支持自签名 SSL 证书吗?
**A:** 当前版本使用系统默认的证书验证。如需支持自签名证书,请提交 Issue。
## 故障排除
### 连接超时
```bash
# 增加超时时间
mcp-proxy convert https://slow-api.com/mcp --timeout 120
```
### 认证失败
```bash
# 检查 token 格式,确保包含 "Bearer " 前缀
mcp-proxy convert https://api.com/mcp --auth "Bearer your-token-here"
# 或使用自定义 header
mcp-proxy convert https://api.com/mcp -H "Authorization=Bearer your-token"
```
### 协议检测失败
```bash
# 查看详细错误信息
mcp-proxy detect https://api.com/mcp --verbose
# 检查服务状态
mcp-proxy check https://api.com/mcp
```
## 系统要求
- **操作系统**Linux, macOS, Windows
- **Rust 版本**1.70+ (仅从源码构建时需要)
- **网络**:能够访问目标 MCP 服务
## 许可证
本项目采用 MIT 或 Apache-2.0 双许可证。
## 贡献
欢迎提交 Issue 和 Pull Request
- **GitHub 仓库**https://github.com/nuwax-ai/mcp-proxy
- **问题反馈**https://github.com/nuwax-ai/mcp-proxy/issues
- **功能建议**https://github.com/nuwax-ai/mcp-proxy/discussions
## 相关资源
- [MCP 官方文档](https://modelcontextprotocol.io/)
- [rmcp - Rust MCP 实现](https://crates.io/crates/rmcp)
- [MCP 服务器列表](https://github.com/modelcontextprotocol/servers)
## 更新日志
### v0.1.18
- ✅ 支持 SSE 和 Streamable HTTP 协议转换
- ✅ 自动协议检测
- ✅ 认证和自定义 headers 支持
- ✅ 服务状态检查命令
- ✅ 协议检测命令
- ✅ OpenTelemetry 集成,支持 OTLP
- ✅ 后台健康检查
- ✅ 通过外部进程执行代码

View File

@@ -0,0 +1,66 @@
# 性能基准测试
这个目录包含使用 Criterion.rs 对 `/api/run_code_with_log` 端点进行性能测试的代码。
## 可用的基准测试
1. `run_code_bench` - 基本性能测试测试三种语言JS、TS、Python的简单脚本执行性能
2. `run_code_advanced_bench` - 高级性能测试,测试多种场景、不同复杂度的脚本和参数组合
## 如何运行测试
使用以下命令运行基本测试:
```bash
cargo bench --bench run_code_bench
```
使用以下命令运行高级测试:
```bash
cargo bench --bench run_code_advanced_bench
```
运行特定的测试场景:
```bash
cargo bench --bench run_code_advanced_bench -- js_basic
```
## 查看测试结果
测试结果将保存在 `target/criterion` 目录下,你可以在浏览器中打开 HTML 报告查看详细的结果:
```bash
open target/criterion/report/index.html
```
## 配置测试参数
在基准测试文件中可以修改以下参数,以适应不同性能的机器:
- `sample_size`: 每个测试场景运行的样本数量默认为10
- `warm_up_time`: 预热时间默认为20秒
- `measurement_time`: 测量时间默认为10秒
- `significance_level`: 统计显著性水平默认为0.05即5%
如果在较慢的机器上运行,可能需要进一步减少样本数或增加测量时间。可以通过命令行参数指定:
```bash
# 使用命令行参数减少样本数
cargo bench --bench run_code_bench -- --sample-size 5
# 使用命令行参数增加预热时间
cargo bench --bench run_code_bench -- --warm-up-time 30
# 使用命令行参数增加测量时间
cargo bench --bench run_code_bench -- --measurement-time 15
```
## 测试脚本说明
测试使用了 `fixtures` 目录下的各种测试脚本:
- `test_js.js``test_ts.ts``test_python_simple.py` - 基本语言测试脚本
- `test_js_params.js``test_ts_params.ts``test_python_params.py` - 带参数的测试脚本
- `import_lodash_example.js``test_python_logging.py` - 复杂功能测试脚本

View File

@@ -0,0 +1,195 @@
use criterion::{BenchmarkId, Criterion, Throughput, criterion_group, criterion_main};
use mcp_stdio_proxy::{RunCodeMessageRequest, run_code_handler};
use serde_json::json;
use std::{collections::HashMap, fs};
use tokio::runtime::Runtime;
use uuid::Uuid;
// 测试脚本类型
#[derive(Debug, Clone, Copy)]
enum ScriptType {
Js,
Ts,
Python,
}
// 测试场景
#[allow(dead_code)]
struct TestScenario {
name: &'static str,
file_path: &'static str,
script_type: ScriptType,
description: &'static str,
}
// 获取所有测试场景
fn get_test_scenarios() -> Vec<TestScenario> {
vec![
// 基本测试场景
TestScenario {
name: "js_basic",
file_path: "fixtures/test_js.js",
script_type: ScriptType::Js,
description: "基本JavaScript执行",
},
TestScenario {
name: "ts_basic",
file_path: "fixtures/test_ts.ts",
script_type: ScriptType::Ts,
description: "基本TypeScript执行",
},
TestScenario {
name: "python_basic",
file_path: "fixtures/test_python_simple.py",
script_type: ScriptType::Python,
description: "基本Python执行",
},
// 参数传递测试场景
TestScenario {
name: "js_params",
file_path: "fixtures/test_js_params.js",
script_type: ScriptType::Js,
description: "带参数的JavaScript执行",
},
TestScenario {
name: "ts_params",
file_path: "fixtures/test_ts_params.ts",
script_type: ScriptType::Ts,
description: "带参数的TypeScript执行",
},
TestScenario {
name: "python_params",
file_path: "fixtures/test_python_params.py",
script_type: ScriptType::Python,
description: "带参数的Python执行",
},
// 复杂测试场景
TestScenario {
name: "js_import",
file_path: "fixtures/import_lodash_example.js",
script_type: ScriptType::Js,
description: "导入lodash的JavaScript执行",
},
TestScenario {
name: "python_logging",
file_path: "fixtures/test_python_logging.py",
script_type: ScriptType::Python,
description: "使用logging的Python执行",
},
TestScenario {
name: "ts_complex",
file_path: "fixtures/test_ts.ts",
script_type: ScriptType::Ts,
description: "复杂TypeScript执行",
},
]
}
// 读取测试脚本文件
fn read_script_file(file_path: &str) -> String {
fs::read_to_string(file_path).unwrap_or_else(|_| panic!("无法读取文件: {file_path}"))
}
// 创建运行代码请求
fn create_run_code_request(
code: &str,
script_type: ScriptType,
with_complex_params: bool,
) -> RunCodeMessageRequest {
let mut json_param = HashMap::new();
if with_complex_params {
// 创建复杂参数
json_param.insert("input".to_string(), json!("测试输入"));
json_param.insert("number".to_string(), json!(42));
json_param.insert("boolean".to_string(), json!(true));
json_param.insert("array".to_string(), json!([1, 2, 3, 4, 5]));
json_param.insert(
"object".to_string(),
json!({
"name": "测试对象",
"properties": {
"a": 1,
"b": "string",
"c": [true, false]
}
}),
);
} else {
// 创建简单参数
json_param.insert("input".to_string(), json!("测试输入"));
}
RunCodeMessageRequest {
json_param,
code: code.to_string(),
uid: Uuid::new_v4().to_string(),
engine_type: match script_type {
ScriptType::Js => "js".to_string(),
ScriptType::Ts => "ts".to_string(),
ScriptType::Python => "python".to_string(),
},
}
}
// 设置基准测试
fn bench_run_code_advanced(c: &mut Criterion) {
// 创建测试组
let mut group = c.benchmark_group("run_code_handler_advanced");
// 设置采样大小和预热次数
group.sample_size(10); // 减少到10次取平均值
group.warm_up_time(std::time::Duration::from_secs(20)); // 20秒预热时间
group.measurement_time(std::time::Duration::from_secs(10)); // 10秒测量时间
// 获取所有测试场景
let scenarios = get_test_scenarios();
// 创建tokio运行时
let rt = Runtime::new().unwrap();
// 遍历测试场景
for scenario in scenarios {
// 读取脚本内容
let code = read_script_file(scenario.file_path);
// 设置吞吐量计数为脚本字节大小
// 这样可以比较不同大小脚本的执行效率
group.throughput(Throughput::Bytes(code.len() as u64));
// 测试使用简单参数
group.bench_function(
BenchmarkId::new(format!("{}_simple", scenario.name), "simple_params"),
|b| {
b.iter(|| {
let req = create_run_code_request(&code, scenario.script_type, false);
rt.block_on(async { run_code_handler(axum::Json(req)).await.unwrap() })
});
},
);
// 测试使用复杂参数
group.bench_function(
BenchmarkId::new(format!("{}_complex", scenario.name), "complex_params"),
|b| {
b.iter(|| {
let req = create_run_code_request(&code, scenario.script_type, true);
rt.block_on(async { run_code_handler(axum::Json(req)).await.unwrap() })
});
},
);
}
group.finish();
}
criterion_group!(
name = benches;
config = Criterion::default()
.significance_level(0.05) // 设置显著性水平为5%
.sample_size(10) // 设置样本大小为10
.warm_up_time(std::time::Duration::from_secs(20)) // 20秒预热时间
.measurement_time(std::time::Duration::from_secs(10)); // 10秒测量时间
targets = bench_run_code_advanced
);
criterion_main!(benches);

View File

@@ -0,0 +1,90 @@
use criterion::{BenchmarkId, Criterion, criterion_group, criterion_main};
use mcp_stdio_proxy::{RunCodeMessageRequest, run_code_handler};
use serde_json::json;
use std::{collections::HashMap, fs};
use tokio::runtime::Runtime;
use uuid::Uuid;
// 测试脚本类型枚举
enum ScriptType {
Js,
Ts,
Python,
}
// 读取测试脚本文件
fn read_script_file(file_path: &str) -> String {
fs::read_to_string(file_path).unwrap_or_else(|_| panic!("无法读取文件: {file_path}"))
}
// 创建运行代码请求
fn create_run_code_request(code: &str, script_type: ScriptType) -> RunCodeMessageRequest {
let mut json_param = HashMap::new();
json_param.insert("input".to_string(), json!("测试输入"));
RunCodeMessageRequest {
json_param,
code: code.to_string(),
uid: Uuid::new_v4().to_string(),
engine_type: match script_type {
ScriptType::Js => "js".to_string(),
ScriptType::Ts => "ts".to_string(),
ScriptType::Python => "python".to_string(),
},
}
}
// 设置基准测试
fn bench_run_code(c: &mut Criterion) {
// 创建测试组
let mut group = c.benchmark_group("run_code_handler");
// 设置采样配置
group.sample_size(10); // 减少样本数量
group.warm_up_time(std::time::Duration::from_secs(20)); // 增加预热时间为20秒
group.measurement_time(std::time::Duration::from_secs(10)); // 增加测量时间
// 加载测试脚本
let js_code = read_script_file("fixtures/test_js.js");
let ts_code = read_script_file("fixtures/test_ts.ts");
let py_code = read_script_file("fixtures/test_python_simple.py");
// 创建运行时
let rt = Runtime::new().unwrap();
// 测试JS代码执行性能
group.bench_function(BenchmarkId::new("javascript", "test_js.js"), |b| {
b.iter(|| {
let req = create_run_code_request(&js_code, ScriptType::Js);
rt.block_on(async { run_code_handler(axum::Json(req)).await.unwrap() })
});
});
// 测试TS代码执行性能
group.bench_function(BenchmarkId::new("typescript", "test_ts.ts"), |b| {
b.iter(|| {
let req = create_run_code_request(&ts_code, ScriptType::Ts);
rt.block_on(async { run_code_handler(axum::Json(req)).await.unwrap() })
});
});
// 测试Python代码执行性能
group.bench_function(BenchmarkId::new("python", "test_python_simple.py"), |b| {
b.iter(|| {
let req = create_run_code_request(&py_code, ScriptType::Python);
rt.block_on(async { run_code_handler(axum::Json(req)).await.unwrap() })
});
});
group.finish();
}
criterion_group! {
name = benches;
config = Criterion::default()
.sample_size(10) // 减少样本数量
.warm_up_time(std::time::Duration::from_secs(20)) // 设置20秒的预热时间
.measurement_time(std::time::Duration::from_secs(10)); // 设置更长的测量时间
targets = bench_run_code
}
criterion_main!(benches);

View File

@@ -0,0 +1,5 @@
fn main() {
println!("cargo:rerun-if-changed=locales/en.yml");
println!("cargo:rerun-if-changed=locales/zh-CN.yml");
println!("cargo:rerun-if-changed=locales/zh-TW.yml");
}

View File

@@ -0,0 +1,13 @@
server:
# The port to listen on for incoming connections
port: 8085
# The log level to use
log:
level: debug
# The path to the log file
path: logs
# The number of log files to retain (default: 5)
retain_days: 5
mirror:
npm_registry: ""
pypi_index_url: ""

View File

@@ -0,0 +1,23 @@
CREATE TABLE IF NOT EXISTS mcp_plugin (
id bigint NOT NULL AUTO_INCREMENT PRIMARY KEY COMMENT '主键',
name varchar(128) NOT NULL COMMENT '插件名称',
command varchar(512) NOT NULL COMMENT '启动命令,用户输入,或者从json配置中解析获取',
args JSON NULL COMMENT '启动参数(JSON格式),例如:[{"name":"port","value":"8000"}]',
envs JSON NULL COMMENT '环境变量(JSON格式),例如:[{"name":"PORT","value":"8000"}]',
mounts JSON NULL COMMENT '挂载目录(JSON格式),例如:[{"name":"/data/mcp","path":"/data/mcp"}]',
port int NULL COMMENT '端口,例如:8000',
external_url varchar(512) NULL COMMENT '外部访问路径,例如:http://192.168.1.1:8000',
container_name varchar(128) NULL COMMENT '容器名称,例如:mcp-plugin',
sse_path varchar(128) NULL COMMENT 'SSE路径,例如:/sse,注意不要与其他 Server 重复',
config_json JSON NULL COMMENT '插件配置(JSON)',
status tinyint DEFAULT 2 NOT NULL COMMENT 'MCP启动状态:1:RUNNING(运行中),2:STOPPED(已停止),3:ERROR(错误)',
enabled tinyint DEFAULT 1 NOT NULL COMMENT '是否启用:1(启用),0(禁用)',
-- 公共字段
created datetime DEFAULT CURRENT_TIMESTAMP NOT NULL COMMENT '创建时间',
creator_id bigint NULL COMMENT '创建人id',
creator_name varchar(64) NULL COMMENT '创建人',
modified datetime DEFAULT CURRENT_TIMESTAMP NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
modified_id bigint NULL COMMENT '最后修改人id',
modified_name varchar(64) NULL COMMENT '最后修改人',
yn tinyint DEFAULT 1 NULL COMMENT '逻辑标记,1:有效;-1:无效'
) DEFAULT CHARSET=utf8mb4 COMMENT='MCP插件服务表';

View File

@@ -0,0 +1,24 @@
import * as o from 'https://deno.land/x/cowsay/mod.ts'
// 使用类型注解时应确保使用的是支持该特性的环境或编译器(如 TypeScript
function add(input) {
console.log("test Add", input.a, "+", input.b);
let m = o.say({
text: 'hello every one'
})
console.log(m)
return input.a + input.b;
}
export default async function handler(input) {
let a = add(input)
console.log("计算结果=" + a)
return {
message: a,
};
}

View File

@@ -0,0 +1,30 @@
import axios from 'npm:axios';
// 使用axios发起HTTP请求的函数
async function fetchData(url) {
console.log(`正在请求: ${url}`);
try {
const response = await axios.get(url);
console.log(`请求成功,状态码: ${response.status}`);
return response.data;
} catch (error) {
console.error(`请求失败: ${error.message}`);
return { error: error.message };
}
}
export default async function handler(input) {
// 默认URL或从输入参数获取
const url = input.url || 'https://jsonplaceholder.typicode.com/todos/1';
console.log(`开始处理请求URL: ${url}`);
// 发起请求并获取数据
const data = await fetchData(url);
return {
message: '请求完成',
data: data,
timestamp: new Date().toISOString()
};
}

View File

@@ -0,0 +1,79 @@
import { join, basename } from "https://deno.land/std/path/mod.ts";
import { readLines } from "https://deno.land/std/io/mod.ts";
import { format } from "https://deno.land/std/datetime/mod.ts";
// 使用Deno标准库处理路径和文件
async function processPath(path) {
console.log(`处理路径: ${path}`);
// 使用path模块处理路径
const fileName = basename(path);
console.log(`文件名: ${fileName}`);
const fullPath = join("/tmp", fileName);
console.log(`完整路径: ${fullPath}`);
// 获取当前时间并格式化
const now = new Date();
const formattedDate = format(now, "yyyy-MM-dd HH:mm:ss");
console.log(`当前时间: ${formattedDate}`);
return {
originalPath: path,
fileName: fileName,
fullPath: fullPath,
timestamp: formattedDate
};
}
// 模拟读取文件内容
async function simulateFileReading() {
const text = `这是第一行
这是第二行
这是第三行
这是最后一行`;
// 使用TextEncoder和StringReader代替Deno.Buffer
const encoder = new TextEncoder();
const decoder = new TextDecoder();
const data = encoder.encode(text);
// 创建一个可读流
const stream = new ReadableStream({
start(controller) {
controller.enqueue(data);
controller.close();
}
});
// 将流转换为Reader
const reader = stream.getReader();
console.log("开始读取文件内容:");
// 手动处理文本行
const lines = text.split('\n');
for (let i = 0; i < lines.length; i++) {
console.log(`${i+1}行: ${lines[i]}`);
}
return lines;
}
export default async function handler(input) {
console.log("开始处理");
// 处理路径
const path = input.path || "example.txt";
const pathInfo = await processPath(path);
// 模拟读取文件
const fileLines = await simulateFileReading();
return {
message: '处理完成',
pathInfo: pathInfo,
fileContent: fileLines,
linesCount: fileLines.length
};
}

View File

@@ -0,0 +1,59 @@
// 使用ES模块导入方式
import { createHash } from "node:crypto";
import { Buffer } from "node:buffer";
// 使用Node.js内置模块处理数据
function hashData(data, algorithm = 'sha256') {
console.log(`使用 ${algorithm} 算法处理数据`);
// 将数据转换为Buffer
const buffer = typeof data === 'string'
? Buffer.from(data)
: Buffer.from(JSON.stringify(data));
// 创建哈希
const hash = createHash(algorithm);
hash.update(buffer);
// 获取哈希结果
const digest = hash.digest('hex');
console.log(`哈希结果: ${digest}`);
return digest;
}
// 加密数据
function encryptData(data, salt = 'default-salt') {
console.log(`加密数据,使用盐值: ${salt}`);
// 组合数据和盐值
const combined = `${data}:${salt}:${Date.now()}`;
// 生成多种哈希
const sha256Hash = hashData(combined, 'sha256');
const md5Hash = hashData(combined, 'md5');
return {
original: data,
salt,
sha256: sha256Hash,
md5: md5Hash,
timestamp: new Date().toISOString()
};
}
export default async function handler(input) {
console.log("开始处理加密任务");
// 获取输入数据
const data = input.data || "这是需要加密的默认数据";
const salt = input.salt || "custom-salt-" + Math.floor(Math.random() * 1000);
// 加密数据
const encryptedResult = encryptData(data, salt);
return {
message: '加密处理完成',
result: encryptedResult
};
}

View File

@@ -0,0 +1,79 @@
import { assertEquals } from "jsr:@std/assert";
import { delay } from "jsr:@std/async";
import { bold, red, green } from "jsr:@std/fmt/colors";
// 使用JSR标准库中的工具函数
async function runTests(input) {
console.log(bold("开始运行测试..."));
// 使用delay函数模拟异步操作
console.log("等待1秒...");
await delay(1000);
// 测试结果数组
const results = [];
// 测试1: 加法运算
try {
const a = input.a || 5;
const b = input.b || 3;
const expected = input.expected || 8;
console.log(`测试加法: ${a} + ${b} = ${expected}`);
assertEquals(a + b, expected);
console.log(green("✓ 加法测试通过"));
results.push({ name: "加法测试", success: true });
} catch (error) {
console.log(red(`✗ 加法测试失败: ${error.message}`));
results.push({ name: "加法测试", success: false, error: error.message });
}
// 测试2: 字符串操作
try {
const str1 = input.str1 || "hello";
const str2 = input.str2 || "world";
const expectedStr = input.expectedStr || "hello world";
console.log(`测试字符串连接: "${str1}" + " " + "${str2}" = "${expectedStr}"`);
assertEquals(`${str1} ${str2}`, expectedStr);
console.log(green("✓ 字符串测试通过"));
results.push({ name: "字符串测试", success: true });
} catch (error) {
console.log(red(`✗ 字符串测试失败: ${error.message}`));
results.push({ name: "字符串测试", success: false, error: error.message });
}
// 等待再次展示结果
console.log("处理结果中...");
await delay(500);
return results;
}
export default async function handler(input) {
console.log(bold("JSR依赖示例"));
// 运行测试
const testResults = await runTests(input);
// 统计结果
const passed = testResults.filter(r => r.success).length;
const failed = testResults.filter(r => !r.success).length;
// 使用彩色输出显示结果
console.log(bold("\n测试结果摘要:"));
console.log(green(`通过: ${passed}`));
console.log(failed > 0 ? red(`失败: ${failed}`) : green(`失败: ${failed}`));
return {
message: '测试完成',
results: testResults,
summary: {
total: testResults.length,
passed,
failed
}
};
}

View File

@@ -0,0 +1,74 @@
// 首先创建一个本地模块,通过相对路径引入
// 注意这个示例假设有一个名为utils.js的本地模块
// 模拟本地utils.js模块的内容
const utils = {
formatNumber(num) {
return num.toLocaleString('zh-CN');
},
calculateTax(amount, rate = 0.1) {
return amount * rate;
},
generateId() {
return `id-${Date.now()}-${Math.floor(Math.random() * 1000)}`;
},
formatDate(date = new Date()) {
return date.toLocaleDateString('zh-CN');
}
};
// 使用本地模块的函数
function processOrder(order) {
console.log(`处理订单: ${order.id || utils.generateId()}`);
// 计算订单总额
const total = order.items.reduce((sum, item) => sum + (item.price * item.quantity), 0);
console.log(`订单总额: ${utils.formatNumber(total)}`);
// 计算税费
const taxRate = order.taxRate || 0.13;
const tax = utils.calculateTax(total, taxRate);
console.log(`税费(${taxRate * 100}%): ${utils.formatNumber(tax)}`);
// 计算最终金额
const finalAmount = total + tax;
console.log(`最终金额: ${utils.formatNumber(finalAmount)}`);
// 生成订单日期
const orderDate = order.date ? new Date(order.date) : new Date();
console.log(`订单日期: ${utils.formatDate(orderDate)}`);
return {
id: order.id || utils.generateId(),
items: order.items,
subtotal: total,
tax: tax,
total: finalAmount,
date: utils.formatDate(orderDate)
};
}
export default async function handler(input) {
console.log("开始处理订单");
// 默认订单或从输入获取
const order = input.order || {
items: [
{ name: "产品A", price: 100, quantity: 2 },
{ name: "产品B", price: 50, quantity: 1 },
{ name: "产品C", price: 200, quantity: 3 }
],
taxRate: 0.13
};
// 处理订单
const processedOrder = processOrder(order);
return {
message: '订单处理完成',
order: processedOrder
};
}

View File

@@ -0,0 +1,43 @@
import _ from 'npm:lodash';
// 使用lodash处理数据的函数
function processData(input) {
console.log("输入数据:", input);
// 使用lodash的chunk方法将数组分块
if (Array.isArray(input.data)) {
const chunks = _.chunk(input.data, input.chunkSize || 2);
console.log("数据分块结果:", chunks);
// 使用lodash的map方法处理每个块
const processedChunks = _.map(chunks, (chunk) => {
return {
items: chunk,
sum: _.sum(chunk),
average: _.mean(chunk)
};
});
return processedChunks;
}
return { error: "输入数据不是数组" };
}
export default async function handler(input) {
console.log("开始处理数据");
// 从输入获取数据,或使用默认数据
const data = input.data || [1, 2, 3, 4, 5, 6, 7, 8, 9];
const chunkSize = input.chunkSize || 3;
// 处理数据
const result = processData({ data, chunkSize });
return {
message: '数据处理完成',
originalData: data,
processedData: result,
timestamp: new Date().toISOString()
};
}

View File

@@ -0,0 +1,26 @@
import requests
from bs4 import BeautifulSoup
# 发送请求到百度
url = 'https://www.baidu.com'
response = requests.get(url)
# 检查请求是否成功
if response.status_code == 200:
# 解析网页内容
soup = BeautifulSoup(response.text, 'html.parser')
# 提取你想要的信息,例如标题
title = soup.title.string
else:
print(f"请求失败,状态码:")
# 入口函数不可修改否则无法执行args 为配置的入参
def main(args: dict) -> dict:
params = args.get("params")
# 构建输出对象出参中的key需与配置的出参保持一致
ret = {
"key": "value"
}
return ret

View File

@@ -0,0 +1,20 @@
// 使用类型注解时应确保使用的是支持该特性的环境或编译器(如 TypeScript
function add(input) {
console.log("test Add", input.a, "+", input.b);
// 简单的字符串输出,不使用外部模块
console.log("Hello from JavaScript!");
return input.a + input.b;
}
function handler(input) {
console.log("输入参数:", JSON.stringify(input));
let a = add(input);
console.log("计算结果=" + a);
return {
message: a,
};
}

View File

@@ -0,0 +1,40 @@
#!/usr/bin/env python3
import pandas as pd
import logging
import json
# 创建一个简单的数据结构不使用pandas
data = {
'Name': ['Alice', 'Bob', 'Charlie'],
'Age': [25, 30, 35]
}
df = pd.DataFrame(data)
# 记录数据信息
logging.info(f"Created data structure:\n{json.dumps(data, indent=2)}")
def main(args: dict) -> dict:
logging.info(f"input args: {args}")
params = args.get("params")
# 处理params为None的情况
if params is None:
logging.warning("params is None, using default values")
params = {"input": "default_value"}
elif not isinstance(params, dict):
logging.warning(f"params is not a dictionary: {type(params)}, using default values")
params = {"input": str(params)}
elif "input" not in params:
logging.warning("input key not found in params, using default value")
params["input"] = "default_value"
# 构建输出对象
ret = {
"key0": params['input'], # 使用input参数值
"key1": ["hello", "world"], # 输出一个数组
"key2": { # 输出一个Object
"key21": "hi"
},
}
return ret

View File

@@ -0,0 +1,10 @@
from fastmcp import FastMCP
mcp = FastMCP("MCPServer")
@mcp.tool
def hello(name: str) -> str:
return f"Hello, {name}!"
if __name__ == "__main__":
mcp.run(transport="streamable-http", host="0.0.0.0", port=8000, path="/mcp")

View File

@@ -0,0 +1,25 @@
// Sample JavaScript file with handler function
// Log some debug information
console.log("Initializing JavaScript test...");
// Define a simple add function
function add(a, b) {
console.log(`Adding ${a} + ${b}`);
return a + b;
}
// Some processing
const numbers = [1, 2, 3, 4, 5];
console.log("Processing numbers:", numbers);
// Handler function that will be called to get the result
function handler() {
console.log("Handler function called");
// Calculate sum
const sum = numbers.reduce((acc, num) => add(acc, num), 0);
console.log("Final calculation completed");
return `The sum of [${numbers.join(", ")}] is ${sum}`;
}

View File

@@ -0,0 +1,39 @@
// JavaScript测试文件演示参数传递
// 打印一些调试信息
console.log("JavaScript脚本开始执行...");
// 定义一个加法函数
function add(a, b) {
console.log(`正在计算: ${a} + ${b}`);
return a + b;
}
// 处理一些数据
const numbers = [1, 2, 3, 4, 5];
console.log(`处理数字列表: ${numbers}`);
/**
* 处理函数,接收参数并返回结果
* 注意:这个函数必须存在,并且会被框架调用来获取结果
*/
function handler(input) {
console.log(`接收到的参数: ${JSON.stringify(input)}`);
// 从参数中获取值,提供默认值
const a = input.a || 0;
const b = input.b || 0;
const name = input.name || "用户";
// 计算结果
const result = add(a, b);
console.log(`计算完成: ${a} + ${b} = ${result}`);
// 返回结果
return {
sum: result,
numbers: numbers,
greeting: `你好,${name}`,
message: `成功计算 ${a} + ${b} 的结果`
};
}

View File

@@ -0,0 +1,38 @@
#!/usr/bin/env python3
# Sample Python file with handler function
import time
time.time()
# Debug print to confirm script execution
print("DEBUGGING: Script is being executed!")
# Log some debug information
print("Initializing Python test...")
# Define a simple function
def multiply(a, b):
print(f"Multiplying {a} * {b}")
return a * b
# Some processing
numbers = [1, 2, 3, 4, 5]
print(f"Processing numbers: {numbers}")
# Handler function that will be called to get the result
def handler():
print("Handler function called")
# Calculate product
product = 1
for num in numbers:
product = multiply(product, num)
print("Final calculation completed")
return f"The product of {numbers} is {product}"
# This part won't be executed when the code is run through the MCP runner
if __name__ == "__main__":
print("Running directly, calling handler...")
result = handler()
print(f"Result: {result}")

View File

@@ -0,0 +1,36 @@
#!/usr/bin/env python3
# 测试logging模块的日志捕获功能
import logging
import json
# 配置日志
logging.basicConfig(level=logging.DEBUG)
# 使用不同级别的日志
def log_messages():
logging.debug("这是一条DEBUG级别的日志")
logging.info("这是一条INFO级别的日志")
logging.warning("这是一条WARNING级别的日志")
logging.error("这是一条ERROR级别的日志")
logging.critical("这是一条CRITICAL级别的日志")
# 使用格式化
data = {"name": "测试", "value": 42}
logging.info(f"格式化的JSON数据: {json.dumps(data, ensure_ascii=False)}")
return "日志测试完成"
def handler(args):
# 记录输入参数
logging.info(f"收到参数: {args}")
# 生成一些日志
result = log_messages()
# 返回结果
return {
"message": result,
"log_count": 6, # 总共记录了6条日志
"args": args
}

View File

@@ -0,0 +1,33 @@
import json
import os
# 打印一些调试信息
print("Python脚本开始执行...")
# 定义一个简单的加法函数
def add(a, b):
print("正在计算: " + str(a) + " + " + str(b))
return a + b
# 处理一些数据
numbers = [1, 2, 3, 4, 5]
print("处理数字列表: " + str(numbers))
# 入口函数,接收参数
def main(args):
print("接收到的参数: " + str(args))
# 从参数中获取值
a = args.get("a", 0)
b = args.get("b", 0)
# 计算结果
result = add(a, b)
print("计算完成: " + str(a) + " + " + str(b) + " = " + str(result))
# 返回结果
return {
"sum": result,
"numbers": numbers,
"message": "成功计算 " + str(a) + " + " + str(b) + " 的结果"
}

View File

@@ -0,0 +1,23 @@
#!/usr/bin/env python3
# 简单的Python测试脚本用于测试参数传递机制
def main(args: dict) -> dict:
print(f"接收到的参数: {args}")
# 尝试通过两种方式获取参数
direct_params = args.get("input", "direct_default")
nested_params = None
params = args.get("params")
if params:
if isinstance(params, dict):
nested_params = params.get("input", "nested_default")
else:
nested_params = str(params)
# 返回结果,展示两种方式获取的参数
return {
"direct_access": direct_params,
"nested_access": nested_params,
"args_structure": args
}

View File

@@ -0,0 +1,46 @@
import json
import os
# 打印一些调试信息
print("Python类型测试脚本开始执行...")
# 入口函数,接收参数并返回不同类型的值
def main(args):
print("接收到的参数:", args)
# 获取要测试的类型
test_type = args.get("type", "string")
# 根据参数返回不同类型的值
if test_type == "string":
print("返回字符串类型")
return "这是一个字符串"
elif test_type == "number":
print("返回数字类型")
return 12345
elif test_type == "boolean":
print("返回布尔类型")
return True
elif test_type == "null":
print("返回None类型")
return None
elif test_type == "list":
print("返回列表类型")
return [1, 2, 3, "", "", True]
elif test_type == "dict":
print("返回字典类型")
return {
"name": "测试用户",
"age": 30,
"is_active": True,
"tags": ["python", "rust", "json"]
}
else:
print("未知类型,返回默认字符串")
return "未知类型"

View File

@@ -0,0 +1,41 @@
// Sample TypeScript file with handler function
// Log some debug information
console.log("Initializing TypeScript test...");
// Define a simple add function with type annotations
function add(a: number, b: number): number {
console.log(`Adding ${a} + ${b}`);
return a + b;
}
// Some processing with type annotations
const numbers: number[] = [1, 2, 3, 4, 5];
console.log("Processing numbers:", numbers);
// Interface for a person object
interface Person {
name: string;
age: number;
}
// Create a person object
const person: Person = {
name: "TypeScript User",
age: 30
};
console.log("Person object:", person);
/**
* Handler function that will be called to get the result
* 注意:这个函数必须存在,并且会被框架调用来获取结果
*/
function handler(): string {
console.log("Handler function called");
// Calculate sum
const sum = numbers.reduce((acc, num) => add(acc, num), 0);
console.log("Final calculation completed");
return `Hello ${person.name}! The sum of [${numbers.join(", ")}] is ${sum}`;
}

View File

@@ -0,0 +1,46 @@
// TypeScript测试文件演示参数传递
// 定义参数接口
interface InputParams {
a: number;
b: number;
name?: string;
}
// 打印一些调试信息
console.log("TypeScript脚本开始执行...");
// 定义一个带类型的加法函数
function add(a: number, b: number): number {
console.log(`正在计算: ${a} + ${b}`);
return a + b;
}
// 处理一些数据
const numbers: number[] = [1, 2, 3, 4, 5];
console.log(`处理数字列表: ${numbers}`);
/**
* 处理函数,接收参数并返回结果
* 注意:这个函数必须存在,并且会被框架调用来获取结果
*/
function handler(input: InputParams): object {
console.log(`接收到的参数: ${JSON.stringify(input)}`);
// 从参数中获取值,提供默认值
const a = input.a || 0;
const b = input.b || 0;
const name = input.name || "用户";
// 计算结果
const result = add(a, b);
console.log(`计算完成: ${a} + ${b} = ${result}`);
// 返回结果
return {
sum: result,
numbers: numbers,
greeting: `你好,${name}`,
message: `成功计算 ${a} + ${b} 的结果`
};
}

View File

@@ -0,0 +1,468 @@
# ===========================================
# Error Messages - mcp-proxy
# ===========================================
errors.mcp_proxy.service_not_found: "Service %{service} not found"
errors.mcp_proxy.service_restart_cooldown: "Service %{service} is in restart cooldown, please try again later"
errors.mcp_proxy.service_startup_in_progress: "Service %{service} is starting, please wait"
errors.mcp_proxy.service_startup_failed: "Service startup failed: %{mcp_id}: %{reason}"
errors.mcp_proxy.backend_connection: "Backend connection error: %{detail}"
errors.mcp_proxy.config_parse: "Configuration parse error: %{detail}"
errors.mcp_proxy.mcp_server_error: "MCP server error: %{detail}"
errors.mcp_proxy.json_serialization: "JSON serialization error: %{detail}"
errors.mcp_proxy.io_error: "IO error: %{detail}"
errors.mcp_proxy.route_not_found: "Route not found: %{path}"
errors.mcp_proxy.invalid_parameter: "Invalid request parameter: %{detail}"
# ===========================================
# Error Messages - document-parser
# ===========================================
errors.document_parser.config: "Configuration error: %{detail}"
errors.document_parser.file: "File operation error: %{detail}"
errors.document_parser.unsupported_format: "Unsupported file format: %{format}"
errors.document_parser.parse: "Parse error: %{detail}"
errors.document_parser.mineru: "MinerU error: %{detail}"
errors.document_parser.markitdown: "MarkItDown error: %{detail}"
errors.document_parser.oss: "OSS operation error: %{detail}"
errors.document_parser.database: "Database error: %{detail}"
errors.document_parser.network: "Network error: %{detail}"
errors.document_parser.task: "Task error: %{detail}"
errors.document_parser.internal: "Internal error: %{detail}"
errors.document_parser.timeout: "Operation timeout: %{detail}"
errors.document_parser.validation: "Validation error: %{detail}"
errors.document_parser.environment: "Environment error: %{detail}"
errors.document_parser.virtual_environment_path: "Virtual environment path error: %{detail}"
errors.document_parser.permission: "Permission error: %{detail}"
errors.document_parser.path: "Path error: %{detail}"
errors.document_parser.queue: "Queue error: %{detail}"
errors.document_parser.processing: "Processing error: %{detail}"
# Error Suggestions - document-parser
errors.document_parser.suggestions.config: "Check configuration file and environment variables"
errors.document_parser.suggestions.file: "Check file path and permissions"
errors.document_parser.suggestions.unsupported_format: "Check if file format is supported"
errors.document_parser.suggestions.parse: "Check if file content is complete"
errors.document_parser.suggestions.mineru: "Check MinerU environment configuration"
errors.document_parser.suggestions.markitdown: "Check MarkItDown environment configuration"
errors.document_parser.suggestions.oss: "Check OSS configuration and network connection"
errors.document_parser.suggestions.database: "Check database connection and permissions"
errors.document_parser.suggestions.network: "Check network connection and firewall settings"
errors.document_parser.suggestions.task: "Check task parameters and status"
errors.document_parser.suggestions.internal: "Contact technical support"
errors.document_parser.suggestions.timeout: "Check network latency or increase timeout"
errors.document_parser.suggestions.validation: "Check input parameter format"
errors.document_parser.suggestions.environment: "Check system environment and dependency installation"
errors.document_parser.suggestions.queue: "Check queue service status and configuration"
errors.document_parser.suggestions.processing: "Check processing flow and data format"
errors.document_parser.suggestions.virtual_environment_path: "Check virtual environment path and directory permissions"
errors.document_parser.suggestions.permission: "Check file and directory permission settings"
errors.document_parser.suggestions.path: "Check if path exists and is accessible"
# ===========================================
# Error Messages - oss-client
# ===========================================
errors.oss.config: "Configuration error: %{detail}"
errors.oss.network: "Network error: %{detail}"
errors.oss.file_not_found: "File not found: %{path}"
errors.oss.permission: "Permission denied: %{detail}"
errors.oss.io: "IO error: %{detail}"
errors.oss.sdk: "OSS SDK error: %{detail}"
errors.oss.file_size_exceeded: "File size exceeded: %{detail}"
errors.oss.unsupported_file_type: "Unsupported file type: %{detail}"
errors.oss.timeout: "Operation timeout: %{detail}"
errors.oss.invalid_parameter: "Invalid parameter: %{detail}"
# ===========================================
# Error Messages - voice-cli
# ===========================================
errors.voice.config: "Configuration error: %{detail}"
errors.voice.audio_processing: "Audio processing error: %{detail}"
errors.voice.transcription: "Transcription error: %{detail}"
errors.voice.model: "Model error: %{detail}"
errors.voice.file_io: "File I/O error: %{detail}"
errors.voice.http: "HTTP request error: %{detail}"
errors.voice.serialization: "Serialization error: %{detail}"
errors.voice.json: "JSON error: %{detail}"
errors.voice.config_rs: "Config-rs error: %{detail}"
errors.voice.daemon: "Daemon error: %{detail}"
errors.voice.unsupported_format: "Audio format not supported: %{detail}"
errors.voice.file_too_large: "File too large: %{size} bytes (max: %{max} bytes)"
errors.voice.model_not_found: "Model not found: %{model}"
errors.voice.invalid_model_name: "Invalid model name: %{model}"
errors.voice.worker_pool: "Worker pool error: %{detail}"
errors.voice.transcription_timeout: "Transcription timeout after %{seconds} seconds"
errors.voice.transcription_failed: "Transcription failed: %{detail}"
errors.voice.audio_conversion_failed: "Audio conversion failed: %{detail}"
errors.voice.audio_probe_error: "Audio probe error: %{detail}"
errors.voice.temp_file_error: "Temporary file error: %{detail}"
errors.voice.multipart_error: "Multipart form error: %{detail}"
errors.voice.missing_field: "Missing required field: %{field}"
errors.voice.network: "Network error: %{detail}"
errors.voice.storage: "Storage error: %{detail}"
errors.voice.task_management_disabled: "Task management is disabled"
errors.voice.not_found: "Resource not found: %{resource}"
errors.voice.initialization: "Initialization error: %{detail}"
errors.voice.tts: "TTS error: %{detail}"
errors.voice.invalid_input: "Invalid input: %{detail}"
errors.voice.io: "IO error: %{detail}"
# ===========================================
# CLI Messages - mcp-proxy startup
# ===========================================
cli.mirror.not_configured: "No mirror configured (npm/PyPI), using default sources"
cli.mirror.npm: "npm mirror: %{url}"
cli.mirror.pypi: "PyPI mirror: %{url}"
cli.startup.service_starting: "MCP-Proxy starting..."
cli.startup.version: "Version: %{version}"
cli.startup.config_loaded: "Configuration loaded"
cli.startup.port: "Port: %{port}"
cli.startup.log_dir: "Log directory: %{path}"
cli.startup.log_level: "Log level: %{level}"
cli.startup.log_retain_days: "Log retention days: %{days}"
cli.startup.success: "✅ Service started successfully, listening on: %{addr}"
cli.startup.health_endpoint: "✅ Health check endpoint: http://%{addr}/health"
cli.startup.mcp_list: "✅ MCP service list: http://%{addr}/mcp"
cli.startup.schedule_task_started: "✅ MCP service status check scheduled task started"
cli.startup.log_rotation_configured: "✅ Log rotation configured (keeping last %{count} log files)"
cli.startup.system_info: "System information:"
cli.startup.os: "Operating system: %{os}"
cli.startup.arch: "Architecture: %{arch}"
cli.startup.work_dir: "Working directory: %{path}"
cli.startup.env_override: "Environment variable overrides:"
cli.startup.env_port_override: "MCP_PROXY_PORT: %{port}"
cli.startup.env_log_dir_override: "MCP_PROXY_LOG_DIR: %{dir}"
cli.startup.env_log_level_override: "MCP_PROXY_LOG_LEVEL: %{level}"
cli.startup.trying_bind: "Attempting to bind to address: %{addr}"
cli.startup.bind_success: "Successfully bound to address: %{addr}"
cli.startup.bind_failed: "Failed to bind address %{addr}: %{error}"
cli.startup.init_state: "Initializing application state..."
cli.startup.state_done: "Application state initialized"
cli.startup.init_router: "Initializing router..."
cli.startup.router_done: "Router initialized"
cli.startup.warming_up: "\U0001F504 Warming up uv/deno environment dependencies..."
cli.startup.warmup_success: "✅ uv/deno environment warmup complete"
cli.startup.warmup_failed: "❌ uv/deno environment warmup failed: %{error}"
cli.startup.http_server_starting: "\U0001F680 HTTP server starting, waiting for connections..."
cli.startup.proxy_mode: "Command: proxy (HTTP server mode)"
cli.startup.config_info: "Configuration info:"
cli.startup.listen_port: "Listen port: %{port}"
cli.startup.log_retention: "Log retention: %{days} days"
# CLI Messages - mcp-proxy shutdown
cli.shutdown.signal_received: "⚠️ Server received shutdown signal, cleaning up resources..."
cli.shutdown.cleanup_success: "✅ Resource cleanup successful"
cli.shutdown.cleanup_failed: "❌ Error during resource cleanup: %{error}"
cli.shutdown.complete: "✅ Resource cleanup complete, service fully shut down"
cli.shutdown.service_error: "❌ Service runtime error: %{error}"
# CLI Messages - panic handling
cli.panic.handler_started: "Program panic occurred, executing cleanup..."
cli.panic.reason: "Panic reason: %{reason}"
cli.panic.reason_unknown: "Panic reason: unknown"
cli.panic.location: "Panic location: %{file}:%{line}"
cli.panic.stack_trace: "Stack trace:"
# ===========================================
# CLI Messages - convert mode
# ===========================================
cli.convert.starting: "Starting URL mode processing"
cli.convert.target_url: "Target URL: %{url}"
cli.convert.protocol_specified: "Using specified protocol: %{protocol}"
cli.convert.protocol_config: "Using configured protocol: %{protocol}"
cli.convert.detecting_protocol: "Starting protocol auto-detection..."
cli.convert.detect_failed: "Protocol detection failed: %{error}"
cli.convert.using_protocol: "Using %{protocol} protocol mode"
cli.convert.stdio_url_not_supported: "Stdio protocol does not support URL conversion"
cli.convert.tool_whitelist: "Tool whitelist: %{tools}"
cli.convert.tool_blacklist: "Tool blacklist: %{tools}"
cli.convert.config_parsed: "Configuration parsed successfully"
cli.convert.service_name: "MCP service name: %{name}"
cli.convert.service_name_not_specified: "MCP service name: not specified (using direct URL)"
cli.convert.cli_starting: "MCP-Proxy CLI starting"
cli.convert.command: "Command: convert (stdio bridge mode)"
cli.convert.version: "Version: %{version}"
cli.convert.diagnostic_mode: "Diagnostic mode: %{enabled}"
cli.convert.mode_direct_url: "Mode: direct URL mode"
cli.convert.mode_remote_service: "Mode: remote service configuration mode"
cli.convert.service_url: "Service URL: %{url}"
cli.convert.config_protocol: "Configured protocol: %{protocol}"
cli.convert.ping_config: "Ping interval: %{interval}s, Ping timeout: %{timeout}s"
cli.convert.connecting_backend: "Connecting to backend service (timeout: %{timeout}s)..."
cli.convert.detect_complete: "Protocol detection complete: %{protocol} (duration: %{duration})"
# ===========================================
# CLI Messages - SSE mode
# ===========================================
cli.sse.mode_starting: "SSE mode starting"
cli.sse.connect_timeout: "Backend connection timeout (%{seconds}s)"
cli.sse.connect_failed: "Backend connection failed: %{error}"
cli.sse.connect_success: "Backend connected (duration: %{duration})"
cli.sse.stdio_starting: "Starting stdio server..."
cli.sse.stdio_started: "Stdio server started"
cli.sse.waiting_events: "Waiting for stdio server events..."
cli.sse.stdio_exit_eof: "Stdio server exited - reason: MCP client disconnected (stdin EOF)"
cli.sse.watchdog_exit: "Watchdog task exited"
cli.sse.normal_exit: "mcp-proxy convert (SSE mode) exited normally"
cli.sse.watchdog_starting: "SSE Watchdog starting"
cli.sse.max_retries: "Max retries: %{count} (0=unlimited)"
cli.sse.monitoring_connection: "Monitoring initial connection..."
cli.sse.initial_disconnect: "Initial connection disconnected: %{reason}"
cli.sse.connection_alive: "Initial connection alive for: %{seconds}s"
cli.sse.reconnect_attempt: "Reconnect attempt #%{attempt}/%{max}"
cli.sse.backoff_time: "Backoff time: %{seconds}s"
cli.sse.reconnect_success: "Reconnect successful (duration: %{duration})"
cli.sse.monitoring_reconnect: "Monitoring reconnected connection..."
cli.sse.reconnect_disconnect: "Disconnected after reconnect: %{reason}"
cli.sse.reconnect_alive: "Reconnected connection alive for: %{seconds}s"
cli.sse.max_retries_reached: "Max retries reached (%{count}), stopping reconnect"
cli.sse.watchdog_exit_msg: "SSE Watchdog exited"
# ===========================================
# CLI Messages - Stream mode
# ===========================================
cli.stream.mode_starting: "Stream mode starting"
cli.stream.connect_timeout: "Backend connection timeout (%{seconds}s)"
cli.stream.connect_failed: "Backend connection failed: %{error}"
cli.stream.connect_success: "Backend connected (duration: %{duration})"
cli.stream.stdio_starting: "Starting stdio server..."
cli.stream.stdio_started: "Stdio server started"
cli.stream.waiting_events: "Waiting for stdio server events..."
cli.stream.stdio_exit_eof: "Stdio server exited - reason: MCP client disconnected (stdin EOF)"
cli.stream.watchdog_exit: "Watchdog task exited"
cli.stream.normal_exit: "mcp-proxy convert (Stream mode) exited normally"
# ===========================================
# CLI Messages - Command mode
# ===========================================
cli.command.local_mode: "Mode: local command mode"
cli.command.command: "Command: %{cmd} %{args}"
cli.command.ctrl_c: "Received Ctrl+C signal, shutting down..."
# ===========================================
# CLI Messages - monitoring
# ===========================================
cli.monitoring.health: "Monitoring %{protocol} connection health"
cli.monitoring.health_check_status: "\U0001F493 [%{protocol}][Health Check] Connection status: %{status} (connection check only, no list_tools), check #%{count}, alive: %{seconds}s"
cli.monitoring.health_check_normal: "\U0001F493 [%{protocol}][Health Check] Backend service healthy (list_tools verified), Ping #%{count}, alive: %{seconds}s"
# ===========================================
# Diagnostic Messages
# ===========================================
diagnostic.report_header: "========== Diagnostic Report =========="
diagnostic.connection_protocol: "Connection protocol: %{protocol}"
diagnostic.service_url: "Service URL: %{url}"
diagnostic.connection_duration: "Connection duration: %{seconds} seconds"
diagnostic.disconnect_reason: "Disconnect reason: %{reason}"
diagnostic.error_type: "Error type: %{error_type}"
diagnostic.possible_causes: "Possible causes:"
diagnostic.suggestions: "Suggestions:"
# Diagnostic - 30s timeout analysis
diagnostic.analysis.timeout_30s: "⚠️ Connection dropped at ~30 seconds, likely causes:"
diagnostic.analysis.timeout_30s_cause1: "Server-side 30-second timeout limit"
diagnostic.analysis.timeout_30s_cause2: "Load balancer (e.g., Nginx/ALB) default timeout"
diagnostic.analysis.timeout_30s_cause3: "Cloud provider gateway timeout limit"
# Diagnostic - Quick disconnect analysis
diagnostic.analysis.quick_disconnect: "⚠️ Connection dropped quickly (%{seconds}s), possible causes:"
diagnostic.analysis.quick_disconnect_cause1: "Authentication failed or invalid token"
diagnostic.analysis.quick_disconnect_cause2: "Server rejected connection"
diagnostic.analysis.quick_disconnect_cause3: "Network instability"
# Diagnostic - Long connection analysis
diagnostic.analysis.long_connection: "✅ Connection maintained for a long time (%{seconds}s), possible causes:"
diagnostic.analysis.long_connection_cause1: "Tool call execution took too long"
diagnostic.analysis.long_connection_cause2: "Network fluctuation caused disconnect"
# Diagnostic suggestions
diagnostic.suggestion.timeout_30s: "Contact service provider to increase timeout limit"
diagnostic.suggestion.timeout_client: "Use --request-timeout parameter to set client timeout"
diagnostic.suggestion.async_mode: "Consider using async processing mode (webhook callback)"
diagnostic.suggestion.ping_interval: "Try increasing ping interval: --ping-interval %{seconds}"
diagnostic.suggestion.ping_timeout: "Increase ping timeout: --ping-timeout %{seconds}"
diagnostic.suggestion.ping_disable: "Or disable ping: --ping-interval 0"
# ===========================================
# Error Classification
# ===========================================
error_classify.timeout_30s: "30s timeout (possibly server limit)"
error_classify.service_unavailable_503: "Service unavailable (503)"
error_classify.internal_server_error_500: "Internal server error (500)"
error_classify.bad_gateway_502: "Bad gateway (502)"
error_classify.gateway_timeout_504: "Gateway timeout (504)"
error_classify.unauthorized_401: "Unauthorized (401)"
error_classify.forbidden_403: "Forbidden (403)"
error_classify.not_found_404: "Not found (404)"
error_classify.request_timeout_408: "Request timeout (408)"
error_classify.timeout: "Timeout"
error_classify.connection_refused: "Connection refused"
error_classify.connection_reset: "Connection reset"
error_classify.connection_closed: "Connection closed"
error_classify.dns_failed: "DNS resolution failed"
error_classify.ssl_tls_error: "SSL/TLS error"
error_classify.network_error: "Network error"
error_classify.session_error: "Session error"
error_classify.unknown_error: "Unknown error"
# ===========================================
# CLI Messages - health command
# ===========================================
cli.health.checking: "\U0001F50D Health checking service: %{url}"
cli.health.using_protocol: "\U0001F50D Using specified protocol: %{protocol}"
cli.health.detecting_protocol: "\U0001F50D Detecting protocol..."
cli.health.detected_protocol: "\U0001F50D Detected %{protocol} protocol"
cli.health.healthy: "✅ Service healthy"
cli.health.unhealthy: "❌ Service unhealthy"
cli.health.stdio_not_supported: "health command does not support stdio protocol"
# ===========================================
# CLI Messages - check command
# ===========================================
cli.check.checking: "\U0001F50D Checking service: %{url}"
cli.check.healthy: "✅ Service healthy, detected %{protocol} protocol"
cli.check.failed: "❌ Service check failed: %{error}"
# ===========================================
# CLI Messages - document-parser
# ===========================================
doc_parser.startup.app_info: "=== %{name} v%{version} starting ==="
doc_parser.startup.config_summary: "Configuration summary: %{summary}"
doc_parser.startup.listening: "Service listening on: %{host}:%{port}"
doc_parser.startup.checking_python: "Starting Python environment check and initialization..."
doc_parser.startup.checking_venv: "Checking and activating virtual environment..."
doc_parser.startup.venv_activate_failed: "Virtual environment auto-activation failed: %{error}"
doc_parser.startup.venv_activate_manual: "Please manually activate virtual environment: source ./venv/bin/activate"
doc_parser.startup.venv_activated: "Virtual environment auto-activated"
doc_parser.startup.env_check_failed: "Environment check failed, will auto-install in background: %{error}"
doc_parser.startup.mineru_not_installed: "MinerU dependencies not installed, starting background auto-install..."
doc_parser.startup.markitdown_not_installed: "MarkItDown dependencies not installed, starting background auto-install..."
doc_parser.startup.install_complete: "Background Python environment installation complete"
doc_parser.startup.install_failed: "Background Python environment installation failed: %{error}"
doc_parser.startup.install_task_started: "Python dependency installation task started (background), service will start normally"
doc_parser.startup.mineru_ready: "MinerU dependencies installed, version: %{version}"
doc_parser.startup.markitdown_ready: "MarkItDown dependencies installed"
doc_parser.startup.python_ready: "Python environment check complete, all dependencies ready"
doc_parser.startup.state_failed: "Failed to create application state: %{error}"
doc_parser.startup.health_check_failed: "Application health check failed: %{error}"
doc_parser.startup.state_ready: "Application state initialized successfully"
doc_parser.startup.http_routes_ready: "HTTP routes initialized successfully"
doc_parser.startup.background_tasks_started: "Background tasks started"
doc_parser.startup.service_ready: "Service started successfully, waiting for connections..."
doc_parser.shutdown.service_stopped: "Service stopped"
doc_parser.shutdown.ctrl_c_received: "Received Ctrl+C signal, starting graceful shutdown..."
doc_parser.shutdown.terminate_received: "Received terminate signal, starting graceful shutdown..."
doc_parser.shutdown.closing: "Shutting down service..."
# uv-init command
doc_parser.uv_init.starting: "\U0001F680 Starting uv virtual environment and dependency initialization in current directory..."
doc_parser.uv_init.current_dir: "\U0001F4C1 Current working directory: %{path}"
doc_parser.uv_init.venv_path: "\U0001F4C1 Virtual environment will be created at: ./venv/"
doc_parser.uv_init.validating_dir: "\U0001F50D Validating current directory settings..."
doc_parser.uv_init.dir_valid: " ✅ Directory validation passed"
doc_parser.uv_init.warnings_found: " ⚠️ Found %{count} warnings"
doc_parser.uv_init.dir_invalid: " ❌ Directory validation failed, found %{count} issues"
doc_parser.uv_init.auto_fixing: " \U0001F527 Attempting to auto-fix %{count} issues..."
doc_parser.uv_init.fix_success: " ✅ %{message}"
doc_parser.uv_init.fix_failed: " ❌ Cleanup failed: %{error}"
doc_parser.uv_init.manual_fix_hint: " \U0001F4A1 Please manually resolve the following issues:"
doc_parser.uv_init.dir_validation_failed: "Directory validation failed, please resolve the above issues and try again"
doc_parser.uv_init.dir_validation_error: " ⚠️ Directory validation failed: %{error}"
doc_parser.uv_init.continue_install: " Continuing installation, but may encounter issues..."
# Environment check
doc_parser.check.checking_env: "\U0001F50D Checking current environment status..."
doc_parser.check.env_check_complete: " Environment check complete:"
doc_parser.check.python_available: "✅ Available"
doc_parser.check.python_unavailable: "❌ Unavailable"
doc_parser.check.uv_available: "✅ Available"
doc_parser.check.uv_unavailable: "❌ Unavailable"
doc_parser.check.venv_active: "✅ Active"
doc_parser.check.venv_inactive: "❌ Inactive"
doc_parser.check.mineru_available: "✅ Available"
doc_parser.check.mineru_unavailable: "❌ Unavailable"
doc_parser.check.markitdown_available: "✅ Available"
doc_parser.check.markitdown_unavailable: "❌ Unavailable"
doc_parser.check.env_check_failed: " ⚠️ Environment check failed: %{error}"
doc_parser.check.continue_install: " Continuing installation..."
# Installation process
doc_parser.install.all_ready: "✨ All dependencies are ready, no installation needed!"
doc_parser.install.plan: "\U0001F4CB Installation plan:"
doc_parser.install.install_uv: "Install uv tool"
doc_parser.install.create_venv: "Create virtual environment (./venv/)"
doc_parser.install.install_mineru: "Install MinerU dependencies"
doc_parser.install.install_markitdown: "Install MarkItDown dependencies"
doc_parser.install.starting: "⚙️ Starting Python environment and dependency setup..."
doc_parser.install.wait_hint: "This may take a few minutes, please wait..."
doc_parser.install.path_issues: "⚠️ Detected potential path issues:"
doc_parser.install.auto_fixing: "\U0001F527 Attempting to auto-fix issues..."
doc_parser.install.fixed: "✅ Fixed the following issues:"
doc_parser.install.cannot_auto_fix: "Cannot auto-fix, please manually resolve the above issues"
doc_parser.install.fix_failed: "❌ Auto-fix failed: %{error}"
doc_parser.install.manual_fix_hint: "\U0001F4A1 Manual fix suggestions:"
doc_parser.install.python_setup_complete: "✅ Python environment setup complete!"
doc_parser.install.python_setup_failed: "❌ Python environment setup failed: %{error}"
doc_parser.install.detailed_hints: "\U0001F4A1 Detailed troubleshooting suggestions:"
doc_parser.install.diagnostic_commands: "\U0001F50D Diagnostic commands:"
doc_parser.install.check_env: "Check environment status: document-parser check"
doc_parser.install.view_logs: "View detailed logs: check logs/ directory"
# Verification
doc_parser.verify.starting: "\U0001F50D Verifying installation results..."
doc_parser.verify.complete: "Verification complete:"
doc_parser.verify.version: "Version: %{version}"
doc_parser.verify.partial_issues: "⚠️ Some dependencies may have installation issues"
doc_parser.verify.need_fix: "\U0001F527 Issues to resolve:"
doc_parser.verify.suggestion: "Suggestion: %{suggestion}"
doc_parser.verify.init_incomplete: "Environment initialization incomplete"
doc_parser.verify.failed: "❌ Verification failed: %{error}"
# Success messages
doc_parser.success.init_complete: "\U0001F389 uv environment initialization complete!"
doc_parser.success.all_ready: "✨ All dependencies are ready, you can now start the server"
doc_parser.success.venv_activate: "\U0001F4CB Virtual environment activation commands:"
doc_parser.success.start_server: "\U0001F680 Start server:"
doc_parser.success.use_uv: "\U0001F527 Or use uv to run commands directly:"
doc_parser.success.more_help: "\U0001F4DA More help:"
doc_parser.success.tips: "\U0001F4A1 Tips:"
doc_parser.success.venv_location: "Virtual environment location: ./venv/"
doc_parser.success.python_path: "Python executable: ./venv/bin/python (Linux/macOS) or .\\venv\\Scripts\\python.exe (Windows)"
doc_parser.success.troubleshoot_hint: "If issues occur, run 'document-parser troubleshoot' for detailed guide"
# troubleshoot command
doc_parser.troubleshoot.title: "\U0001F527 Document Parser Troubleshooting Guide"
doc_parser.troubleshoot.env_overview: "\U0001F4CA Current Environment Overview:"
doc_parser.troubleshoot.work_dir: "Working directory: %{path}"
doc_parser.troubleshoot.venv_path: "Virtual environment: ./venv/"
doc_parser.troubleshoot.os: "Operating system: %{os}"
# Virtual environment issues
doc_parser.troubleshoot.venv_problems: "\U0001F3E0 1. Virtual Environment Issues"
doc_parser.troubleshoot.venv_create_failed: "❓ Issue: Virtual environment creation failed"
doc_parser.troubleshoot.diagnostic_steps: "\U0001F50D Diagnostic steps:"
doc_parser.troubleshoot.solutions: "\U0001F4A1 Solutions:"
doc_parser.troubleshoot.venv_activate_failed: "❓ Issue: Virtual environment activation failed"
# Dependency installation issues
doc_parser.troubleshoot.deps_problems: "\U0001F4E6 2. Dependency Installation Issues"
doc_parser.troubleshoot.uv_not_installed: "❓ Issue: UV tool not installed or unavailable"
doc_parser.troubleshoot.mineru_markitdown_failed: "❓ Issue: MinerU or MarkItDown installation failed"
# Network issues
doc_parser.troubleshoot.network_problems: "\U0001F310 3. Network and Download Issues"
doc_parser.troubleshoot.network_timeout: "❓ Issue: Network connection timeout or download failed"
# System environment issues
doc_parser.troubleshoot.system_problems: "⚙️ 4. System Environment Issues"
doc_parser.troubleshoot.python_incompatible: "❓ Issue: Python version incompatible"
doc_parser.troubleshoot.cuda_config: "❓ Issue: CUDA environment configuration (optional, for GPU acceleration)"
# Diagnostic commands
doc_parser.troubleshoot.diagnostic_commands: "\U0001F50D 5. Common Diagnostic Commands"
doc_parser.troubleshoot.env_check: "Environment check:"
doc_parser.troubleshoot.manual_verify: "Manual verification:"
doc_parser.troubleshoot.view_logs: "Log viewing:"
# Get help
doc_parser.troubleshoot.more_help: "\U0001F198 6. Get More Help"
doc_parser.troubleshoot.if_unsolved: "If the above methods don't resolve the issue:"
# Real-time diagnosis
doc_parser.troubleshoot.realtime_diagnosis: "\U0001F52C Real-time Environment Diagnosis"
doc_parser.troubleshoot.env_good: "✅ Environment status good, all dependencies ready"
doc_parser.troubleshoot.issues_found: "⚠️ Found the following issues:"
doc_parser.troubleshoot.env_check_failed: "❌ Environment check failed: %{error}"
doc_parser.troubleshoot.follow_guide: "Please follow the above guide for troubleshooting"
# Tip
doc_parser.troubleshoot.tip: "\U0001F4A1 Tip: Most issues can be resolved by re-running 'document-parser uv-init'"
# Background cleanup
doc_parser.background.cleanup_failed: "Failed to cleanup expired data: %{error}"
doc_parser.background.cleanup_complete: "Background cleanup task completed"
# Signal handling
doc_parser.signal.ctrl_c_failed: "Failed to listen for Ctrl+C signal"
doc_parser.signal.terminate_failed: "Failed to listen for terminate signal"
# ===========================================
# Common Messages
# ===========================================
common.yes: "Yes"
common.no: "No"
common.success: "Success"
common.failed: "Failed"
common.error: "Error"
common.warning: "Warning"
common.info: "Info"
common.loading: "Loading..."
common.please_wait: "Please wait..."
common.retry: "Retry"
common.cancel: "Cancel"
common.confirm: "Confirm"
common.back: "Back"
common.next: "Next"
common.previous: "Previous"
common.done: "Done"
common.skip: "Skip"

View File

@@ -0,0 +1,468 @@
# ===========================================
# 错误消息 - mcp-proxy
# ===========================================
errors.mcp_proxy.service_not_found: "服务 %{service} 未找到"
errors.mcp_proxy.service_restart_cooldown: "服务 %{service} 在重启冷却期内,请稍后再试"
errors.mcp_proxy.service_startup_in_progress: "服务 %{service} 正在启动中,请稍后再试"
errors.mcp_proxy.service_startup_failed: "服务启动失败: %{mcp_id}: %{reason}"
errors.mcp_proxy.backend_connection: "后端连接错误: %{detail}"
errors.mcp_proxy.config_parse: "配置解析错误: %{detail}"
errors.mcp_proxy.mcp_server_error: "MCP 服务器错误: %{detail}"
errors.mcp_proxy.json_serialization: "JSON 序列化错误: %{detail}"
errors.mcp_proxy.io_error: "IO 错误: %{detail}"
errors.mcp_proxy.route_not_found: "路由未找到: %{path}"
errors.mcp_proxy.invalid_parameter: "无效的请求参数: %{detail}"
# ===========================================
# 错误消息 - document-parser
# ===========================================
errors.document_parser.config: "配置错误: %{detail}"
errors.document_parser.file: "文件操作错误: %{detail}"
errors.document_parser.unsupported_format: "不支持的文件格式: %{format}"
errors.document_parser.parse: "解析错误: %{detail}"
errors.document_parser.mineru: "MinerU错误: %{detail}"
errors.document_parser.markitdown: "MarkItDown错误: %{detail}"
errors.document_parser.oss: "OSS操作错误: %{detail}"
errors.document_parser.database: "数据库错误: %{detail}"
errors.document_parser.network: "网络错误: %{detail}"
errors.document_parser.task: "任务错误: %{detail}"
errors.document_parser.internal: "内部错误: %{detail}"
errors.document_parser.timeout: "操作超时: %{detail}"
errors.document_parser.validation: "验证错误: %{detail}"
errors.document_parser.environment: "环境错误: %{detail}"
errors.document_parser.virtual_environment_path: "虚拟环境路径错误: %{detail}"
errors.document_parser.permission: "权限错误: %{detail}"
errors.document_parser.path: "路径错误: %{detail}"
errors.document_parser.queue: "队列错误: %{detail}"
errors.document_parser.processing: "处理错误: %{detail}"
# 错误建议 - document-parser
errors.document_parser.suggestions.config: "检查配置文件和环境变量"
errors.document_parser.suggestions.file: "检查文件路径和权限"
errors.document_parser.suggestions.unsupported_format: "检查文件格式是否支持"
errors.document_parser.suggestions.parse: "检查文件内容是否完整"
errors.document_parser.suggestions.mineru: "检查MinerU环境配置"
errors.document_parser.suggestions.markitdown: "检查MarkItDown环境配置"
errors.document_parser.suggestions.oss: "检查OSS配置和网络连接"
errors.document_parser.suggestions.database: "检查数据库连接和权限"
errors.document_parser.suggestions.network: "检查网络连接和防火墙设置"
errors.document_parser.suggestions.task: "检查任务参数和状态"
errors.document_parser.suggestions.internal: "联系技术支持"
errors.document_parser.suggestions.timeout: "检查网络延迟或增加超时时间"
errors.document_parser.suggestions.validation: "检查输入参数格式"
errors.document_parser.suggestions.environment: "检查系统环境和依赖安装"
errors.document_parser.suggestions.queue: "检查队列服务状态和配置"
errors.document_parser.suggestions.processing: "检查处理流程和数据格式"
errors.document_parser.suggestions.virtual_environment_path: "检查虚拟环境路径和目录权限"
errors.document_parser.suggestions.permission: "检查文件和目录权限设置"
errors.document_parser.suggestions.path: "检查路径是否存在和可访问"
# ===========================================
# 错误消息 - oss-client
# ===========================================
errors.oss.config: "配置错误: %{detail}"
errors.oss.network: "网络错误: %{detail}"
errors.oss.file_not_found: "文件不存在: %{path}"
errors.oss.permission: "权限不足: %{detail}"
errors.oss.io: "IO错误: %{detail}"
errors.oss.sdk: "OSS SDK错误: %{detail}"
errors.oss.file_size_exceeded: "文件大小超出限制: %{detail}"
errors.oss.unsupported_file_type: "不支持的文件类型: %{detail}"
errors.oss.timeout: "操作超时: %{detail}"
errors.oss.invalid_parameter: "无效的参数: %{detail}"
# ===========================================
# 错误消息 - voice-cli
# ===========================================
errors.voice.config: "配置错误: %{detail}"
errors.voice.audio_processing: "音频处理错误: %{detail}"
errors.voice.transcription: "转录错误: %{detail}"
errors.voice.model: "模型错误: %{detail}"
errors.voice.file_io: "文件I/O错误: %{detail}"
errors.voice.http: "HTTP请求错误: %{detail}"
errors.voice.serialization: "序列化错误: %{detail}"
errors.voice.json: "JSON错误: %{detail}"
errors.voice.config_rs: "配置读取错误: %{detail}"
errors.voice.daemon: "守护进程错误: %{detail}"
errors.voice.unsupported_format: "不支持的音频格式: %{detail}"
errors.voice.file_too_large: "文件过大: %{size} 字节 (最大: %{max} 字节)"
errors.voice.model_not_found: "模型未找到: %{model}"
errors.voice.invalid_model_name: "无效的模型名称: %{model}"
errors.voice.worker_pool: "工作池错误: %{detail}"
errors.voice.transcription_timeout: "转录超时 (%{seconds} 秒后)"
errors.voice.transcription_failed: "转录失败: %{detail}"
errors.voice.audio_conversion_failed: "音频转换失败: %{detail}"
errors.voice.audio_probe_error: "音频探测错误: %{detail}"
errors.voice.temp_file_error: "临时文件错误: %{detail}"
errors.voice.multipart_error: "Multipart表单错误: %{detail}"
errors.voice.missing_field: "缺少必填字段: %{field}"
errors.voice.network: "网络错误: %{detail}"
errors.voice.storage: "存储错误: %{detail}"
errors.voice.task_management_disabled: "任务管理已禁用"
errors.voice.not_found: "资源未找到: %{resource}"
errors.voice.initialization: "初始化错误: %{detail}"
errors.voice.tts: "TTS错误: %{detail}"
errors.voice.invalid_input: "无效输入: %{detail}"
errors.voice.io: "IO错误: %{detail}"
# ===========================================
# CLI 消息 - mcp-proxy 启动
# ===========================================
cli.mirror.not_configured: "未配置镜像源npm/PyPI将使用默认源"
cli.mirror.npm: "npm 镜像: %{url}"
cli.mirror.pypi: "PyPI 镜像: %{url}"
cli.startup.service_starting: "MCP-Proxy 启动中..."
cli.startup.version: "版本: %{version}"
cli.startup.config_loaded: "配置加载完成"
cli.startup.port: "端口: %{port}"
cli.startup.log_dir: "日志目录: %{path}"
cli.startup.log_level: "日志级别: %{level}"
cli.startup.log_retain_days: "日志保留天数: %{days}"
cli.startup.success: "✅ 服务启动成功,监听地址: %{addr}"
cli.startup.health_endpoint: "✅ 健康检查端点: http://%{addr}/health"
cli.startup.mcp_list: "✅ MCP 服务列表: http://%{addr}/mcp"
cli.startup.schedule_task_started: "✅ MCP服务状态检查定时任务已启动"
cli.startup.log_rotation_configured: "✅ 日志自动轮转已配置(保留最近 %{count} 个日志文件)"
cli.startup.system_info: "系统信息:"
cli.startup.os: "操作系统: %{os}"
cli.startup.arch: "架构: %{arch}"
cli.startup.work_dir: "工作目录: %{path}"
cli.startup.env_override: "环境变量覆盖:"
cli.startup.env_port_override: "MCP_PROXY_PORT: %{port}"
cli.startup.env_log_dir_override: "MCP_PROXY_LOG_DIR: %{dir}"
cli.startup.env_log_level_override: "MCP_PROXY_LOG_LEVEL: %{level}"
cli.startup.trying_bind: "尝试绑定到地址: %{addr}"
cli.startup.bind_success: "成功绑定到地址: %{addr}"
cli.startup.bind_failed: "绑定地址 %{addr} 失败: %{error}"
cli.startup.init_state: "初始化应用状态..."
cli.startup.state_done: "应用状态初始化完成"
cli.startup.init_router: "初始化路由..."
cli.startup.router_done: "路由初始化完成"
cli.startup.warming_up: "\U0001F504 开始预热 uv/deno 环境依赖..."
cli.startup.warmup_success: "✅ 预热 uv/deno 环境依赖完成"
cli.startup.warmup_failed: "❌ 预热 uv/deno 环境依赖失败: %{error}"
cli.startup.http_server_starting: "\U0001F680 HTTP 服务器启动,等待连接..."
cli.startup.proxy_mode: "命令: proxy (HTTP 服务器模式)"
cli.startup.config_info: "配置信息:"
cli.startup.listen_port: "监听端口: %{port}"
cli.startup.log_retention: "日志保留: %{days} 天"
# CLI 消息 - mcp-proxy 关闭
cli.shutdown.signal_received: "⚠️ 服务器收到关闭信号,开始清理资源..."
cli.shutdown.cleanup_success: "✅ 资源清理成功"
cli.shutdown.cleanup_failed: "❌ 清理资源时出错: %{error}"
cli.shutdown.complete: "✅ 资源清理完成,服务已完全关闭"
cli.shutdown.service_error: "❌ 服务运行错误: %{error}"
# CLI 消息 - panic 处理
cli.panic.handler_started: "程序发生panic执行清理..."
cli.panic.reason: "Panic 原因: %{reason}"
cli.panic.reason_unknown: "Panic 原因: 未知"
cli.panic.location: "Panic 位置: %{file}:%{line}"
cli.panic.stack_trace: "堆栈跟踪:"
# ===========================================
# CLI 消息 - convert 模式
# ===========================================
cli.convert.starting: "开始 URL 模式处理"
cli.convert.target_url: "目标 URL: %{url}"
cli.convert.protocol_specified: "使用命令行指定协议: %{protocol}"
cli.convert.protocol_config: "使用配置文件协议: %{protocol}"
cli.convert.detecting_protocol: "开始自动检测协议..."
cli.convert.detect_failed: "协议检测失败: %{error}"
cli.convert.using_protocol: "使用 %{protocol} 协议模式"
cli.convert.stdio_url_not_supported: "Stdio 协议不支持通过 URL 转换"
cli.convert.tool_whitelist: "工具白名单: %{tools}"
cli.convert.tool_blacklist: "工具黑名单: %{tools}"
cli.convert.config_parsed: "配置解析成功"
cli.convert.service_name: "MCP 服务名称: %{name}"
cli.convert.service_name_not_specified: "MCP 服务名称: 未指定(使用 direct URL"
cli.convert.cli_starting: "MCP-Proxy CLI 启动"
cli.convert.command: "命令: convert (stdio 桥接模式)"
cli.convert.version: "版本: %{version}"
cli.convert.diagnostic_mode: "诊断模式: %{enabled}"
cli.convert.mode_direct_url: "模式: 直接 URL 模式"
cli.convert.mode_remote_service: "模式: 远程服务配置模式"
cli.convert.service_url: "服务 URL: %{url}"
cli.convert.config_protocol: "配置协议: %{protocol}"
cli.convert.ping_config: "Ping 间隔: %{interval}s, Ping 超时: %{timeout}s"
cli.convert.connecting_backend: "开始连接到后端服务 (超时: %{timeout}s)..."
cli.convert.detect_complete: "协议检测完成: %{protocol} (耗时: %{duration})"
# ===========================================
# CLI 消息 - SSE 模式
# ===========================================
cli.sse.mode_starting: "SSE 模式启动"
cli.sse.connect_timeout: "连接后端超时 (%{seconds}s)"
cli.sse.connect_failed: "连接后端失败: %{error}"
cli.sse.connect_success: "后端连接成功 (耗时: %{duration})"
cli.sse.stdio_starting: "启动 stdio server..."
cli.sse.stdio_started: "stdio server 已启动"
cli.sse.waiting_events: "开始等待 stdio server 事件..."
cli.sse.stdio_exit_eof: "stdio server 退出 - 原因: MCP 客户端断开连接 (stdin EOF)"
cli.sse.watchdog_exit: "Watchdog 任务退出"
cli.sse.normal_exit: "mcp-proxy convert (SSE 模式) 正常退出"
cli.sse.watchdog_starting: "SSE Watchdog 启动"
cli.sse.max_retries: "最大重试次数: %{count} (0=无限)"
cli.sse.monitoring_connection: "开始监控初始连接..."
cli.sse.initial_disconnect: "初始连接断开: %{reason}"
cli.sse.connection_alive: "初始连接存活时长: %{seconds}s"
cli.sse.reconnect_attempt: "重连尝试 #%{attempt}/%{max}"
cli.sse.backoff_time: "退避时间: %{seconds}s"
cli.sse.reconnect_success: "重连成功 (耗时: %{duration})"
cli.sse.monitoring_reconnect: "开始监控重连后的连接..."
cli.sse.reconnect_disconnect: "重连后断开: %{reason}"
cli.sse.reconnect_alive: "重连后存活时长: %{seconds}s"
cli.sse.max_retries_reached: "达到最大重试次数 (%{count}), 停止重连"
cli.sse.watchdog_exit_msg: "SSE Watchdog 退出"
# ===========================================
# CLI 消息 - Stream 模式
# ===========================================
cli.stream.mode_starting: "Stream 模式启动"
cli.stream.connect_timeout: "连接后端超时 (%{seconds}s)"
cli.stream.connect_failed: "连接后端失败: %{error}"
cli.stream.connect_success: "后端连接成功 (耗时: %{duration})"
cli.stream.stdio_starting: "启动 stdio server..."
cli.stream.stdio_started: "stdio server 已启动"
cli.stream.waiting_events: "开始等待 stdio server 事件..."
cli.stream.stdio_exit_eof: "stdio server 退出 - 原因: MCP 客户端断开连接 (stdin EOF)"
cli.stream.watchdog_exit: "Watchdog 任务退出"
cli.stream.normal_exit: "mcp-proxy convert (Stream 模式) 正常退出"
# ===========================================
# CLI 消息 - Command 模式
# ===========================================
cli.command.local_mode: "模式: 本地命令模式"
cli.command.command: "命令: %{cmd} %{args}"
cli.command.ctrl_c: "收到 Ctrl+C 信号,正在关闭..."
# ===========================================
# CLI 消息 - 监控
# ===========================================
cli.monitoring.health: "开始监控 %{protocol} 连接健康状态"
cli.monitoring.health_check_status: "\U0001F493 [%{protocol}][健康检查] 连接状态: %{status} (仅检查连接通道, 未调用 list_tools), 检查 #%{count}, 已存活: %{seconds}s"
cli.monitoring.health_check_normal: "\U0001F493 [%{protocol}][健康检查] 后端服务正常 (list_tools 验证通过), Ping #%{count}, 已存活: %{seconds}s"
# ===========================================
# 诊断消息
# ===========================================
diagnostic.report_header: "========== 诊断报告 =========="
diagnostic.connection_protocol: "连接协议: %{protocol}"
diagnostic.service_url: "服务 URL: %{url}"
diagnostic.connection_duration: "连接存活时长: %{seconds} 秒"
diagnostic.disconnect_reason: "断开原因: %{reason}"
diagnostic.error_type: "错误类型: %{error_type}"
diagnostic.possible_causes: "可能原因分析:"
diagnostic.suggestions: "建议:"
# 诊断 - 30秒超时分析
diagnostic.analysis.timeout_30s: "⚠️ 连接在约 30 秒时断开,极有可能是:"
diagnostic.analysis.timeout_30s_cause1: "服务器端设置了 30 秒超时限制"
diagnostic.analysis.timeout_30s_cause2: "负载均衡器(如 Nginx/ALB的默认超时"
diagnostic.analysis.timeout_30s_cause3: "云服务商的网关超时限制"
# 诊断 - 快速断开分析
diagnostic.analysis.quick_disconnect: "⚠️ 连接很快断开(%{seconds}秒),可能是:"
diagnostic.analysis.quick_disconnect_cause1: "认证失败或 token 无效"
diagnostic.analysis.quick_disconnect_cause2: "服务器拒绝连接"
diagnostic.analysis.quick_disconnect_cause3: "网络不稳定"
# 诊断 - 长时间连接分析
diagnostic.analysis.long_connection: "✅ 连接保持了较长时间(%{seconds}秒),可能是:"
diagnostic.analysis.long_connection_cause1: "工具调用执行时间过长"
diagnostic.analysis.long_connection_cause2: "网络波动导致断开"
# 诊断建议
diagnostic.suggestion.timeout_30s: "联系服务提供商增加超时限制"
diagnostic.suggestion.timeout_client: "使用 --request-timeout 参数设置客户端超时"
diagnostic.suggestion.async_mode: "考虑使用异步处理模式webhook 回调)"
diagnostic.suggestion.ping_interval: "尝试增加 ping 间隔: --ping-interval %{seconds}"
diagnostic.suggestion.ping_timeout: "增加 ping 超时时间: --ping-timeout %{seconds}"
diagnostic.suggestion.ping_disable: "或禁用 ping: --ping-interval 0"
# ===========================================
# 错误分类
# ===========================================
error_classify.timeout_30s: "30秒超时可能是服务器限制"
error_classify.service_unavailable_503: "服务不可用(503)"
error_classify.internal_server_error_500: "服务器内部错误(500)"
error_classify.bad_gateway_502: "网关错误(502)"
error_classify.gateway_timeout_504: "网关超时(504)"
error_classify.unauthorized_401: "未授权(401)"
error_classify.forbidden_403: "禁止访问(403)"
error_classify.not_found_404: "资源未找到(404)"
error_classify.request_timeout_408: "请求超时(408)"
error_classify.timeout: "超时"
error_classify.connection_refused: "连接被拒绝"
error_classify.connection_reset: "连接被重置"
error_classify.connection_closed: "连接关闭"
error_classify.dns_failed: "DNS解析失败"
error_classify.ssl_tls_error: "SSL/TLS错误"
error_classify.network_error: "网络错误"
error_classify.session_error: "会话错误"
error_classify.unknown_error: "未知错误"
# ===========================================
# CLI 消息 - health 命令
# ===========================================
cli.health.checking: "\U0001F50D 健康检查服务: %{url}"
cli.health.using_protocol: "\U0001F50D 使用指定协议: %{protocol}"
cli.health.detecting_protocol: "\U0001F50D 正在检测协议..."
cli.health.detected_protocol: "\U0001F50D 检测到 %{protocol} 协议"
cli.health.healthy: "✅ 服务健康"
cli.health.unhealthy: "❌ 服务不健康"
cli.health.stdio_not_supported: "health 命令不支持 stdio 协议"
# ===========================================
# CLI 消息 - check 命令
# ===========================================
cli.check.checking: "\U0001F50D 检查服务: %{url}"
cli.check.healthy: "✅ 服务正常,检测到 %{protocol} 协议"
cli.check.failed: "❌ 服务检查失败: %{error}"
# ===========================================
# CLI 消息 - document-parser
# ===========================================
doc_parser.startup.app_info: "=== %{name} v%{version} 启动 ==="
doc_parser.startup.config_summary: "配置摘要: %{summary}"
doc_parser.startup.listening: "服务监听地址: %{host}:%{port}"
doc_parser.startup.checking_python: "开始检查和初始化Python环境..."
doc_parser.startup.checking_venv: "检查并激活虚拟环境..."
doc_parser.startup.venv_activate_failed: "虚拟环境自动激活失败: %{error}"
doc_parser.startup.venv_activate_manual: "请手动激活虚拟环境: source ./venv/bin/activate"
doc_parser.startup.venv_activated: "虚拟环境已自动激活"
doc_parser.startup.env_check_failed: "环境检查失败,将在后台自动安装: %{error}"
doc_parser.startup.mineru_not_installed: "MinerU依赖未安装开始后台自动安装..."
doc_parser.startup.markitdown_not_installed: "MarkItDown依赖未安装开始后台自动安装..."
doc_parser.startup.install_complete: "后台Python环境安装完成"
doc_parser.startup.install_failed: "后台Python环境安装失败: %{error}"
doc_parser.startup.install_task_started: "Python依赖安装任务已启动后台进行服务将正常启动"
doc_parser.startup.mineru_ready: "MinerU依赖已安装版本: %{version}"
doc_parser.startup.markitdown_ready: "MarkItDown依赖已安装"
doc_parser.startup.python_ready: "Python环境检查完成所有依赖已就绪"
doc_parser.startup.state_failed: "无法创建应用状态: %{error}"
doc_parser.startup.health_check_failed: "应用健康检查失败: %{error}"
doc_parser.startup.state_ready: "应用状态初始化成功"
doc_parser.startup.http_routes_ready: "HTTP路由初始化成功"
doc_parser.startup.background_tasks_started: "后台任务已启动"
doc_parser.startup.service_ready: "服务启动成功,开始监听连接..."
doc_parser.shutdown.service_stopped: "服务已关闭"
doc_parser.shutdown.ctrl_c_received: "收到 Ctrl+C 信号,开始优雅关闭..."
doc_parser.shutdown.terminate_received: "收到 terminate 信号,开始优雅关闭..."
doc_parser.shutdown.closing: "正在关闭服务..."
# uv-init 命令
doc_parser.uv_init.starting: "\U0001F680 开始在当前目录初始化uv虚拟环境和依赖..."
doc_parser.uv_init.current_dir: "\U0001F4C1 当前工作目录: %{path}"
doc_parser.uv_init.venv_path: "\U0001F4C1 虚拟环境将创建在: ./venv/"
doc_parser.uv_init.validating_dir: "\U0001F50D 验证当前目录设置..."
doc_parser.uv_init.dir_valid: " ✅ 目录验证通过"
doc_parser.uv_init.warnings_found: " ⚠️ 发现 %{count} 个警告"
doc_parser.uv_init.dir_invalid: " ❌ 目录验证失败,发现 %{count} 个问题"
doc_parser.uv_init.auto_fixing: " \U0001F527 尝试自动修复 %{count} 个问题..."
doc_parser.uv_init.fix_success: " ✅ %{message}"
doc_parser.uv_init.fix_failed: " ❌ 清理失败: %{error}"
doc_parser.uv_init.manual_fix_hint: " \U0001F4A1 请手动解决以下问题:"
doc_parser.uv_init.dir_validation_failed: "目录验证失败,请解决上述问题后重试"
doc_parser.uv_init.dir_validation_error: " ⚠️ 目录验证失败: %{error}"
doc_parser.uv_init.continue_install: " 继续进行安装,但可能遇到问题..."
# 环境检查
doc_parser.check.checking_env: "\U0001F50D 检查当前环境状态..."
doc_parser.check.env_check_complete: " 环境检查完成:"
doc_parser.check.python_available: "✅ 可用"
doc_parser.check.python_unavailable: "❌ 不可用"
doc_parser.check.uv_available: "✅ 可用"
doc_parser.check.uv_unavailable: "❌ 不可用"
doc_parser.check.venv_active: "✅ 激活"
doc_parser.check.venv_inactive: "❌ 未激活"
doc_parser.check.mineru_available: "✅ 可用"
doc_parser.check.mineru_unavailable: "❌ 不可用"
doc_parser.check.markitdown_available: "✅ 可用"
doc_parser.check.markitdown_unavailable: "❌ 不可用"
doc_parser.check.env_check_failed: " ⚠️ 环境检查失败: %{error}"
doc_parser.check.continue_install: " 继续进行安装..."
# 安装过程
doc_parser.install.all_ready: "✨ 所有依赖都已就绪,无需安装!"
doc_parser.install.plan: "\U0001F4CB 安装计划:"
doc_parser.install.install_uv: "安装 uv 工具"
doc_parser.install.create_venv: "创建虚拟环境 (./venv/)"
doc_parser.install.install_mineru: "安装 MinerU 依赖"
doc_parser.install.install_markitdown: "安装 MarkItDown 依赖"
doc_parser.install.starting: "⚙️ 开始设置Python环境和依赖..."
doc_parser.install.wait_hint: "这可能需要几分钟时间,请耐心等待..."
doc_parser.install.path_issues: "⚠️ 检测到潜在的路径问题:"
doc_parser.install.auto_fixing: "\U0001F527 尝试自动修复问题..."
doc_parser.install.fixed: "✅ 已修复以下问题:"
doc_parser.install.cannot_auto_fix: "无法自动修复,请手动解决上述问题"
doc_parser.install.fix_failed: "❌ 自动修复失败: %{error}"
doc_parser.install.manual_fix_hint: "\U0001F4A1 手动修复建议:"
doc_parser.install.python_setup_complete: "✅ Python环境设置完成"
doc_parser.install.python_setup_failed: "❌ Python环境设置失败: %{error}"
doc_parser.install.detailed_hints: "\U0001F4A1 详细故障排除建议:"
doc_parser.install.diagnostic_commands: "\U0001F50D 诊断命令:"
doc_parser.install.check_env: "检查环境状态: document-parser check"
doc_parser.install.view_logs: "查看详细日志: 检查 logs/ 目录"
# 验证
doc_parser.verify.starting: "\U0001F50D 验证安装结果..."
doc_parser.verify.complete: "验证完成:"
doc_parser.verify.version: "版本: %{version}"
doc_parser.verify.partial_issues: "⚠️ 部分依赖安装可能存在问题"
doc_parser.verify.need_fix: "\U0001F527 需要解决的问题:"
doc_parser.verify.suggestion: "建议: %{suggestion}"
doc_parser.verify.init_incomplete: "环境初始化未完全成功"
doc_parser.verify.failed: "❌ 验证失败: %{error}"
# 成功消息
doc_parser.success.init_complete: "\U0001F389 uv环境初始化完成"
doc_parser.success.all_ready: "✨ 所有依赖都已就绪,现在可以启动服务器了"
doc_parser.success.venv_activate: "\U0001F4CB 虚拟环境激活指令:"
doc_parser.success.start_server: "\U0001F680 启动服务器:"
doc_parser.success.use_uv: "\U0001F527 或者使用 uv 直接运行命令:"
doc_parser.success.more_help: "\U0001F4DA 更多帮助:"
doc_parser.success.tips: "\U0001F4A1 提示:"
doc_parser.success.venv_location: "虚拟环境位置: ./venv/"
doc_parser.success.python_path: "Python可执行文件: ./venv/bin/python (Linux/macOS) 或 .\\venv\\Scripts\\python.exe (Windows)"
doc_parser.success.troubleshoot_hint: "如遇问题,请运行 'document-parser troubleshoot' 查看详细指南"
# troubleshoot 命令
doc_parser.troubleshoot.title: "\U0001F527 Document Parser 故障排除指南"
doc_parser.troubleshoot.env_overview: "\U0001F4CA 当前环境概览:"
doc_parser.troubleshoot.work_dir: "工作目录: %{path}"
doc_parser.troubleshoot.venv_path: "虚拟环境: ./venv/"
doc_parser.troubleshoot.os: "操作系统: %{os}"
# 虚拟环境问题
doc_parser.troubleshoot.venv_problems: "\U0001F3E0 1. 虚拟环境问题"
doc_parser.troubleshoot.venv_create_failed: "❓ 问题: 虚拟环境创建失败"
doc_parser.troubleshoot.diagnostic_steps: "\U0001F50D 诊断步骤:"
doc_parser.troubleshoot.solutions: "\U0001F4A1 解决方案:"
doc_parser.troubleshoot.venv_activate_failed: "❓ 问题: 虚拟环境激活失败"
# 依赖安装问题
doc_parser.troubleshoot.deps_problems: "\U0001F4E6 2. 依赖安装问题"
doc_parser.troubleshoot.uv_not_installed: "❓ 问题: UV工具未安装或不可用"
doc_parser.troubleshoot.mineru_markitdown_failed: "❓ 问题: MinerU或MarkItDown安装失败"
# 网络问题
doc_parser.troubleshoot.network_problems: "\U0001F310 3. 网络和下载问题"
doc_parser.troubleshoot.network_timeout: "❓ 问题: 网络连接超时或下载失败"
# 系统环境问题
doc_parser.troubleshoot.system_problems: "⚙️ 4. 系统环境问题"
doc_parser.troubleshoot.python_incompatible: "❓ 问题: Python版本不兼容"
doc_parser.troubleshoot.cuda_config: "❓ 问题: CUDA环境配置 (可选用于GPU加速)"
# 诊断命令
doc_parser.troubleshoot.diagnostic_commands: "\U0001F50D 5. 常用诊断命令"
doc_parser.troubleshoot.env_check: "环境检查:"
doc_parser.troubleshoot.manual_verify: "手动验证:"
doc_parser.troubleshoot.view_logs: "日志查看:"
# 获取帮助
doc_parser.troubleshoot.more_help: "\U0001F198 6. 获取更多帮助"
doc_parser.troubleshoot.if_unsolved: "如果上述方法都无法解决问题,请:"
# 实时诊断
doc_parser.troubleshoot.realtime_diagnosis: "\U0001F52C 实时环境诊断"
doc_parser.troubleshoot.env_good: "✅ 环境状态良好,所有依赖都已就绪"
doc_parser.troubleshoot.issues_found: "⚠️ 发现以下问题:"
doc_parser.troubleshoot.env_check_failed: "❌ 环境检查失败: %{error}"
doc_parser.troubleshoot.follow_guide: "请按照上述指南进行故障排除"
# 提示
doc_parser.troubleshoot.tip: "\U0001F4A1 提示: 大多数问题可以通过重新运行 'document-parser uv-init' 解决"
# 后台清理
doc_parser.background.cleanup_failed: "清理过期数据失败: %{error}"
doc_parser.background.cleanup_complete: "后台清理任务执行完成"
# 信号处理
doc_parser.signal.ctrl_c_failed: "无法监听 Ctrl+C 信号"
doc_parser.signal.terminate_failed: "无法监听 terminate 信号"
# ===========================================
# 通用消息
# ===========================================
common.yes: "是"
common.no: "否"
common.success: "成功"
common.failed: "失败"
common.error: "错误"
common.warning: "警告"
common.info: "信息"
common.loading: "加载中..."
common.please_wait: "请稍候..."
common.retry: "重试"
common.cancel: "取消"
common.confirm: "确认"
common.back: "返回"
common.next: "下一步"
common.previous: "上一步"
common.done: "完成"
common.skip: "跳过"

View File

@@ -0,0 +1,468 @@
# ===========================================
# 錯誤訊息 - mcp-proxy
# ===========================================
errors.mcp_proxy.service_not_found: "服務 %{service} 未找到"
errors.mcp_proxy.service_restart_cooldown: "服務 %{service} 在重啟冷卻期內,請稍後再試"
errors.mcp_proxy.service_startup_in_progress: "服務 %{service} 正在啟動中,請稍後再試"
errors.mcp_proxy.service_startup_failed: "服務啟動失敗: %{mcp_id}: %{reason}"
errors.mcp_proxy.backend_connection: "後端連線錯誤: %{detail}"
errors.mcp_proxy.config_parse: "設定解析錯誤: %{detail}"
errors.mcp_proxy.mcp_server_error: "MCP 伺服器錯誤: %{detail}"
errors.mcp_proxy.json_serialization: "JSON 序列化錯誤: %{detail}"
errors.mcp_proxy.io_error: "IO 錯誤: %{detail}"
errors.mcp_proxy.route_not_found: "路由未找到: %{path}"
errors.mcp_proxy.invalid_parameter: "無效的請求參數: %{detail}"
# ===========================================
# 錯誤訊息 - document-parser
# ===========================================
errors.document_parser.config: "設定錯誤: %{detail}"
errors.document_parser.file: "檔案操作錯誤: %{detail}"
errors.document_parser.unsupported_format: "不支援的檔案格式: %{format}"
errors.document_parser.parse: "解析錯誤: %{detail}"
errors.document_parser.mineru: "MinerU錯誤: %{detail}"
errors.document_parser.markitdown: "MarkItDown錯誤: %{detail}"
errors.document_parser.oss: "OSS操作錯誤: %{detail}"
errors.document_parser.database: "資料庫錯誤: %{detail}"
errors.document_parser.network: "網路錯誤: %{detail}"
errors.document_parser.task: "任務錯誤: %{detail}"
errors.document_parser.internal: "內部錯誤: %{detail}"
errors.document_parser.timeout: "操作逾時: %{detail}"
errors.document_parser.validation: "驗證錯誤: %{detail}"
errors.document_parser.environment: "環境錯誤: %{detail}"
errors.document_parser.virtual_environment_path: "虛擬環境路徑錯誤: %{detail}"
errors.document_parser.permission: "權限錯誤: %{detail}"
errors.document_parser.path: "路徑錯誤: %{detail}"
errors.document_parser.queue: "佇列錯誤: %{detail}"
errors.document_parser.processing: "處理錯誤: %{detail}"
# 錯誤建議 - document-parser
errors.document_parser.suggestions.config: "檢查設定檔案和環境變數"
errors.document_parser.suggestions.file: "檢查檔案路徑和權限"
errors.document_parser.suggestions.unsupported_format: "檢查檔案格式是否支援"
errors.document_parser.suggestions.parse: "檢查檔案內容是否完整"
errors.document_parser.suggestions.mineru: "檢查MinerU環境設定"
errors.document_parser.suggestions.markitdown: "檢查MarkItDown環境設定"
errors.document_parser.suggestions.oss: "檢查OSS設定和網路連線"
errors.document_parser.suggestions.database: "檢查資料庫連線和權限"
errors.document_parser.suggestions.network: "檢查網路連線和防火牆設定"
errors.document_parser.suggestions.task: "檢查任務參數和狀態"
errors.document_parser.suggestions.internal: "聯絡技術支援"
errors.document_parser.suggestions.timeout: "檢查網路延遲或增加逾時時間"
errors.document_parser.suggestions.validation: "檢查輸入參數格式"
errors.document_parser.suggestions.environment: "檢查系統環境和相依套件安裝"
errors.document_parser.suggestions.queue: "檢查佇列服務狀態和設定"
errors.document_parser.suggestions.processing: "檢查處理流程和資料格式"
errors.document_parser.suggestions.virtual_environment_path: "檢查虛擬環境路徑和目錄權限"
errors.document_parser.suggestions.permission: "檢查檔案和目錄權限設定"
errors.document_parser.suggestions.path: "檢查路徑是否存在且可存取"
# ===========================================
# 錯誤訊息 - oss-client
# ===========================================
errors.oss.config: "設定錯誤: %{detail}"
errors.oss.network: "網路錯誤: %{detail}"
errors.oss.file_not_found: "檔案不存在: %{path}"
errors.oss.permission: "權限不足: %{detail}"
errors.oss.io: "IO錯誤: %{detail}"
errors.oss.sdk: "OSS SDK錯誤: %{detail}"
errors.oss.file_size_exceeded: "檔案大小超出限制: %{detail}"
errors.oss.unsupported_file_type: "不支援的檔案類型: %{detail}"
errors.oss.timeout: "操作逾時: %{detail}"
errors.oss.invalid_parameter: "無效的參數: %{detail}"
# ===========================================
# 錯誤訊息 - voice-cli
# ===========================================
errors.voice.config: "設定錯誤: %{detail}"
errors.voice.audio_processing: "音訊處理錯誤: %{detail}"
errors.voice.transcription: "轉錄錯誤: %{detail}"
errors.voice.model: "模型錯誤: %{detail}"
errors.voice.file_io: "檔案I/O錯誤: %{detail}"
errors.voice.http: "HTTP請求錯誤: %{detail}"
errors.voice.serialization: "序列化錯誤: %{detail}"
errors.voice.json: "JSON錯誤: %{detail}"
errors.voice.config_rs: "設定讀取錯誤: %{detail}"
errors.voice.daemon: "常駐程式錯誤: %{detail}"
errors.voice.unsupported_format: "不支援的音訊格式: %{detail}"
errors.voice.file_too_large: "檔案過大: %{size} 位元組 (最大: %{max} 位元組)"
errors.voice.model_not_found: "模型未找到: %{model}"
errors.voice.invalid_model_name: "無效的模型名稱: %{model}"
errors.voice.worker_pool: "工作池錯誤: %{detail}"
errors.voice.transcription_timeout: "轉錄逾時 (%{seconds} 秒後)"
errors.voice.transcription_failed: "轉錄失敗: %{detail}"
errors.voice.audio_conversion_failed: "音訊轉換失敗: %{detail}"
errors.voice.audio_probe_error: "音訊探測錯誤: %{detail}"
errors.voice.temp_file_error: "暫存檔錯誤: %{detail}"
errors.voice.multipart_error: "Multipart表單錯誤: %{detail}"
errors.voice.missing_field: "缺少必填欄位: %{field}"
errors.voice.network: "網路錯誤: %{detail}"
errors.voice.storage: "儲存錯誤: %{detail}"
errors.voice.task_management_disabled: "任務管理已停用"
errors.voice.not_found: "資源未找到: %{resource}"
errors.voice.initialization: "初始化錯誤: %{detail}"
errors.voice.tts: "TTS錯誤: %{detail}"
errors.voice.invalid_input: "無效輸入: %{detail}"
errors.voice.io: "IO錯誤: %{detail}"
# ===========================================
# CLI 訊息 - mcp-proxy 啟動
# ===========================================
cli.mirror.not_configured: "未配置鏡像源npm/PyPI將使用預設來源"
cli.mirror.npm: "npm 鏡像: %{url}"
cli.mirror.pypi: "PyPI 鏡像: %{url}"
cli.startup.service_starting: "MCP-Proxy 啟動中..."
cli.startup.version: "版本: %{version}"
cli.startup.config_loaded: "設定載入完成"
cli.startup.port: "連接埠: %{port}"
cli.startup.log_dir: "日誌目錄: %{path}"
cli.startup.log_level: "日誌級別: %{level}"
cli.startup.log_retain_days: "日誌保留天數: %{days}"
cli.startup.success: "✅ 服務啟動成功,監聽位址: %{addr}"
cli.startup.health_endpoint: "✅ 健康檢查端點: http://%{addr}/health"
cli.startup.mcp_list: "✅ MCP 服務列表: http://%{addr}/mcp"
cli.startup.schedule_task_started: "✅ MCP服務狀態檢查定時任務已啟動"
cli.startup.log_rotation_configured: "✅ 日誌自動輪轉已設定(保留最近 %{count} 個日誌檔案)"
cli.startup.system_info: "系統資訊:"
cli.startup.os: "作業系統: %{os}"
cli.startup.arch: "架構: %{arch}"
cli.startup.work_dir: "工作目錄: %{path}"
cli.startup.env_override: "環境變數覆蓋:"
cli.startup.env_port_override: "MCP_PROXY_PORT: %{port}"
cli.startup.env_log_dir_override: "MCP_PROXY_LOG_DIR: %{dir}"
cli.startup.env_log_level_override: "MCP_PROXY_LOG_LEVEL: %{level}"
cli.startup.trying_bind: "嘗試綁定到位址: %{addr}"
cli.startup.bind_success: "成功綁定到位址: %{addr}"
cli.startup.bind_failed: "綁定位址 %{addr} 失敗: %{error}"
cli.startup.init_state: "初始化應用狀態..."
cli.startup.state_done: "應用狀態初始化完成"
cli.startup.init_router: "初始化路由..."
cli.startup.router_done: "路由初始化完成"
cli.startup.warming_up: "\U0001F504 開始預熱 uv/deno 環境相依套件..."
cli.startup.warmup_success: "✅ 預熱 uv/deno 環境相依套件完成"
cli.startup.warmup_failed: "❌ 預熱 uv/deno 環境相依套件失敗: %{error}"
cli.startup.http_server_starting: "\U0001F680 HTTP 伺服器啟動,等待連線..."
cli.startup.proxy_mode: "命令: proxy (HTTP 伺服器模式)"
cli.startup.config_info: "設定資訊:"
cli.startup.listen_port: "監聽連接埠: %{port}"
cli.startup.log_retention: "日誌保留: %{days} 天"
# CLI 訊息 - mcp-proxy 關閉
cli.shutdown.signal_received: "⚠️ 伺服器收到關閉訊號,開始清理資源..."
cli.shutdown.cleanup_success: "✅ 資源清理成功"
cli.shutdown.cleanup_failed: "❌ 清理資源時出錯: %{error}"
cli.shutdown.complete: "✅ 資源清理完成,服務已完全關閉"
cli.shutdown.service_error: "❌ 服務執行錯誤: %{error}"
# CLI 訊息 - panic 處理
cli.panic.handler_started: "程式發生panic執行清理..."
cli.panic.reason: "Panic 原因: %{reason}"
cli.panic.reason_unknown: "Panic 原因: 未知"
cli.panic.location: "Panic 位置: %{file}:%{line}"
cli.panic.stack_trace: "堆疊追蹤:"
# ===========================================
# CLI 訊息 - convert 模式
# ===========================================
cli.convert.starting: "開始 URL 模式處理"
cli.convert.target_url: "目標 URL: %{url}"
cli.convert.protocol_specified: "使用命令列指定協定: %{protocol}"
cli.convert.protocol_config: "使用設定檔協定: %{protocol}"
cli.convert.detecting_protocol: "開始自動偵測協定..."
cli.convert.detect_failed: "協定偵測失敗: %{error}"
cli.convert.using_protocol: "使用 %{protocol} 協定模式"
cli.convert.stdio_url_not_supported: "Stdio 協定不支援透過 URL 轉換"
cli.convert.tool_whitelist: "工具白名單: %{tools}"
cli.convert.tool_blacklist: "工具黑名單: %{tools}"
cli.convert.config_parsed: "設定解析成功"
cli.convert.service_name: "MCP 服務名稱: %{name}"
cli.convert.service_name_not_specified: "MCP 服務名稱: 未指定(使用 direct URL"
cli.convert.cli_starting: "MCP-Proxy CLI 啟動"
cli.convert.command: "命令: convert (stdio 橋接模式)"
cli.convert.version: "版本: %{version}"
cli.convert.diagnostic_mode: "診斷模式: %{enabled}"
cli.convert.mode_direct_url: "模式: 直接 URL 模式"
cli.convert.mode_remote_service: "模式: 遠端服務設定模式"
cli.convert.service_url: "服務 URL: %{url}"
cli.convert.config_protocol: "設定協定: %{protocol}"
cli.convert.ping_config: "Ping 間隔: %{interval}s, Ping 逾時: %{timeout}s"
cli.convert.connecting_backend: "開始連線到後端服務 (逾時: %{timeout}s)..."
cli.convert.detect_complete: "協定偵測完成: %{protocol} (耗時: %{duration})"
# ===========================================
# CLI 訊息 - SSE 模式
# ===========================================
cli.sse.mode_starting: "SSE 模式啟動"
cli.sse.connect_timeout: "連線後端逾時 (%{seconds}s)"
cli.sse.connect_failed: "連線後端失敗: %{error}"
cli.sse.connect_success: "後端連線成功 (耗時: %{duration})"
cli.sse.stdio_starting: "啟動 stdio server..."
cli.sse.stdio_started: "stdio server 已啟動"
cli.sse.waiting_events: "開始等待 stdio server 事件..."
cli.sse.stdio_exit_eof: "stdio server 結束 - 原因: MCP 客戶端斷開連線 (stdin EOF)"
cli.sse.watchdog_exit: "Watchdog 任務結束"
cli.sse.normal_exit: "mcp-proxy convert (SSE 模式) 正常結束"
cli.sse.watchdog_starting: "SSE Watchdog 啟動"
cli.sse.max_retries: "最大重試次數: %{count} (0=無限)"
cli.sse.monitoring_connection: "開始監控初始連線..."
cli.sse.initial_disconnect: "初始連線斷開: %{reason}"
cli.sse.connection_alive: "初始連線存活時長: %{seconds}s"
cli.sse.reconnect_attempt: "重連嘗試 #%{attempt}/%{max}"
cli.sse.backoff_time: "退避時間: %{seconds}s"
cli.sse.reconnect_success: "重連成功 (耗時: %{duration})"
cli.sse.monitoring_reconnect: "開始監控重連後的連線..."
cli.sse.reconnect_disconnect: "重連後斷開: %{reason}"
cli.sse.reconnect_alive: "重連後存活時長: %{seconds}s"
cli.sse.max_retries_reached: "達到最大重試次數 (%{count}), 停止重連"
cli.sse.watchdog_exit_msg: "SSE Watchdog 結束"
# ===========================================
# CLI 訊息 - Stream 模式
# ===========================================
cli.stream.mode_starting: "Stream 模式啟動"
cli.stream.connect_timeout: "連線後端逾時 (%{seconds}s)"
cli.stream.connect_failed: "連線後端失敗: %{error}"
cli.stream.connect_success: "後端連線成功 (耗時: %{duration})"
cli.stream.stdio_starting: "啟動 stdio server..."
cli.stream.stdio_started: "stdio server 已啟動"
cli.stream.waiting_events: "開始等待 stdio server 事件..."
cli.stream.stdio_exit_eof: "stdio server 結束 - 原因: MCP 客戶端斷開連線 (stdin EOF)"
cli.stream.watchdog_exit: "Watchdog 任務結束"
cli.stream.normal_exit: "mcp-proxy convert (Stream 模式) 正常結束"
# ===========================================
# CLI 訊息 - Command 模式
# ===========================================
cli.command.local_mode: "模式: 本地命令模式"
cli.command.command: "命令: %{cmd} %{args}"
cli.command.ctrl_c: "收到 Ctrl+C 訊號,正在關閉..."
# ===========================================
# CLI 訊息 - 監控
# ===========================================
cli.monitoring.health: "開始監控 %{protocol} 連線健康狀態"
cli.monitoring.health_check_status: "\U0001F493 [%{protocol}][健康檢查] 連線狀態: %{status} (僅檢查連線通道, 未呼叫 list_tools), 檢查 #%{count}, 已存活: %{seconds}s"
cli.monitoring.health_check_normal: "\U0001F493 [%{protocol}][健康檢查] 後端服務正常 (list_tools 驗證通過), Ping #%{count}, 已存活: %{seconds}s"
# ===========================================
# 診斷訊息
# ===========================================
diagnostic.report_header: "========== 診斷報告 =========="
diagnostic.connection_protocol: "連線協定: %{protocol}"
diagnostic.service_url: "服務 URL: %{url}"
diagnostic.connection_duration: "連線存活時長: %{seconds} 秒"
diagnostic.disconnect_reason: "斷開原因: %{reason}"
diagnostic.error_type: "錯誤類型: %{error_type}"
diagnostic.possible_causes: "可能原因分析:"
diagnostic.suggestions: "建議:"
# 診斷 - 30秒逾時分析
diagnostic.analysis.timeout_30s: "⚠️ 連線在約 30 秒時斷開,極有可能是:"
diagnostic.analysis.timeout_30s_cause1: "伺服器端設定了 30 秒逾時限制"
diagnostic.analysis.timeout_30s_cause2: "負載平衡器(如 Nginx/ALB的預設逾時"
diagnostic.analysis.timeout_30s_cause3: "雲端服務商的閘道逾時限制"
# 診斷 - 快速斷開分析
diagnostic.analysis.quick_disconnect: "⚠️ 連線很快斷開(%{seconds}秒),可能是:"
diagnostic.analysis.quick_disconnect_cause1: "認證失敗或 token 無效"
diagnostic.analysis.quick_disconnect_cause2: "伺服器拒絕連線"
diagnostic.analysis.quick_disconnect_cause3: "網路不穩定"
# 診斷 - 長時間連線分析
diagnostic.analysis.long_connection: "✅ 連線保持了較長時間(%{seconds}秒),可能是:"
diagnostic.analysis.long_connection_cause1: "工具呼叫執行時間過長"
diagnostic.analysis.long_connection_cause2: "網路波動導致斷開"
# 診斷建議
diagnostic.suggestion.timeout_30s: "聯絡服務提供商增加逾時限制"
diagnostic.suggestion.timeout_client: "使用 --request-timeout 參數設定客戶端逾時"
diagnostic.suggestion.async_mode: "考慮使用非同步處理模式webhook 回呼)"
diagnostic.suggestion.ping_interval: "嘗試增加 ping 間隔: --ping-interval %{seconds}"
diagnostic.suggestion.ping_timeout: "增加 ping 逾時時間: --ping-timeout %{seconds}"
diagnostic.suggestion.ping_disable: "或停用 ping: --ping-interval 0"
# ===========================================
# 錯誤分類
# ===========================================
error_classify.timeout_30s: "30秒逾時可能是伺服器限制"
error_classify.service_unavailable_503: "服務不可用(503)"
error_classify.internal_server_error_500: "伺服器內部錯誤(500)"
error_classify.bad_gateway_502: "閘道錯誤(502)"
error_classify.gateway_timeout_504: "閘道逾時(504)"
error_classify.unauthorized_401: "未授權(401)"
error_classify.forbidden_403: "禁止存取(403)"
error_classify.not_found_404: "資源未找到(404)"
error_classify.request_timeout_408: "請求逾時(408)"
error_classify.timeout: "逾時"
error_classify.connection_refused: "連線被拒絕"
error_classify.connection_reset: "連線被重設"
error_classify.connection_closed: "連線關閉"
error_classify.dns_failed: "DNS解析失敗"
error_classify.ssl_tls_error: "SSL/TLS錯誤"
error_classify.network_error: "網路錯誤"
error_classify.session_error: "工作階段錯誤"
error_classify.unknown_error: "未知錯誤"
# ===========================================
# CLI 訊息 - health 命令
# ===========================================
cli.health.checking: "\U0001F50D 健康檢查服務: %{url}"
cli.health.using_protocol: "\U0001F50D 使用指定協定: %{protocol}"
cli.health.detecting_protocol: "\U0001F50D 正在偵測協定..."
cli.health.detected_protocol: "\U0001F50D 偵測到 %{protocol} 協定"
cli.health.healthy: "✅ 服務健康"
cli.health.unhealthy: "❌ 服務不健康"
cli.health.stdio_not_supported: "health 命令不支援 stdio 協定"
# ===========================================
# CLI 訊息 - check 命令
# ===========================================
cli.check.checking: "\U0001F50D 檢查服務: %{url}"
cli.check.healthy: "✅ 服務正常,偵測到 %{protocol} 協定"
cli.check.failed: "❌ 服務檢查失敗: %{error}"
# ===========================================
# CLI 訊息 - document-parser
# ===========================================
doc_parser.startup.app_info: "=== %{name} v%{version} 啟動 ==="
doc_parser.startup.config_summary: "設定摘要: %{summary}"
doc_parser.startup.listening: "服務監聽位址: %{host}:%{port}"
doc_parser.startup.checking_python: "開始檢查和初始化Python環境..."
doc_parser.startup.checking_venv: "檢查並啟用虛擬環境..."
doc_parser.startup.venv_activate_failed: "虛擬環境自動啟用失敗: %{error}"
doc_parser.startup.venv_activate_manual: "請手動啟用虛擬環境: source ./venv/bin/activate"
doc_parser.startup.venv_activated: "虛擬環境已自動啟用"
doc_parser.startup.env_check_failed: "環境檢查失敗,將在背景自動安裝: %{error}"
doc_parser.startup.mineru_not_installed: "MinerU相依套件未安裝開始背景自動安裝..."
doc_parser.startup.markitdown_not_installed: "MarkItDown相依套件未安裝開始背景自動安裝..."
doc_parser.startup.install_complete: "背景Python環境安裝完成"
doc_parser.startup.install_failed: "背景Python環境安裝失敗: %{error}"
doc_parser.startup.install_task_started: "Python相依套件安裝任務已啟動背景進行服務將正常啟動"
doc_parser.startup.mineru_ready: "MinerU相依套件已安裝版本: %{version}"
doc_parser.startup.markitdown_ready: "MarkItDown相依套件已安裝"
doc_parser.startup.python_ready: "Python環境檢查完成所有相依套件已就緒"
doc_parser.startup.state_failed: "無法建立應用狀態: %{error}"
doc_parser.startup.health_check_failed: "應用健康檢查失敗: %{error}"
doc_parser.startup.state_ready: "應用狀態初始化成功"
doc_parser.startup.http_routes_ready: "HTTP路由初始化成功"
doc_parser.startup.background_tasks_started: "背景任務已啟動"
doc_parser.startup.service_ready: "服務啟動成功,開始監聽連線..."
doc_parser.shutdown.service_stopped: "服務已關閉"
doc_parser.shutdown.ctrl_c_received: "收到 Ctrl+C 訊號,開始優雅關閉..."
doc_parser.shutdown.terminate_received: "收到 terminate 訊號,開始優雅關閉..."
doc_parser.shutdown.closing: "正在關閉服務..."
# uv-init 命令
doc_parser.uv_init.starting: "\U0001F680 開始在目前目錄初始化uv虛擬環境和相依套件..."
doc_parser.uv_init.current_dir: "\U0001F4C1 目前工作目錄: %{path}"
doc_parser.uv_init.venv_path: "\U0001F4C1 虛擬環境將建立在: ./venv/"
doc_parser.uv_init.validating_dir: "\U0001F50D 驗證目前目錄設定..."
doc_parser.uv_init.dir_valid: " ✅ 目錄驗證通過"
doc_parser.uv_init.warnings_found: " ⚠️ 發現 %{count} 個警告"
doc_parser.uv_init.dir_invalid: " ❌ 目錄驗證失敗,發現 %{count} 個問題"
doc_parser.uv_init.auto_fixing: " \U0001F527 嘗試自動修復 %{count} 個問題..."
doc_parser.uv_init.fix_success: " ✅ %{message}"
doc_parser.uv_init.fix_failed: " ❌ 清理失敗: %{error}"
doc_parser.uv_init.manual_fix_hint: " \U0001F4A1 請手動解決以下問題:"
doc_parser.uv_init.dir_validation_failed: "目錄驗證失敗,請解決上述問題後重試"
doc_parser.uv_init.dir_validation_error: " ⚠️ 目錄驗證失敗: %{error}"
doc_parser.uv_init.continue_install: " 繼續進行安裝,但可能遇到問題..."
# 環境檢查
doc_parser.check.checking_env: "\U0001F50D 檢查目前環境狀態..."
doc_parser.check.env_check_complete: " 環境檢查完成:"
doc_parser.check.python_available: "✅ 可用"
doc_parser.check.python_unavailable: "❌ 不可用"
doc_parser.check.uv_available: "✅ 可用"
doc_parser.check.uv_unavailable: "❌ 不可用"
doc_parser.check.venv_active: "✅ 啟用"
doc_parser.check.venv_inactive: "❌ 未啟用"
doc_parser.check.mineru_available: "✅ 可用"
doc_parser.check.mineru_unavailable: "❌ 不可用"
doc_parser.check.markitdown_available: "✅ 可用"
doc_parser.check.markitdown_unavailable: "❌ 不可用"
doc_parser.check.env_check_failed: " ⚠️ 環境檢查失敗: %{error}"
doc_parser.check.continue_install: " 繼續進行安裝..."
# 安裝過程
doc_parser.install.all_ready: "✨ 所有相依套件都已就緒,無需安裝!"
doc_parser.install.plan: "\U0001F4CB 安裝計畫:"
doc_parser.install.install_uv: "安裝 uv 工具"
doc_parser.install.create_venv: "建立虛擬環境 (./venv/)"
doc_parser.install.install_mineru: "安裝 MinerU 相依套件"
doc_parser.install.install_markitdown: "安裝 MarkItDown 相依套件"
doc_parser.install.starting: "⚙️ 開始設定Python環境和相依套件..."
doc_parser.install.wait_hint: "這可能需要幾分鐘時間,請耐心等待..."
doc_parser.install.path_issues: "⚠️ 偵測到潛在的路徑問題:"
doc_parser.install.auto_fixing: "\U0001F527 嘗試自動修復問題..."
doc_parser.install.fixed: "✅ 已修復以下問題:"
doc_parser.install.cannot_auto_fix: "無法自動修復,請手動解決上述問題"
doc_parser.install.fix_failed: "❌ 自動修復失敗: %{error}"
doc_parser.install.manual_fix_hint: "\U0001F4A1 手動修復建議:"
doc_parser.install.python_setup_complete: "✅ Python環境設定完成"
doc_parser.install.python_setup_failed: "❌ Python環境設定失敗: %{error}"
doc_parser.install.detailed_hints: "\U0001F4A1 詳細故障排除建議:"
doc_parser.install.diagnostic_commands: "\U0001F50D 診斷命令:"
doc_parser.install.check_env: "檢查環境狀態: document-parser check"
doc_parser.install.view_logs: "查看詳細日誌: 檢查 logs/ 目錄"
# 驗證
doc_parser.verify.starting: "\U0001F50D 驗證安裝結果..."
doc_parser.verify.complete: "驗證完成:"
doc_parser.verify.version: "版本: %{version}"
doc_parser.verify.partial_issues: "⚠️ 部分相依套件安裝可能有問題"
doc_parser.verify.need_fix: "\U0001F527 需要解決的問題:"
doc_parser.verify.suggestion: "建議: %{suggestion}"
doc_parser.verify.init_incomplete: "環境初始化未完全成功"
doc_parser.verify.failed: "❌ 驗證失敗: %{error}"
# 成功訊息
doc_parser.success.init_complete: "\U0001F389 uv環境初始化完成"
doc_parser.success.all_ready: "✨ 所有相依套件都已就緒,現在可以啟動伺服器了"
doc_parser.success.venv_activate: "\U0001F4CB 虛擬環境啟用指令:"
doc_parser.success.start_server: "\U0001F680 啟動伺服器:"
doc_parser.success.use_uv: "\U0001F527 或者使用 uv 直接執行命令:"
doc_parser.success.more_help: "\U0001F4DA 更多說明:"
doc_parser.success.tips: "\U0001F4A1 提示:"
doc_parser.success.venv_location: "虛擬環境位置: ./venv/"
doc_parser.success.python_path: "Python可執行檔: ./venv/bin/python (Linux/macOS) 或 .\\venv\\Scripts\\python.exe (Windows)"
doc_parser.success.troubleshoot_hint: "如遇問題,請執行 'document-parser troubleshoot' 查看詳細指南"
# troubleshoot 命令
doc_parser.troubleshoot.title: "\U0001F527 Document Parser 故障排除指南"
doc_parser.troubleshoot.env_overview: "\U0001F4CA 目前環境概覽:"
doc_parser.troubleshoot.work_dir: "工作目錄: %{path}"
doc_parser.troubleshoot.venv_path: "虛擬環境: ./venv/"
doc_parser.troubleshoot.os: "作業系統: %{os}"
# 虛擬環境問題
doc_parser.troubleshoot.venv_problems: "\U0001F3E0 1. 虛擬環境問題"
doc_parser.troubleshoot.venv_create_failed: "❓ 問題: 虛擬環境建立失敗"
doc_parser.troubleshoot.diagnostic_steps: "\U0001F50D 診斷步驟:"
doc_parser.troubleshoot.solutions: "\U0001F4A1 解決方案:"
doc_parser.troubleshoot.venv_activate_failed: "❓ 問題: 虛擬環境啟用失敗"
# 相依套件安裝問題
doc_parser.troubleshoot.deps_problems: "\U0001F4E6 2. 相依套件安裝問題"
doc_parser.troubleshoot.uv_not_installed: "❓ 問題: UV工具未安裝或不可用"
doc_parser.troubleshoot.mineru_markitdown_failed: "❓ 問題: MinerU或MarkItDown安裝失敗"
# 網路問題
doc_parser.troubleshoot.network_problems: "\U0001F310 3. 網路和下載問題"
doc_parser.troubleshoot.network_timeout: "❓ 問題: 網路連線逾時或下載失敗"
# 系統環境問題
doc_parser.troubleshoot.system_problems: "⚙️ 4. 系統環境問題"
doc_parser.troubleshoot.python_incompatible: "❓ 問題: Python版本不相容"
doc_parser.troubleshoot.cuda_config: "❓ 問題: CUDA環境設定 (可選用於GPU加速)"
# 診斷命令
doc_parser.troubleshoot.diagnostic_commands: "\U0001F50D 5. 常用診斷命令"
doc_parser.troubleshoot.env_check: "環境檢查:"
doc_parser.troubleshoot.manual_verify: "手動驗證:"
doc_parser.troubleshoot.view_logs: "日誌查看:"
# 取得幫助
doc_parser.troubleshoot.more_help: "\U0001F198 6. 取得更多幫助"
doc_parser.troubleshoot.if_unsolved: "如果上述方法都無法解決問題,請:"
# 即時診斷
doc_parser.troubleshoot.realtime_diagnosis: "\U0001F52C 即時環境診斷"
doc_parser.troubleshoot.env_good: "✅ 環境狀態良好,所有相依套件都已就緒"
doc_parser.troubleshoot.issues_found: "⚠️ 發現以下問題:"
doc_parser.troubleshoot.env_check_failed: "❌ 環境檢查失敗: %{error}"
doc_parser.troubleshoot.follow_guide: "請按照上述指南進行故障排除"
# 提示
doc_parser.troubleshoot.tip: "\U0001F4A1 提示: 大多數問題可以透過重新執行 'document-parser uv-init' 解決"
# 背景清理
doc_parser.background.cleanup_failed: "清理過期資料失敗: %{error}"
doc_parser.background.cleanup_complete: "背景清理任務執行完成"
# 訊號處理
doc_parser.signal.ctrl_c_failed: "無法監聽 Ctrl+C 訊號"
doc_parser.signal.terminate_failed: "無法監聽 terminate 訊號"
# ===========================================
# 通用訊息
# ===========================================
common.yes: "是"
common.no: "否"
common.success: "成功"
common.failed: "失敗"
common.error: "錯誤"
common.warning: "警告"
common.info: "資訊"
common.loading: "載入中..."
common.please_wait: "請稍候..."
common.retry: "重試"
common.cancel: "取消"
common.confirm: "確認"
common.back: "返回"
common.next: "下一步"
common.previous: "上一步"
common.done: "完成"
common.skip: "跳過"

View File

@@ -0,0 +1,65 @@
// MCP-Proxy CLI 实现
//
// CLI 主入口,负责命令分发到各个实现模块
use anyhow::{Result, bail};
// 导出 CLI 特定类型
// 注意: ConvertArgs 等通用参数类型已由 client/mod.rs 统一导出
pub use crate::client::support::args::{Cli, Commands};
/// 运行 CLI 主逻辑
///
/// 根据命令类型分发到对应的实现模块:
/// - Convert -> cli_impl/convert_cmd.rs
/// - Check/Detect -> cli_impl/check.rs
/// - Health -> cli_impl/health.rs
/// - Proxy -> proxy_server.rs
pub async fn run_cli(cli: Cli) -> Result<()> {
match cli.command {
Some(Commands::Convert(args)) => {
crate::client::cli_impl::run_convert_command(args, cli.verbose, cli.quiet).await
}
Some(Commands::Check(args)) => {
crate::client::cli_impl::run_check_command(args, cli.verbose, cli.quiet).await
}
Some(Commands::Detect(args)) => {
crate::client::cli_impl::run_detect_command(args, cli.verbose, cli.quiet).await
}
Some(Commands::Health(args)) => {
crate::client::cli_impl::run_health_command(args, cli.quiet).await
}
Some(Commands::Proxy(args)) => {
super::proxy_server::run_proxy_command(args, cli.verbose, cli.quiet).await
}
None => {
// 直接 URL 模式(向后兼容)
if let Some(url) = cli.url {
let args = crate::client::support::args::ConvertArgs {
url: Some(url),
config: None,
config_file: None,
name: None,
protocol: None,
auth: None,
header: vec![],
retries: 0, // 无限重试
allow_tools: None,
deny_tools: None,
ping_interval: 30, // 默认 30 秒 ping 一次
ping_timeout: 10, // 默认 10 秒超时
logging: crate::client::support::LoggingArgs {
diagnostic: true, // 默认启用诊断模式
log_dir: None, // 默认无日志目录(将在 init_logging 中自动设置)
log_file: None, // 默认无日志文件
otlp_endpoint: None, // 默认不启用 OTLP 追踪
service_name: "mcp-proxy".to_string(),
},
};
crate::client::cli_impl::run_convert_command(args, cli.verbose, cli.quiet).await
} else {
bail!("请提供 URL 或使用子命令")
}
}
}
}

View File

@@ -0,0 +1,42 @@
//! 检查和检测命令
//!
//! 实现服务状态检查和协议检测功能
use anyhow::Result;
use crate::client::support::{CheckArgs, DetectArgs};
/// 运行检查命令
pub async fn run_check_command(args: CheckArgs, _verbose: bool, quiet: bool) -> Result<()> {
if !quiet {
eprintln!("Checking service health: {}", &args.url);
}
match crate::client::protocol::detect_mcp_protocol(&args.url).await {
Ok(protocol) => {
if !quiet {
eprintln!("Service is healthy (protocol: {protocol})");
}
Ok(())
}
Err(e) => {
if !quiet {
eprintln!("Service check failed: {e}");
}
Err(e)
}
}
}
/// 运行协议检测命令
pub async fn run_detect_command(args: DetectArgs, _verbose: bool, quiet: bool) -> Result<()> {
let protocol = crate::client::protocol::detect_mcp_protocol(&args.url).await?;
if quiet {
println!("{}", protocol);
} else {
eprintln!("{}", protocol);
}
Ok(())
}

View File

@@ -0,0 +1,130 @@
//! Convert 命令的 CLI 实现
//!
//! 处理 convert 命令的参数解析和路由到核心逻辑
use anyhow::Result;
use crate::client::core::{run_command_mode, run_url_mode_with_retry};
use crate::client::support::{ConvertArgs, init_logging, parse_convert_config};
use crate::proxy::ToolFilter;
/// 运行转换命令 - 核心功能
pub async fn run_convert_command(args: ConvertArgs, verbose: bool, quiet: bool) -> Result<()> {
// 检查 --allow-tools 和 --deny-tools 互斥
if args.allow_tools.is_some() && args.deny_tools.is_some() {
anyhow::bail!(
"--allow-tools and --deny-tools cannot be used together, please choose only one"
);
}
// 创建工具过滤器
let tool_filter = if let Some(allow_tools) = args.allow_tools.clone() {
tracing::info!("Tool allowlist enabled: {:?}", allow_tools);
ToolFilter::allow(allow_tools)
} else if let Some(deny_tools) = args.deny_tools.clone() {
tracing::info!("Tool denylist enabled: {:?}", deny_tools);
ToolFilter::deny(deny_tools)
} else {
tracing::debug!("Tool filter disabled");
ToolFilter::default()
};
// 解析配置
tracing::debug!("Parsing convert configuration...");
let config_source = parse_convert_config(&args)?;
tracing::info!("Configuration parsed");
// 提取 MCP 名称用于日志文件命名
let mcp_name = match &config_source {
crate::client::support::McpConfigSource::RemoteService { name, .. } => {
tracing::info!("Service name: {}", name);
Some(name.as_str())
}
crate::client::support::McpConfigSource::LocalCommand { name, .. } => {
tracing::info!("Service name: {}", name);
Some(name.as_str())
}
_ => {
tracing::info!("Service name not specified");
None
}
};
// 初始化日志系统
init_logging(&args, mcp_name, quiet, verbose)?;
tracing::debug!("Logging initialized");
// 记录命令启动(必须在日志系统初始化之后)
tracing::info!("========================================");
tracing::info!("Starting convert command");
tracing::info!("Command: convert");
tracing::info!("Version: {}", env!("CARGO_PKG_VERSION"));
tracing::info!("Diagnostic mode: {}", args.logging.diagnostic);
tracing::info!("========================================");
// 根据配置源执行不同逻辑
match config_source {
crate::client::support::McpConfigSource::DirectUrl { url } => {
tracing::info!("Mode: direct URL");
tracing::info!("Target URL: {}", url);
// 直接 URL 模式(带自动重连)
run_url_mode_with_retry(
&args,
&url,
std::collections::HashMap::new(),
None,
tool_filter,
verbose,
quiet,
)
.await
}
crate::client::support::McpConfigSource::RemoteService {
name,
url,
protocol,
headers,
timeout,
} => {
// 远程服务配置模式
tracing::info!("Mode: remote service config");
tracing::info!("Service name: {}", name);
tracing::info!("Service URL: {}", url);
if let Some(proto) = &protocol {
tracing::info!("Configured protocol: {:?}", proto);
}
if !headers.is_empty() {
tracing::debug!("Custom headers: {:?}", headers);
}
if let Some(timeout) = timeout {
tracing::debug!("Configured timeout: {}s", timeout);
}
if !quiet {
eprintln!("🚀 MCP-Stdio-Proxy: {} ({}) → stdio", name, url);
}
// 合并 headers配置 + 命令行
let merged_headers =
crate::client::support::merge_headers(headers, &args.header, args.auth.as_ref());
run_url_mode_with_retry(
&args,
&url,
merged_headers,
protocol.or(timeout.map(|_| crate::client::protocol::McpProtocol::Stream)),
tool_filter,
verbose,
quiet,
)
.await
}
crate::client::support::McpConfigSource::LocalCommand {
name,
command,
args: cmd_args,
env,
} => {
// 本地命令模式子进程继承父进程环境变量MCP JSON 的 env 会覆盖同名变量)
run_command_mode(&name, &command, cmd_args, env, tool_filter, quiet).await
}
}
}

View File

@@ -0,0 +1,207 @@
//! 健康检查命令
//!
//! 通过建立 MCP 连接验证服务是否健康
use std::time::{Duration, Instant};
use anyhow::{Result, bail};
use mcp_common::McpClientConfig;
use mcp_sse_proxy::SseClientConnection;
use mcp_streamable_proxy::StreamClientConnection;
use crate::client::protocol::detect_mcp_protocol;
use crate::client::proxy_server::ProxyProtocol;
use crate::client::support::HealthArgs;
use crate::model::McpProtocol;
/// 健康检查结果
struct HealthCheckResult {
/// 工具数量
tool_count: usize,
/// 服务器名称
server_name: Option<String>,
/// 服务器版本
server_version: Option<String>,
}
/// 运行健康检查命令
///
/// 通过真正建立 MCP 连接来验证服务是否健康。
/// 成功返回 Ok(()),失败返回 Err。
pub async fn run_health_command(args: HealthArgs, quiet: bool) -> Result<()> {
if !quiet {
eprintln!("Checking health for: {}", &args.url);
}
// 1. 确定协议类型
let protocol = match &args.protocol {
Some(p) => {
let proto = proxy_protocol_to_mcp_protocol(p.clone());
if !quiet {
eprintln!("Using protocol: {}", protocol_display_name(&proto));
}
proto
}
None => {
if !quiet {
eprintln!("Detecting protocol...");
}
let proto = detect_mcp_protocol(&args.url).await?;
if !quiet {
eprintln!("Detected protocol: {}", protocol_display_name(&proto));
}
proto
}
};
// 2. 检查协议类型是否支持
if protocol == McpProtocol::Stdio {
bail!("Stdio protocol is not supported for health checks");
}
// 3. 构建配置
let config = build_config(&args);
// 4. 尝试连接(带超时)
let start = Instant::now();
let result = tokio::time::timeout(
Duration::from_secs(args.timeout),
connect_and_verify(&config, protocol.clone()),
)
.await;
let elapsed = start.elapsed();
// 5. 输出结果
match result {
Ok(Ok(health_result)) => {
if !quiet {
eprintln!("Health check passed");
eprintln!(" Protocol: {}", protocol_display_name(&protocol));
eprintln!(" Tools: {}", health_result.tool_count);
eprintln!(" Response time: {}ms", elapsed.as_millis());
if let (Some(name), Some(version)) =
(&health_result.server_name, &health_result.server_version)
{
eprintln!(" Server: {} v{}", name, version);
} else if let Some(name) = &health_result.server_name {
eprintln!(" Server: {}", name);
}
}
Ok(())
}
Ok(Err(e)) => {
if !quiet {
eprintln!("Health check failed");
eprintln!(" Error: {}", e);
eprintln!(" Response time: {}ms", elapsed.as_millis());
}
Err(anyhow::anyhow!("health check failed: {}", e))
}
Err(_) => {
if !quiet {
eprintln!("Health check failed");
eprintln!(" Error: connection timed out ({}s)", args.timeout);
}
Err(anyhow::anyhow!("health check timeout"))
}
}
}
/// 获取协议的显示名称
fn protocol_display_name(protocol: &McpProtocol) -> &'static str {
match protocol {
McpProtocol::Sse => "SSE",
McpProtocol::Stream => "Streamable HTTP",
McpProtocol::Stdio => "Stdio",
}
}
/// 将 ProxyProtocol 转换为 McpProtocol
fn proxy_protocol_to_mcp_protocol(p: ProxyProtocol) -> McpProtocol {
match p {
ProxyProtocol::Sse => McpProtocol::Sse,
ProxyProtocol::Stream => McpProtocol::Stream,
}
}
/// 构建 MCP 客户端配置
fn build_config(args: &HealthArgs) -> McpClientConfig {
let mut config = McpClientConfig::new(&args.url);
// 添加认证 header
if let Some(auth) = &args.auth {
config = config.with_header("Authorization", auth);
}
// 添加自定义 headers
for (key, value) in &args.header {
config = config.with_header(key, value);
}
// 设置超时
config = config.with_connect_timeout(Duration::from_secs(args.timeout));
config = config.with_read_timeout(Duration::from_secs(args.timeout));
config
}
/// 尝试连接并验证服务
async fn connect_and_verify(
config: &McpClientConfig,
protocol: McpProtocol,
) -> Result<HealthCheckResult> {
match protocol {
McpProtocol::Sse => {
let conn = SseClientConnection::connect(config.clone()).await?;
// 获取工具列表
let tools = conn.list_tools().await?;
let tool_count = tools.len();
// 获取服务器信息
let (server_name, server_version) = conn
.peer_info()
.map(|info| {
(
Some(info.server_info.name.clone()),
Some(info.server_info.version.clone()),
)
})
.unwrap_or((None, None));
Ok(HealthCheckResult {
tool_count,
server_name,
server_version,
})
}
McpProtocol::Stream => {
let conn = StreamClientConnection::connect(config.clone()).await?;
// 获取工具列表
let tools = conn.list_tools().await?;
let tool_count = tools.len();
// 获取服务器信息
let (server_name, server_version) = conn
.peer_info()
.map(|info| {
(
Some(info.server_info.name.clone()),
Some(info.server_info.version.clone()),
)
})
.unwrap_or((None, None));
Ok(HealthCheckResult {
tool_count,
server_name,
server_version,
})
}
McpProtocol::Stdio => {
// 不应该到达这里,因为前面已经检查过了
bail!("stdio protocol is not supported for health check")
}
}
}

View File

@@ -0,0 +1,12 @@
//! CLI 实现模块
//!
//! 处理 CLI 命令的实现和参数解析
pub mod check;
pub mod convert_cmd;
pub mod health;
// 导出 CLI 命令实现
pub use check::{run_check_command, run_detect_command};
pub use convert_cmd::run_convert_command;
pub use health::run_health_command;

View File

@@ -0,0 +1,165 @@
//! 本地命令模式处理
//!
//! 处理本地命令形式的 MCP 服务(通过子进程)
use anyhow::Result;
use std::collections::HashMap;
use std::process::Stdio;
use crate::proxy::{StreamProxyHandler, ToolFilter};
// 使用 mcp-streamable-proxy 的类型rmcp 0.12process-wrap 9.0
use mcp_streamable_proxy::{
ClientCapabilities, ClientInfo, Implementation, ServiceExt, TokioChildProcess, stdio,
};
use crate::client::support::utils::truncate_str;
// 进程组管理(跨平台子进程清理)
use process_wrap::tokio::{CommandWrap, KillOnDrop};
#[cfg(unix)]
use process_wrap::tokio::ProcessGroup;
#[cfg(windows)]
use process_wrap::tokio::JobObject;
/// 命令模式执行(本地子进程)
/// 使用 mcp-streamable-proxyrmcp 0.12)实现 stdio CLI 模式
pub async fn run_command_mode(
name: &str,
command: &str,
cmd_args: Vec<String>,
env: HashMap<String, String>,
tool_filter: ToolFilter,
quiet: bool,
) -> Result<()> {
tracing::info!("Running in local command mode");
tracing::info!("Command: {} {:?}", command, cmd_args);
if !env.is_empty() {
tracing::debug!("Env var count: {}", env.len());
}
if !quiet {
eprintln!("🚀 MCP-Stdio-Proxy: {} (command) → stdio", name);
eprintln!(" Command: {} {:?}", command, cmd_args);
if !env.is_empty() {
eprintln!(" Env vars: {:?}", env);
}
}
// 显示过滤器配置
if !quiet && tool_filter.is_enabled() {
eprintln!("🔧 Tool filtering enabled");
}
// 诊断日志:记录子进程启动信息
tracing::debug!("[child-process] {} {:?}", command, cmd_args);
// 使用 process-wrap 创建子进程命令(跨平台进程清理)
// process-wrap 会自动处理进程组Unix或 Job ObjectWindows
// 并且在 Drop 时自动清理子进程树
// 子进程默认继承父进程的所有环境变量
let mut wrapped_cmd = CommandWrap::with_new(command, |cmd| {
cmd.args(&cmd_args);
// 设置 MCP JSON 配置中的环境变量(会覆盖继承的同名变量)
for (k, v) in &env {
cmd.env(k, v);
}
});
// Unix: 创建进程组,支持 killpg 清理整个进程树
#[cfg(unix)]
wrapped_cmd.wrap(ProcessGroup::leader());
// Windows: 使用 Job Object 管理进程树,并隐藏控制台窗口
// 使用 CREATE_NO_WINDOW | CREATE_NEW_PROCESS_GROUP 确保孙进程也不弹出窗口
#[cfg(windows)]
{
use process_wrap::tokio::CreationFlags;
use windows::Win32::System::Threading::{CREATE_NEW_PROCESS_GROUP, CREATE_NO_WINDOW};
wrapped_cmd.wrap(CreationFlags(CREATE_NO_WINDOW | CREATE_NEW_PROCESS_GROUP));
wrapped_cmd.wrap(JobObject);
}
// 所有平台: Drop 时自动清理进程
wrapped_cmd.wrap(KillOnDrop);
// 启动子进程
// 使用 builder 模式捕获 stderr便于诊断子 MCP 服务初始化失败
tracing::debug!("Starting child process...");
let (tokio_process, child_stderr) = TokioChildProcess::builder(wrapped_cmd)
.stderr(Stdio::piped())
.spawn()?;
// 启动 stderr 日志读取任务
if let Some(stderr_pipe) = child_stderr {
mcp_common::spawn_stderr_reader(stderr_pipe, name.to_string());
}
if !quiet {
eprintln!("🔗 Starting child process...");
}
// 创建 ClientInfo使用 rmcp 0.12 类型)
let client_info = create_client_info();
// 连接到子进程
let running = client_info.serve(tokio_process).await?;
if !quiet {
eprintln!("✅ Child process started, proxying to stdio...");
// 打印工具列表
match running.list_tools(None).await {
Ok(tools_result) => {
let tools = &tools_result.tools;
if tools.is_empty() {
eprintln!("⚠️ Tool list is empty (tools/list returned 0 tools)");
} else {
eprintln!("🔧 Available tools ({}):", tools.len());
for tool in tools {
let desc = tool.description.as_deref().unwrap_or("no description");
let desc_short = truncate_str(desc, 50);
eprintln!(" - {} : {}", tool.name, desc_short);
}
}
}
Err(e) => {
eprintln!("⚠️ Failed to list tools: {}", e);
}
}
eprintln!("💡 You can now send JSON-RPC requests through stdin");
}
// 使用 StreamProxyHandler + stdio 将本地 MCP 服务透明暴露为 stdio
let proxy_handler =
StreamProxyHandler::with_tool_filter(running, name.to_string(), tool_filter);
let server = proxy_handler.serve(stdio()).await?;
// 设置 Ctrl+C 信号处理
tokio::select! {
result = server.waiting() => {
result?;
}
_ = tokio::signal::ctrl_c() => {
tracing::info!("Ctrl+C received, shutting down");
// tokio runtime 会清理资源,包括子进程
}
}
Ok(())
}
/// 创建 ClientInfo使用 rmcp 1.1.0 类型)
fn create_client_info() -> ClientInfo {
let capabilities = ClientCapabilities::builder()
.enable_experimental()
.enable_roots()
.enable_roots_list_changed()
.enable_sampling()
.build();
ClientInfo::new(
capabilities,
Implementation::new("mcp-proxy-cli", env!("CARGO_PKG_VERSION")),
)
}

View File

@@ -0,0 +1,168 @@
//! 公共模块 - 提取 SSE 和 Stream 模式的共享逻辑
//!
//! 减少代码重复,统一健康检查行为
use std::future::Future;
use std::time::Duration;
/// 健康检查能力 trait
///
/// 抽象 SSE 和 Stream handler 的共同行为
pub trait HealthChecker: Send + Sync {
/// 检查后端是否可用(同步检查)
fn is_backend_available(&self) -> bool;
/// 检查是否已终止(异步,会调用 list_tools 验证后端服务)
fn is_terminated_async(&self) -> impl Future<Output = bool> + Send;
}
/// 监控连接健康状态
///
/// 提取自 monitor_sse_connection 和 monitor_stream_connection 的公共逻辑
///
/// # 参数
/// - `handler`: 实现 HealthChecker trait 的 handler
/// - `ping_interval`: ping 间隔0 表示禁用 ping
/// - `ping_timeout`: ping 超时(秒)
/// - `quiet`: 是否静默模式
/// - `protocol_name`: 协议名称,用于日志输出(如 "SSE" 或 "Stream"
///
/// # 返回值
/// 返回断开连接的原因描述
pub async fn monitor_connection_health<H: HealthChecker>(
handler: &H,
ping_interval: u64,
ping_timeout: u64,
quiet: bool,
protocol_name: &str,
) -> String {
let connection_start = std::time::Instant::now();
if !quiet {
tracing::info!("[{}] Connection health monitoring started", protocol_name);
}
// 健康检查日志间隔:与 ping 间隔一致,但至少 30 秒
let health_log_interval_secs = if ping_interval > 0 {
ping_interval.max(30)
} else {
30
};
if ping_interval == 0 {
// 没有 ping 检测,仅检查连接状态
let mut health_log_interval =
tokio::time::interval(Duration::from_secs(health_log_interval_secs));
health_log_interval.tick().await; // 跳过第一次
let mut check_count = 0u64;
loop {
tokio::select! {
_ = tokio::time::sleep(Duration::from_secs(1)) => {
if !handler.is_backend_available() {
let alive_duration = connection_start.elapsed();
let disconnect_reason =
format!(
"[{}] Backend connection closed (alive: {}s)",
protocol_name,
alive_duration.as_secs()
);
if !quiet {
tracing::error!("{}", disconnect_reason);
}
return disconnect_reason;
}
}
_ = health_log_interval.tick() => {
check_count += 1;
let alive_duration = connection_start.elapsed();
let backend_available = handler.is_backend_available();
tracing::info!(
"[{}] Health check #{} status={}, alive={}s",
protocol_name,
check_count,
if backend_available { "ok" } else { "error" },
alive_duration.as_secs()
);
}
}
}
}
let mut interval = tokio::time::interval(Duration::from_secs(ping_interval));
interval.tick().await;
let mut ping_count = 0u64;
let mut last_health_log = std::time::Instant::now();
let mut first_ping = true; // 标记是否是第一次 ping
loop {
interval.tick().await;
ping_count += 1;
let alive_duration = connection_start.elapsed();
if !handler.is_backend_available() {
let disconnect_reason = format!(
"[{}] Backend connection closed (alive: {}s)",
protocol_name,
alive_duration.as_secs()
);
if !quiet {
tracing::error!("{}", disconnect_reason);
}
return disconnect_reason;
}
let check_result = tokio::time::timeout(
Duration::from_secs(ping_timeout),
handler.is_terminated_async(),
)
.await;
match check_result {
Ok(true) => {
let disconnect_reason = format!(
"[{}] Ping check failed (service error), alive: {}s",
protocol_name,
alive_duration.as_secs()
);
if !quiet {
tracing::error!("{}", disconnect_reason);
}
return disconnect_reason;
}
Ok(false) => {
// 第一次 ping 成功后打印,之后每隔 health_log_interval_secs 秒打印一次
let since_last_log = last_health_log.elapsed().as_secs();
if first_ping || since_last_log >= health_log_interval_secs {
tracing::info!(
"[{}] Ping check #{} passed, alive={}s",
protocol_name,
ping_count,
alive_duration.as_secs()
);
last_health_log = std::time::Instant::now();
first_ping = false;
} else {
tracing::debug!(
"[{}] list_tools ping #{} passed, alive={}s",
protocol_name,
ping_count,
alive_duration.as_secs()
);
}
}
Err(_) => {
let disconnect_reason = format!(
"[{}] Ping check timeout ({}s), alive: {}s",
protocol_name,
ping_timeout,
alive_duration.as_secs()
);
if !quiet {
tracing::error!("{}", disconnect_reason);
}
return disconnect_reason;
}
}
}
}

View File

@@ -0,0 +1,157 @@
//! 协议转换核心逻辑
//!
//! 处理协议转换的主要流程,包括 URL 模式、协议检测等
use anyhow::Result;
use std::collections::HashMap;
use crate::client::support::{ConvertArgs, protocol_name};
use crate::proxy::{McpClientConfig, ToolFilter};
use super::sse::run_sse_mode;
use super::stream::run_stream_mode;
/// URL 模式执行(带自动重连)
/// 使用分支逻辑:根据协议类型调用不同的处理函数
pub async fn run_url_mode_with_retry(
args: &ConvertArgs,
url: &str,
merged_headers: HashMap<String, String>,
config_protocol: Option<crate::client::protocol::McpProtocol>,
tool_filter: ToolFilter,
verbose: bool,
quiet: bool,
) -> Result<()> {
tracing::info!("Starting protocol conversion");
tracing::info!("Target URL: {url}");
tracing::debug!("Header count: {}", merged_headers.len());
tracing::debug!(
"Ping interval: {}s, ping timeout: {}s",
args.ping_interval,
args.ping_timeout
);
tracing::debug!("Retry count: {} (0 = unlimited)", args.retries);
if !quiet && merged_headers.is_empty() {
eprintln!("🚀 MCP-Stdio-Proxy: {} → stdio", url);
}
// 显示过滤器配置
if !quiet {
if let Some(ref allow_tools) = args.allow_tools {
tracing::info!("Tool allowlist: {:?}", allow_tools);
}
if let Some(ref deny_tools) = args.deny_tools {
tracing::info!("Tool denylist: {:?}", deny_tools);
}
}
// 确定协议类型:命令行参数 > 配置文件 > 自动检测
let protocol = if let Some(ref proto) = args.protocol {
let detected = match proto {
crate::client::proxy_server::ProxyProtocol::Sse => {
crate::client::protocol::McpProtocol::Sse
}
crate::client::proxy_server::ProxyProtocol::Stream => {
crate::client::protocol::McpProtocol::Stream
}
};
tracing::info!(
"Using protocol from CLI argument: {}",
protocol_name(&detected)
);
if !quiet {
eprintln!("🔧 Using protocol from CLI: {}", protocol_name(&detected));
}
detected
} else if let Some(proto) = config_protocol {
tracing::info!("Using protocol from config: {}", protocol_name(&proto));
if !quiet {
eprintln!("🔧 Using protocol from config: {}", protocol_name(&proto));
}
proto
} else {
tracing::info!("Detecting protocol...");
if !quiet {
eprintln!("🔍 Detecting protocol...");
}
let detection_start = std::time::Instant::now();
let detected = crate::client::protocol::detect_mcp_protocol(url)
.await
.map_err(|e| {
tracing::error!("Protocol detection failed: {}", e);
e
})?;
let detection_duration = detection_start.elapsed();
tracing::info!(
"Protocol detection completed: protocol={}, duration={:?}",
protocol_name(&detected),
detection_duration
);
if !quiet {
eprintln!("🔍 Detected protocol: {}", protocol_name(&detected));
}
detected
};
// 构建 McpClientConfig
tracing::debug!("Building MCP client config...");
let config = build_mcp_config(url, &merged_headers, args.auth.as_ref());
tracing::debug!("MCP client config ready");
// 根据协议类型分支处理
tracing::info!("Using protocol: {}", protocol_name(&protocol));
match protocol {
crate::client::protocol::McpProtocol::Sse => {
run_sse_mode(config, args.clone(), tool_filter, verbose, quiet)
.await
.map_err(|e| {
tracing::error!("SSE mode failed: {:?}", e);
eprintln!("❌ SSE mode failed: {}", e);
e
})
}
crate::client::protocol::McpProtocol::Stream => {
run_stream_mode(config, args.clone(), tool_filter, verbose, quiet)
.await
.map_err(|e| {
tracing::error!("Stream mode failed: {:?}", e);
eprintln!("❌ Stream mode failed: {}", e);
e
})
}
crate::client::protocol::McpProtocol::Stdio => {
tracing::error!("Stdio protocol does not support URL conversion");
anyhow::bail!(
"Stdio protocol does not support URL conversion, please use --config for local commands"
)
}
}
}
/// 构建 McpClientConfig
pub fn build_mcp_config(
url: &str,
headers: &HashMap<String, String>,
auth: Option<&String>,
) -> McpClientConfig {
let mut config = McpClientConfig::new(url);
for (k, v) in headers {
// Authorization header: 确保有 "Bearer " 前缀,与 Server 模式行为一致
if k.eq_ignore_ascii_case("Authorization") {
let value = if v.starts_with("Bearer ") {
v.clone()
} else {
format!("Bearer {}", v)
};
config = config.with_header(k, value);
} else {
config = config.with_header(k, v);
}
}
if let Some(auth_value) = auth {
// 命令行 --auth 参数不带 "Bearer " 前缀,直接添加
config = config.with_header("Authorization", auth_value);
}
config
}

View File

@@ -0,0 +1,15 @@
//! 核心业务逻辑模块
//!
//! 包含协议转换的核心实现,与 CLI 接口解耦
pub mod command;
pub mod common;
pub mod convert;
pub mod sse;
pub mod stream;
// 导出公共 API
// 注意: run_sse_mode 和 run_stream_mode 是内部实现细节,
// 只被 convert 模块使用,不需要对外暴露
pub use command::run_command_mode;
pub use convert::run_url_mode_with_retry;

View File

@@ -0,0 +1,436 @@
//! SSE 模式处理
//!
//! Server-Sent Events 协议的实现和连接管理
use anyhow::Result;
use std::sync::Arc;
use std::time::Duration;
use tracing::error;
use super::common::HealthChecker;
use crate::client::support::{
ConvertArgs, classify_error, print_diagnostic_report, summarize_error, truncate_str,
};
use crate::proxy::{McpClientConfig, ProxyHandler, SseClientConnection, ToolFilter};
use mcp_sse_proxy::{ServiceExt, stdio as sse_stdio};
/// 为 ProxyHandler 实现 HealthChecker trait
impl HealthChecker for ProxyHandler {
fn is_backend_available(&self) -> bool {
self.is_backend_available()
}
async fn is_terminated_async(&self) -> bool {
self.is_terminated_async().await
}
}
/// SSE 模式处理(使用 mcp-sse-proxyrmcp 0.10
pub async fn run_sse_mode(
config: McpClientConfig,
args: ConvertArgs,
tool_filter: ToolFilter,
verbose: bool,
quiet: bool,
) -> Result<()> {
tracing::info!("========================================");
tracing::info!("Starting SSE mode");
tracing::info!("Target URL: {}", config.url);
tracing::info!(
"Ping config: interval={}s, timeout={}s",
args.ping_interval,
args.ping_timeout
);
tracing::info!("========================================");
if !quiet {
eprintln!("🔗 Connecting to backend service (SSE)...");
}
// 1. 使用高层 API 连接(带重试,防止初始连接因时序问题失败)
let connect_timeout = Duration::from_secs(15);
const MAX_INITIAL_RETRIES: u32 = 3;
const INITIAL_BACKOFF_SECS: u64 = 2;
const MAX_BACKOFF_SECS: u64 = 4;
tracing::info!(
"Connecting to backend (per-attempt timeout: {}s, max retries: {})",
connect_timeout.as_secs(),
MAX_INITIAL_RETRIES
);
let connect_start = std::time::Instant::now();
let mut last_error = None;
let mut conn = None;
let mut backoff_secs = INITIAL_BACKOFF_SECS;
for attempt in 1..=MAX_INITIAL_RETRIES {
match tokio::time::timeout(
connect_timeout,
SseClientConnection::connect(config.clone()),
)
.await
{
Ok(Ok(c)) => {
if attempt > 1 {
tracing::info!(
"Backend connection succeeded on attempt {}/{}",
attempt,
MAX_INITIAL_RETRIES
);
}
conn = Some(c);
break;
}
Ok(Err(e)) => {
tracing::warn!(
"Backend connection attempt {}/{} failed: {}",
attempt,
MAX_INITIAL_RETRIES,
e
);
last_error = Some(format!("Backend connection failed: {}", e));
}
Err(_) => {
tracing::warn!(
"Backend connection attempt {}/{} timed out ({}s)",
attempt,
MAX_INITIAL_RETRIES,
connect_timeout.as_secs()
);
last_error = Some(format!(
"Backend connection timeout ({}s)",
connect_timeout.as_secs()
));
}
}
if attempt < MAX_INITIAL_RETRIES {
tracing::info!("Retrying in {}s... (elapsed: {:?})", backoff_secs, connect_start.elapsed());
if !quiet {
eprintln!(
"⚠️ Connection attempt {}/{} failed, retrying in {}s...",
attempt, MAX_INITIAL_RETRIES, backoff_secs
);
}
tokio::time::sleep(Duration::from_secs(backoff_secs)).await;
backoff_secs = (backoff_secs * 2).min(MAX_BACKOFF_SECS);
}
}
let conn = conn.ok_or_else(|| {
let elapsed = connect_start.elapsed();
let msg = last_error.unwrap_or_else(|| "Unknown connection error".to_string());
tracing::error!(
"All {} connection attempts failed after {:?}: {}",
MAX_INITIAL_RETRIES, elapsed, msg
);
eprintln!(
"❌ All {} connection attempts failed after {:.1}s: {}",
MAX_INITIAL_RETRIES, elapsed.as_secs_f64(), msg
);
anyhow::anyhow!(msg)
})?;
let connect_duration = connect_start.elapsed();
tracing::info!(
"Backend connected successfully (duration: {:?})",
connect_duration
);
if !quiet {
eprintln!("✅ Backend connected successfully");
// 打印工具列表
print_sse_tools(&conn, quiet).await;
if args.ping_interval > 0 {
eprintln!(
"💓 Health ping: every {}s (timeout {}s)",
args.ping_interval, args.ping_timeout
);
}
}
// 2. 创建 handler消耗 conn
tracing::debug!("Creating ProxyHandler...");
let handler = Arc::new(conn.into_handler("cli".to_string(), tool_filter.clone()));
tracing::debug!("ProxyHandler created");
// 3. 启动 stdio server
tracing::info!("Starting stdio server...");
let server = (*handler).clone().serve(sse_stdio()).await.map_err(|e| {
tracing::error!("Failed to start stdio server: {:?}", e);
eprintln!("❌ Failed to start stdio server: {}", e);
e
})?;
tracing::info!("Stdio server started");
if !quiet {
eprintln!("💡 Stdio server started, proxying traffic...");
}
// 4. 启动 watchdog 任务
let handler_for_watchdog = handler.clone();
let mut watchdog_handle = tokio::spawn(run_sse_watchdog(
handler_for_watchdog,
args,
config,
tool_filter,
verbose,
quiet,
));
tracing::debug!("Watchdog task started");
// 5. 等待 stdio server 退出
tracing::info!("Waiting for stdio/watchdog events...");
tokio::select! {
result = server.waiting() => {
tracing::info!("========================================");
tracing::info!("Stdio server exited (EOF)");
tracing::info!("========================================");
watchdog_handle.abort();
result?;
}
watchdog_result = &mut watchdog_handle => {
tracing::info!("========================================");
tracing::info!("Watchdog task exited");
tracing::info!("========================================");
if let Err(e) = watchdog_result
&& !e.is_cancelled()
{
error!("SSE Watchdog task failed: {:?}", e);
}
}
}
tracing::info!("SSE mode exited normally");
Ok(())
}
/// 打印 SSE 连接的工具列表
async fn print_sse_tools(conn: &SseClientConnection, quiet: bool) {
if quiet {
return;
}
match conn.list_tools().await {
Ok(tools) => {
if tools.is_empty() {
eprintln!("⚠️ Tool list is empty (tools/list returned 0 tools)");
} else {
eprintln!("🔧 Available tools ({}):", tools.len());
for tool in &tools {
let desc = tool.description.as_deref().unwrap_or("no description");
let desc_short = truncate_str(desc, 50);
eprintln!(" - {} : {}", tool.name, desc_short);
}
}
}
Err(e) => {
eprintln!("⚠️ Failed to list tools: {}", e);
}
}
}
/// SSE 模式的 watchdog负责监控连接健康、断开时重连
async fn run_sse_watchdog(
handler: Arc<ProxyHandler>,
args: ConvertArgs,
config: McpClientConfig,
_tool_filter: ToolFilter,
verbose: bool,
quiet: bool,
) {
tracing::info!("========================================");
tracing::info!("Starting SSE watchdog");
tracing::info!("Max retries: {}", args.retries);
tracing::info!("========================================");
let max_retries = args.retries;
let mut attempt = 0u32;
let mut backoff_secs = 1u64;
const MAX_BACKOFF_SECS: u64 = 30;
const EVENT_DISCONNECTED: &str = "EVENT_DISCONNECTED";
const EVENT_RECONNECTED: &str = "EVENT_RECONNECTED";
const EVENT_RETRY_BACKOFF: &str = "EVENT_RETRY_BACKOFF";
let initial_connection_start = std::time::Instant::now();
// 首先监控现有连接的健康状态
tracing::info!("Monitoring initial connection health");
let disconnect_reason =
monitor_sse_connection(&handler, args.ping_interval, args.ping_timeout, quiet).await;
// 连接断开,标记后端不可用
tracing::warn!("Initial connection disconnected: {}", disconnect_reason);
handler.swap_backend(None);
let alive_duration = initial_connection_start.elapsed();
tracing::info!(
"Initial connection alive duration: {}s",
alive_duration.as_secs()
);
if !quiet {
eprintln!(
"⚠️ [{}] Connection disconnected: {}",
EVENT_DISCONNECTED, disconnect_reason
);
}
// 生成诊断报告(首次断开)
print_diagnostic_report(
"SSE",
&config.url,
alive_duration.as_secs(),
&disconnect_reason,
None,
args.logging.diagnostic,
);
// 进入重连循环
loop {
attempt += 1;
tracing::info!("========================================");
if max_retries == 0 {
tracing::info!("Reconnect attempt #{} (unlimited mode)", attempt);
} else {
tracing::info!("Reconnect attempt {}/{}", attempt, max_retries);
}
tracing::info!("Backoff: {}s", backoff_secs);
if !quiet {
eprintln!("🔗 Reconnecting (attempt #{})...", attempt);
}
// 尝试建立连接
tracing::debug!("Attempting to establish connection...");
let connect_start = std::time::Instant::now();
let connect_result = SseClientConnection::connect(config.clone()).await;
let connect_duration = connect_start.elapsed();
match connect_result {
Ok(conn) => {
tracing::info!("Reconnect succeeded (duration: {:?})", connect_duration);
// 连接成功,获取 RunningService 并热替换后端
let running = conn.into_running_service();
handler.swap_backend(Some(running));
backoff_secs = 1;
if !quiet {
eprintln!(
"✅ [{}] Reconnected, proxy service resumed",
EVENT_RECONNECTED
);
}
// 监控连接健康
tracing::info!("Monitoring connection after reconnect");
let reconnect_start = std::time::Instant::now();
let disconnect_reason =
monitor_sse_connection(&handler, args.ping_interval, args.ping_timeout, quiet)
.await;
// 连接断开,标记后端不可用
tracing::warn!("Reconnected session disconnected: {}", disconnect_reason);
handler.swap_backend(None);
let reconnect_alive_duration = reconnect_start.elapsed();
tracing::info!(
"Reconnected session alive duration: {}s",
reconnect_alive_duration.as_secs()
);
if !quiet {
eprintln!(
"⚠️ [{}] Connection disconnected: {}",
EVENT_DISCONNECTED, disconnect_reason
);
}
// 生成诊断报告(重连后断开)
print_diagnostic_report(
"SSE",
&config.url,
reconnect_alive_duration.as_secs(),
&disconnect_reason,
None,
args.logging.diagnostic,
);
}
Err(e) => {
let error_type = classify_error(&e);
tracing::error!(
"Connection failed [{}]: {} (duration: {:?})",
error_type,
summarize_error(&e),
connect_duration
);
if max_retries > 0 && attempt >= max_retries {
tracing::error!("Max retries reached: {}", max_retries);
if !quiet {
eprintln!(
"❌ Connection failed, max retries reached ({})",
max_retries
);
eprintln!(" Error type: {}", error_type);
eprintln!(" Error detail: {}", e);
}
// 生成最终诊断报告
print_diagnostic_report(
"SSE",
&config.url,
0,
"Connection failed: max retries reached",
Some(&error_type),
args.logging.diagnostic,
);
break;
}
if !quiet {
if max_retries == 0 {
eprintln!(
"⚠️ [{}] Connection failed [{}]: {}; retrying in {}s (attempt #{})...",
EVENT_RETRY_BACKOFF,
error_type,
summarize_error(&e),
backoff_secs,
attempt
);
} else {
eprintln!(
"⚠️ [{}] Connection failed [{}]: {}; retrying in {}s ({}/{})...",
EVENT_RETRY_BACKOFF,
error_type,
summarize_error(&e),
backoff_secs,
attempt,
max_retries
);
}
}
if verbose && !quiet {
eprintln!(" Full error: {}", e);
}
}
}
tracing::debug!("Waiting {}s before next reconnect attempt", backoff_secs);
tokio::time::sleep(Duration::from_secs(backoff_secs)).await;
backoff_secs = (backoff_secs * 2).min(MAX_BACKOFF_SECS);
}
tracing::info!("SSE watchdog exited");
}
/// 监控 SSE 连接健康状态
///
/// 委托给 common::monitor_connection_health 公共函数
async fn monitor_sse_connection(
handler: &ProxyHandler,
ping_interval: u64,
ping_timeout: u64,
quiet: bool,
) -> String {
super::common::monitor_connection_health(handler, ping_interval, ping_timeout, quiet, "SSE")
.await
}

View File

@@ -0,0 +1,405 @@
//! Stream 模式处理
//!
//! Streamable HTTP 协议的实现和连接管理
use anyhow::Result;
use std::sync::Arc;
use std::time::Duration;
use super::common::HealthChecker;
use crate::client::support::{
ConvertArgs, classify_error, print_diagnostic_report, summarize_error, truncate_str,
};
use crate::proxy::{McpClientConfig, StreamClientConnection, StreamProxyHandler, ToolFilter};
use mcp_streamable_proxy::{ServiceExt, stdio as stream_stdio};
/// 为 StreamProxyHandler 实现 HealthChecker trait
impl HealthChecker for StreamProxyHandler {
fn is_backend_available(&self) -> bool {
self.is_backend_available()
}
async fn is_terminated_async(&self) -> bool {
self.is_terminated_async().await
}
}
/// Stream 模式处理(使用 mcp-streamable-proxyrmcp 0.12
pub async fn run_stream_mode(
config: McpClientConfig,
args: ConvertArgs,
tool_filter: ToolFilter,
verbose: bool,
quiet: bool,
) -> Result<()> {
tracing::info!("========================================");
tracing::info!("Starting Stream mode");
tracing::info!("Target URL: {}", config.url);
tracing::info!(
"Ping config: interval={}s, timeout={}s",
args.ping_interval,
args.ping_timeout
);
tracing::info!("========================================");
if !quiet {
eprintln!("🔗 Connecting to backend service (Stream)...");
}
// 1. 使用高层 API 连接(带重试,防止初始连接因时序问题失败)
//
// 重试策略:最多 3 次,每次给 15s 连接超时,间隔 2s/4s
// 第1次最多 15s→ 失败 → 等 2s → 第2次最多 15s→ 失败 → 等 4s → 第3次最多 15s→ 退出
// 最坏总耗时15 + 2 + 15 + 4 + 15 = 51s但大多数失败会比 15s 更快返回)
// 通常总耗时:连接快速失败(~1s) + 2 + 快速失败(~1s) + 4 + 快速失败(~1s) = ~9s
let connect_timeout = Duration::from_secs(15);
const MAX_INITIAL_RETRIES: u32 = 3;
const INITIAL_BACKOFF_SECS: u64 = 2;
const MAX_BACKOFF_SECS: u64 = 4;
tracing::info!(
"Connecting to backend (per-attempt timeout: {}s, max retries: {})",
connect_timeout.as_secs(),
MAX_INITIAL_RETRIES
);
let connect_start = std::time::Instant::now();
let mut last_error = None;
let mut conn = None;
let mut backoff_secs = INITIAL_BACKOFF_SECS;
for attempt in 1..=MAX_INITIAL_RETRIES {
match tokio::time::timeout(
connect_timeout,
StreamClientConnection::connect(config.clone()),
)
.await
{
Ok(Ok(c)) => {
if attempt > 1 {
tracing::info!(
"Backend connection succeeded on attempt {}/{}",
attempt,
MAX_INITIAL_RETRIES
);
}
conn = Some(c);
break;
}
Ok(Err(e)) => {
tracing::warn!(
"Backend connection attempt {}/{} failed: {}",
attempt,
MAX_INITIAL_RETRIES,
e
);
last_error = Some(format!("Backend connection failed: {}", e));
}
Err(_) => {
tracing::warn!(
"Backend connection attempt {}/{} timed out ({}s)",
attempt,
MAX_INITIAL_RETRIES,
connect_timeout.as_secs()
);
last_error = Some(format!(
"Backend connection timeout ({}s)",
connect_timeout.as_secs()
));
}
}
if attempt < MAX_INITIAL_RETRIES {
tracing::info!("Retrying in {}s... (elapsed: {:?})", backoff_secs, connect_start.elapsed());
if !quiet {
eprintln!(
"⚠️ Connection attempt {}/{} failed, retrying in {}s...",
attempt, MAX_INITIAL_RETRIES, backoff_secs
);
}
tokio::time::sleep(Duration::from_secs(backoff_secs)).await;
backoff_secs = (backoff_secs * 2).min(MAX_BACKOFF_SECS);
}
}
let conn = conn.ok_or_else(|| {
let elapsed = connect_start.elapsed();
let msg = last_error.unwrap_or_else(|| "Unknown connection error".to_string());
tracing::error!(
"All {} connection attempts failed after {:?}: {}",
MAX_INITIAL_RETRIES, elapsed, msg
);
eprintln!(
"❌ All {} connection attempts failed after {:.1}s: {}",
MAX_INITIAL_RETRIES, elapsed.as_secs_f64(), msg
);
anyhow::anyhow!(msg)
})?;
let connect_duration = connect_start.elapsed();
tracing::info!(
"Backend connected successfully (duration: {:?})",
connect_duration
);
if !quiet {
eprintln!("✅ Backend connected successfully");
// 打印工具列表
print_stream_tools(&conn, quiet).await;
if args.ping_interval > 0 {
eprintln!(
"💓 Health ping: every {}s (timeout {}s)",
args.ping_interval, args.ping_timeout
);
}
}
// 2. 创建 handler消耗 conn
tracing::debug!("Creating ProxyHandler...");
let handler = Arc::new(conn.into_handler("cli".to_string(), tool_filter.clone()));
tracing::debug!("ProxyHandler created");
// 3. 启动 stdio server使用 stream_stdio即 rmcp 0.12 的 stdio
tracing::info!("Starting stdio server...");
let server = (*handler).clone().serve(stream_stdio()).await.map_err(|e| {
tracing::error!("Failed to start stdio server: {:?}", e);
eprintln!("❌ Failed to start stdio server: {}", e);
e
})?;
tracing::info!("Stdio server started");
if !quiet {
eprintln!("💡 Stdio server started, proxying traffic...");
}
// 4. 启动 watchdog 任务
let handler_for_watchdog = handler.clone();
let mut watchdog_handle = tokio::spawn(run_stream_watchdog(
handler_for_watchdog,
args,
config,
tool_filter,
verbose,
quiet,
));
tracing::debug!("Watchdog task started");
// 5. 等待 stdio server 退出
tracing::info!("Waiting for stdio/watchdog events...");
tokio::select! {
result = server.waiting() => {
tracing::info!("========================================");
tracing::info!("Stdio server exited (EOF)");
tracing::info!("========================================");
watchdog_handle.abort();
result?;
}
watchdog_result = &mut watchdog_handle => {
tracing::info!("========================================");
tracing::info!("Watchdog task exited");
tracing::info!("========================================");
if let Err(e) = watchdog_result
&& !e.is_cancelled()
{
tracing::error!("Stream Watchdog task failed: {:?}", e);
}
}
}
tracing::info!("Stream mode exited normally");
Ok(())
}
/// 打印 Stream 连接的工具列表
async fn print_stream_tools(conn: &StreamClientConnection, quiet: bool) {
if quiet {
return;
}
match conn.list_tools().await {
Ok(tools) => {
if tools.is_empty() {
eprintln!("⚠️ Tool list is empty (tools/list returned 0 tools)");
} else {
eprintln!("🔧 Available tools ({}):", tools.len());
for tool in &tools {
let desc = tool.description.as_deref().unwrap_or("no description");
let desc_short = truncate_str(desc, 50);
eprintln!(" - {} : {}", tool.name, desc_short);
}
}
}
Err(e) => {
eprintln!("⚠️ Failed to list tools: {}", e);
}
}
}
/// Stream 模式的 watchdog负责监控连接健康、断开时重连
async fn run_stream_watchdog(
handler: Arc<StreamProxyHandler>,
args: ConvertArgs,
config: McpClientConfig,
_tool_filter: ToolFilter,
verbose: bool,
quiet: bool,
) {
let max_retries = args.retries;
let mut attempt = 0u32;
let mut backoff_secs = 1u64;
const MAX_BACKOFF_SECS: u64 = 30;
const EVENT_DISCONNECTED: &str = "EVENT_DISCONNECTED";
const EVENT_RECONNECTED: &str = "EVENT_RECONNECTED";
const EVENT_RETRY_BACKOFF: &str = "EVENT_RETRY_BACKOFF";
let initial_connection_start = std::time::Instant::now();
// 首先监控现有连接的健康状态
let disconnect_reason =
monitor_stream_connection(&handler, args.ping_interval, args.ping_timeout, quiet).await;
// 连接断开,标记后端不可用
handler.swap_backend(None);
let alive_duration = initial_connection_start.elapsed();
if !quiet {
eprintln!(
"⚠️ [{}] Connection disconnected: {}",
EVENT_DISCONNECTED, disconnect_reason
);
}
// 生成诊断报告(首次断开)
print_diagnostic_report(
"Streamable HTTP",
&config.url,
alive_duration.as_secs(),
&disconnect_reason,
None,
args.logging.diagnostic,
);
// 进入重连循环
loop {
attempt += 1;
if !quiet {
eprintln!("🔗 Reconnecting (attempt #{})...", attempt);
}
// 尝试建立连接
let connect_result = StreamClientConnection::connect(config.clone()).await;
match connect_result {
Ok(conn) => {
// 连接成功,获取 RunningService 并热替换后端
let running = conn.into_running_service();
handler.swap_backend(Some(running));
backoff_secs = 1;
if !quiet {
eprintln!(
"✅ [{}] Reconnected, proxy service resumed",
EVENT_RECONNECTED
);
}
// 监控连接健康
let reconnect_start = std::time::Instant::now();
let disconnect_reason = monitor_stream_connection(
&handler,
args.ping_interval,
args.ping_timeout,
quiet,
)
.await;
// 连接断开,标记后端不可用
handler.swap_backend(None);
let reconnect_alive_duration = reconnect_start.elapsed();
if !quiet {
eprintln!(
"⚠️ [{}] Connection disconnected: {}",
EVENT_DISCONNECTED, disconnect_reason
);
}
// 生成诊断报告(重连后断开)
print_diagnostic_report(
"Streamable HTTP",
&config.url,
reconnect_alive_duration.as_secs(),
&disconnect_reason,
None,
args.logging.diagnostic,
);
}
Err(e) => {
let error_type = classify_error(&e);
if max_retries > 0 && attempt >= max_retries {
if !quiet {
eprintln!(
"❌ Connection failed, max retries reached ({})",
max_retries
);
eprintln!(" Error type: {}", error_type);
eprintln!(" Error detail: {}", e);
}
// 生成最终诊断报告
print_diagnostic_report(
"Streamable HTTP",
&config.url,
0,
"Connection failed: max retries reached",
Some(&error_type),
args.logging.diagnostic,
);
break;
}
if !quiet {
if max_retries == 0 {
eprintln!(
"⚠️ [{}] Connection failed [{}]: {}; retrying in {}s (attempt #{})...",
EVENT_RETRY_BACKOFF,
error_type,
summarize_error(&e),
backoff_secs,
attempt
);
} else {
eprintln!(
"⚠️ [{}] Connection failed [{}]: {}; retrying in {}s ({}/{})...",
EVENT_RETRY_BACKOFF,
error_type,
summarize_error(&e),
backoff_secs,
attempt,
max_retries
);
}
}
if verbose && !quiet {
eprintln!(" Full error: {}", e);
}
}
}
tokio::time::sleep(Duration::from_secs(backoff_secs)).await;
backoff_secs = (backoff_secs * 2).min(MAX_BACKOFF_SECS);
}
}
/// 监控 Stream 连接健康状态
///
/// 委托给 common::monitor_connection_health 公共函数
async fn monitor_stream_connection(
handler: &StreamProxyHandler,
ping_interval: u64,
ping_timeout: u64,
quiet: bool,
) -> String {
super::common::monitor_connection_health(handler, ping_interval, ping_timeout, quiet, "Stream")
.await
}

View File

@@ -0,0 +1,20 @@
// MCP 客户端模块
// 提供各种 MCP 协议的客户端实现和 CLI 工具
mod cli;
mod protocol;
pub(crate) mod proxy_server;
// 新的模块化架构 (按功能层次分组)
pub mod cli_impl; // CLI 命令实现
pub mod core; // 核心业务逻辑
pub mod support; // 支持功能
#[cfg(test)]
mod tests;
// 导出 CLI 功能(公共 API
pub use cli::{Cli, Commands, run_cli};
// 注意ConvertArgs, CheckArgs, DetectArgs 等类型只在内部使用,
// 不需要在这里重新导出。如需使用,请通过 support 模块导入。

View File

@@ -0,0 +1,15 @@
// MCP 协议检测模块
// 用于自动检测远程 MCP 服务的协议类型
// 复用 server 模块的协议检测逻辑
use anyhow::Result;
// 复用 model 中的协议类型定义
pub use crate::model::McpProtocol;
/// 自动检测 MCP 协议类型
///
/// 直接复用 server 模块的协议检测逻辑
pub async fn detect_mcp_protocol(url: &str) -> Result<McpProtocol> {
crate::server::detect_mcp_protocol(url).await
}

View File

@@ -0,0 +1,378 @@
//! MCP Proxy Server - 将 stdio MCP 服务代理为 HTTP/SSE 或 Streamable HTTP 服务
//!
//! 支持多个 agent 复用同一个 MCP 服务
use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::Once;
use std::time::Duration;
use anyhow::{Result, bail};
use clap::Parser;
use serde::Deserialize;
use tracing::{error, info, warn};
use crate::client::support::{LoggingArgs, init_logging_with_config};
use crate::proxy::ToolFilter;
/// Panic hook 初始化标志(确保只设置一次)
static INIT_PANIC_HOOK: Once = Once::new();
/// 最大重试次数 (0 = 无限重试)
const MAX_RETRIES: u32 = 0;
/// 初始重试间隔(秒)
const INITIAL_RETRY_DELAY_SECS: u64 = 3;
/// 最大重试间隔(秒)
const MAX_RETRY_DELAY_SECS: u64 = 30;
/// 输出协议类型
#[derive(clap::ValueEnum, Clone, Debug, Default)]
pub enum ProxyProtocol {
/// SSE 协议
Sse,
/// Streamable HTTP 协议
#[default]
Stream,
}
/// 代理模式参数 - 将 stdio MCP 服务代理为 HTTP 服务
#[derive(Parser, Debug, Clone)]
pub struct ProxyArgs {
/// 监听端口
#[arg(short, long, default_value = "8080", help = "监听端口")]
pub port: u16,
/// 监听地址
#[arg(long, default_value = "127.0.0.1", help = "监听地址")]
pub host: String,
/// MCP 服务名称(当配置包含多个服务时必需)
#[arg(short, long, help = "MCP 服务名称(多服务配置时必需)")]
pub name: Option<String>,
/// MCP 服务配置 JSON
#[arg(long, conflicts_with = "config_file", help = "MCP 服务配置 JSON")]
pub config: Option<String>,
/// MCP 服务配置文件路径
#[arg(long, conflicts_with = "config", help = "MCP 服务配置文件路径")]
pub config_file: Option<PathBuf>,
/// 输出协议类型
#[arg(long, value_enum, default_value = "stream", help = "输出协议类型")]
pub protocol: ProxyProtocol,
/// SSE 端点路径(仅 SSE 协议)
#[arg(long, default_value = "/sse", help = "SSE 端点路径")]
pub sse_path: String,
/// 消息端点路径(仅 SSE 协议)
#[arg(long, default_value = "/message", help = "消息端点路径")]
pub message_path: String,
/// 工具白名单(逗号分隔),只允许指定的工具
#[arg(long, value_delimiter = ',', help = "工具白名单(逗号分隔)")]
pub allow_tools: Option<Vec<String>>,
/// 工具黑名单(逗号分隔),排除指定的工具
#[arg(long, value_delimiter = ',', help = "工具黑名单(逗号分隔)")]
pub deny_tools: Option<Vec<String>>,
/// 日志配置(使用通用结构)
#[command(flatten)]
pub logging: LoggingArgs,
}
/// MCP 配置格式
#[derive(Deserialize, Debug)]
struct McpConfig {
#[serde(rename = "mcpServers")]
mcp_servers: HashMap<String, StdioConfig>,
}
/// stdio 配置
#[derive(Deserialize, Debug, Clone)]
struct StdioConfig {
command: String,
args: Option<Vec<String>>,
env: Option<HashMap<String, String>>,
}
/// 解析后的服务配置(包含服务名)
struct ParsedConfig {
name: String,
config: StdioConfig,
}
/// 运行代理命令
pub async fn run_proxy_command(args: ProxyArgs, verbose: bool, quiet: bool) -> Result<()> {
// 设置 panic hook 以记录详细的 panic 信息(只设置一次)
INIT_PANIC_HOOK.call_once(|| {
std::panic::set_hook(Box::new(|panic_info| {
let backtrace = std::backtrace::Backtrace::capture();
error!(
"[PANIC] Program panic - Location: {}:{}, Message: {:?}",
panic_info.location().map(|l| l.file()).unwrap_or("unknown"),
panic_info.location().map(|l| l.line()).unwrap_or(0),
panic_info.payload().downcast_ref::<String>()
);
error!("[PANIC] Stack trace:\\n{:?}", backtrace);
}));
});
// 1. 验证互斥参数
if args.allow_tools.is_some() && args.deny_tools.is_some() {
bail!("--allow-tools 和 --deny-tools 不能同时使用,请只选择其中一个");
}
// 2. 解析配置
let parsed = parse_config(&args)?;
// 2.5 初始化镜像源环境变量MCP_PROXY_NPM_REGISTRY → npm_config_registry 等)
// 必须在日志初始化之前、单线程阶段调用
{
let mirror = mcp_common::mirror::MirrorConfig::from_env();
if !mirror.is_empty() {
mirror.apply_to_process_env();
}
}
// 3. 初始化日志系统(在启动服务之前)
init_logging_with_config(&args.logging, Some(&parsed.name), quiet, verbose)?;
// 4. 创建工具过滤器
let tool_filter = if let Some(allow_tools) = args.allow_tools.clone() {
ToolFilter::allow(allow_tools)
} else if let Some(deny_tools) = args.deny_tools.clone() {
ToolFilter::deny(deny_tools)
} else {
ToolFilter::default()
};
let protocol_name = match args.protocol {
ProxyProtocol::Sse => "SSE",
ProxyProtocol::Stream => "Streamable HTTP",
};
// 记录服务启动信息到日志文件
info!(
"[Service startup] MCP Proxy service startup - protocol: {}, service name: {}, command: {} {:?}",
protocol_name,
parsed.name,
parsed.config.command,
parsed.config.args.as_ref().unwrap_or(&vec![])
);
if let Some(ref allow_tools) = args.allow_tools {
info!("[Service startup] Tool whitelist: {:?}", allow_tools);
}
if let Some(ref deny_tools) = args.deny_tools {
info!("[Service startup] Tool blacklist: {:?}", deny_tools);
}
if !quiet {
eprintln!("🚀 MCP Proxy Service");
eprintln!("Protocol type: {}", protocol_name);
eprintln!("Service name: {}", parsed.name);
eprintln!(
"Command: {} {:?}",
parsed.config.command,
parsed.config.args.as_ref().unwrap_or(&vec![])
);
if verbose && let Some(ref env) = parsed.config.env {
eprintln!("Environment variable: {:?}", env);
}
// 显示过滤器配置
if let Some(ref allow_tools) = args.allow_tools {
eprintln!("Tool whitelist: {:?}", allow_tools);
}
if let Some(ref deny_tools) = args.deny_tools {
eprintln!("Tool blacklist: {:?}", deny_tools);
}
}
// 5. 端口提前绑定(在重试循环之前),确保 ServiceManager 的 TCP 健康检查能检测到进程存活
let bind_addr = format!("{}:{}", args.host, args.port);
let std_listener = std::net::TcpListener::bind(&bind_addr)
.map_err(|e| anyhow::anyhow!("端口绑定失败 {}: {}", bind_addr, e))?;
std_listener
.set_nonblocking(true)
.map_err(|e| anyhow::anyhow!("设置非阻塞失败: {}", e))?;
info!("[Port Binding] Binded {}", bind_addr);
// 6. 主循环 - 支持子进程崩溃后自动重启(指数退避,有上限)
let mut retry_count: u32 = 0;
let mut retry_delay = Duration::from_secs(INITIAL_RETRY_DELAY_SECS);
loop {
let result = run_proxy_server(
&args,
&parsed,
&std_listener,
tool_filter.clone(),
verbose,
quiet,
)
.await;
match result {
Ok(_) => {
// 正常退出(如 Ctrl+C
info!(
"[Service stopped] MCP Proxy service stopped normally - service name: {}",
parsed.name
);
if !quiet {
eprintln!("🛑 Service has been stopped");
}
break;
}
Err(e) => {
retry_count += 1;
// MAX_RETRIES = 0 表示无限重试,非零值表示最大重试次数
#[allow(clippy::absurd_extreme_comparisons)]
if MAX_RETRIES > 0 && retry_count >= MAX_RETRIES {
error!(
"[Service Termination] Maximum number of retries reached {}, service name: {}, last error: {}",
MAX_RETRIES, parsed.name, e
);
return Err(e);
}
let retry_info = if MAX_RETRIES == 0 {
format!("{}", retry_count)
} else {
format!("{}/{}", retry_count, MAX_RETRIES)
};
error!(
"[Service exception] MCP Proxy service exited abnormally - service name: {}, error: {}, {} and restarts after seconds ({})",
parsed.name,
e,
retry_delay.as_secs(),
retry_info
);
eprintln!(
"⚠️ Service exception: {}, {}, restart after seconds ({})...",
e,
retry_delay.as_secs(),
retry_info
);
tokio::time::sleep(retry_delay).await;
retry_delay =
std::cmp::min(retry_delay * 2, Duration::from_secs(MAX_RETRY_DELAY_SECS));
warn!(
"[Service Restart] Restarting MCP Proxy service - Service name: {}",
parsed.name
);
if !quiet {
eprintln!("🔄 Restarting service...");
}
continue;
}
}
}
Ok(())
}
/// 运行代理服务器(单次运行)
async fn run_proxy_server(
args: &ProxyArgs,
parsed: &ParsedConfig,
std_listener: &std::net::TcpListener,
tool_filter: ToolFilter,
_verbose: bool,
quiet: bool,
) -> Result<()> {
// Windows 上解析命令扩展名(如 npx -> npx.cmd
let resolved_command = mcp_common::resolve_windows_command(&parsed.config.command);
if resolved_command != parsed.config.command {
info!(
"[Command parsing] Windows command parsed: {} -> {}",
parsed.config.command, resolved_command
);
}
// 根据协议类型选择对应的库并启动服务器
// 每个库使用自己的 rmcp 版本创建完整的生命周期
match args.protocol {
ProxyProtocol::Sse => {
// 使用 mcp-sse-proxy 库rmcp 0.10
let config = mcp_sse_proxy::McpServiceConfig {
name: parsed.name.clone(),
command: resolved_command,
args: parsed.config.args.clone(),
env: parsed.config.env.clone(),
tool_filter: Some(tool_filter),
};
mcp_sse_proxy::run_sse_server_from_config(config, std_listener, quiet).await
}
ProxyProtocol::Stream => {
// 使用 mcp-streamable-proxy 库rmcp 0.12
let config = mcp_streamable_proxy::McpServiceConfig {
name: parsed.name.clone(),
command: resolved_command,
args: parsed.config.args.clone(),
env: parsed.config.env.clone(),
tool_filter: Some(tool_filter),
};
mcp_streamable_proxy::run_stream_server_from_config(config, std_listener, quiet).await
}
}
}
// Note: 两个库现在都使用 mcp-common::ToolFilter所以直接传递即可
/// 解析配置
fn parse_config(args: &ProxyArgs) -> Result<ParsedConfig> {
// 1. 读取配置内容
let json_str = if let Some(ref config) = args.config {
config.clone()
} else if let Some(ref path) = args.config_file {
std::fs::read_to_string(path).map_err(|e| anyhow::anyhow!("读取配置文件失败: {}", e))?
} else {
bail!("必须提供 --config 或 --config-file 参数");
};
// 2. 解析配置
let mcp_config: McpConfig = serde_json::from_str(&json_str).map_err(|e| {
anyhow::anyhow!(
"配置解析失败: {}。配置必须是标准 MCP 格式,包含 mcpServers 字段",
e
)
})?;
let servers = mcp_config.mcp_servers;
if servers.is_empty() {
bail!("配置中没有找到任何 MCP 服务");
}
// 3. 根据服务数量和 --name 参数选择服务
if servers.len() == 1 {
// 单服务:自动使用,无需 --name
let (name, config) = servers.into_iter().next().unwrap();
Ok(ParsedConfig { name, config })
} else if let Some(ref name) = args.name {
// 多服务:根据 --name 选择
servers
.get(name)
.cloned()
.map(|config| ParsedConfig {
name: name.clone(),
config,
})
.ok_or_else(|| {
anyhow::anyhow!(
"服务 '{}' 不存在。可用服务: {:?}",
name,
servers.keys().collect::<Vec<_>>()
)
})
} else {
// 多服务但未指定 --name
bail!(
"配置包含多个服务 {:?},请使用 --name 指定要启动的服务",
servers.keys().collect::<Vec<_>>()
);
}
}

View File

@@ -0,0 +1,244 @@
//! CLI 参数定义
//!
//! 定义所有命令行参数结构体
use clap::Parser;
use std::path::PathBuf;
/// 通用日志配置参数
///
/// 用于多个命令之间共享日志配置
#[derive(Parser, Debug, Clone)]
pub struct LoggingArgs {
/// 启用详细诊断模式,输出连接和工具调用的详细时间信息(默认关闭
#[arg(
long,
default_value = "false",
help = "启用详细诊断模式,追踪连接生命周期和超时问题(默认关闭)"
)]
pub diagnostic: bool,
/// 日志输出目录(自动生成文件名)
#[arg(long, help = "日志输出目录,将自动生成日志文件名")]
pub log_dir: Option<PathBuf>,
/// 日志文件完整路径(手动指定)
#[arg(long, conflicts_with = "log_dir", help = "日志文件完整路径")]
pub log_file: Option<PathBuf>,
/// OTLP 追踪端点(如 http://localhost:4317
///
/// 启用 diagnostic 模式时,可配置此参数将追踪数据发送到 Jaeger 等 OTLP 兼容的后端。
/// 支持 gRPC (端口 4317) 和 HTTP (端口 4318) 协议。
#[arg(
long,
env = "OTEL_EXPORTER_OTLP_ENDPOINT",
help = "OTLP 追踪端点 (如 http://localhost:4317)"
)]
pub otlp_endpoint: Option<String>,
/// 追踪服务名称(用于 Jaeger 等追踪后端标识)
#[arg(
long,
default_value = "mcp-proxy",
help = "追踪服务名称(用于 Jaeger 等追踪后端标识)"
)]
pub service_name: String,
}
/// MCP-Proxy CLI 主命令结构
#[derive(Parser, Debug)]
#[command(name = "mcp-proxy")]
#[command(version = env!("CARGO_PKG_VERSION"))]
#[command(about = "MCP 协议转换代理工具", long_about = None)]
pub struct Cli {
#[command(subcommand)]
pub command: Option<Commands>,
/// 直接URL模式向后兼容
#[arg(value_name = "URL", help = "MCP 服务的 URL 地址(直接模式)")]
pub url: Option<String>,
/// 全局详细输出
#[arg(short, long, global = true)]
pub verbose: bool,
/// 全局静默模式
#[arg(short, long, global = true)]
pub quiet: bool,
}
#[derive(clap::Subcommand, Debug)]
pub enum Commands {
/// 协议转换模式 - 将 URL 转换为 stdio
Convert(ConvertArgs),
/// 检查服务状态
Check(CheckArgs),
/// 协议检测
Detect(DetectArgs),
/// 代理模式 - 将 stdio MCP 服务代理为 HTTP/SSE 服务
Proxy(crate::client::proxy_server::ProxyArgs),
/// 健康检查 - 验证 MCP 服务是否可用
Health(HealthArgs),
}
/// 协议转换参数
#[derive(Parser, Debug, Clone)]
pub struct ConvertArgs {
/// MCP 服务的 URL 地址(可选,与 --config/--config-file 二选一)
#[arg(value_name = "URL", help = "MCP 服务的 URL 地址")]
pub url: Option<String>,
/// MCP 服务配置 JSON
#[arg(long, conflicts_with = "config_file", help = "MCP 服务配置 JSON")]
pub config: Option<String>,
/// MCP 服务配置文件路径
#[arg(long, conflicts_with = "config", help = "MCP 服务配置文件路径")]
pub config_file: Option<PathBuf>,
/// MCP 服务名称(多服务配置时必需)
#[arg(short, long, help = "MCP 服务名称(多服务配置时必需)")]
pub name: Option<String>,
/// 指定远程服务协议类型(不指定则自动检测)
#[arg(long, value_enum, help = "指定远程服务协议类型(不指定则自动检测)")]
pub protocol: Option<crate::client::proxy_server::ProxyProtocol>,
/// 认证 header (如: "Bearer token")
#[arg(short, long, help = "认证 header")]
pub auth: Option<String>,
/// 自定义 HTTP headers
#[arg(short = 'H', long, value_parser = parse_key_val, help = "自定义 HTTP headers (KEY=VALUE 格式)")]
pub header: Vec<(String, String)>,
/// 重试次数
#[arg(long, default_value = "0", help = "重试次数0 表示无限重试")]
pub retries: u32,
/// 工具白名单(逗号分隔),只允许指定的工具
#[arg(
long,
value_delimiter = ',',
help = "工具白名单(逗号分隔),只允许指定的工具"
)]
pub allow_tools: Option<Vec<String>>,
/// 工具黑名单(逗号分隔),排除指定的工具
#[arg(
long,
value_delimiter = ',',
help = "工具黑名单(逗号分隔),排除指定的工具"
)]
pub deny_tools: Option<Vec<String>>,
/// 客户端 ping 间隔0 表示禁用
#[arg(
long,
default_value = "30",
help = "客户端 ping 间隔0 表示禁用"
)]
pub ping_interval: u64,
/// 客户端 ping 超时(秒)
#[arg(
long,
default_value = "10",
help = "客户端 ping 超时(秒),超时则认为连接断开"
)]
pub ping_timeout: u64,
/// 日志配置(使用通用结构)
#[command(flatten)]
pub logging: LoggingArgs,
}
/// 检查参数
#[derive(Parser, Debug)]
pub struct CheckArgs {
/// 要检查的 MCP 服务 URL
#[arg(value_name = "URL")]
pub url: String,
/// 认证 header
#[arg(short, long)]
pub auth: Option<String>,
/// 超时时间
#[arg(long, default_value = "10")]
pub timeout: u64,
}
/// 协议检测参数
#[derive(Parser, Debug)]
pub struct DetectArgs {
/// 要检测的 MCP 服务 URL
#[arg(value_name = "URL")]
pub url: String,
/// 认证 header
#[arg(short, long)]
pub auth: Option<String>,
}
/// 健康检查参数
#[derive(Parser, Debug)]
#[command(after_help = "\
退出码:
0 服务健康 - MCP 连接握手成功
1 服务不健康 - 连接失败、超时或握手失败
示例:
# 基本用法
mcp-proxy health http://localhost:8080/mcp
# 带认证
mcp-proxy health http://localhost:8080/mcp -a \"Bearer token123\"
# 指定协议和超时
mcp-proxy health http://localhost:8080/mcp --protocol sse --timeout 5
# 静默模式(仅返回退出码,适合脚本使用)
mcp-proxy health http://localhost:8080/mcp -q
# 在 shell 脚本中使用
if mcp-proxy health http://localhost:8080/mcp -q; then
echo \"MCP 服务正常\"
else
echo \"MCP 服务不可用\"
fi
")]
pub struct HealthArgs {
/// 要检查的 MCP 服务 URL
#[arg(value_name = "URL")]
pub url: String,
/// 认证 header (如: "Bearer token")
#[arg(short, long, help = "认证 header")]
pub auth: Option<String>,
/// 自定义 HTTP headers
#[arg(short = 'H', long, value_parser = parse_key_val, help = "自定义 HTTP headers (KEY=VALUE 格式)")]
pub header: Vec<(String, String)>,
/// 超时时间(秒)
#[arg(long, default_value = "10")]
pub timeout: u64,
/// 指定远程服务协议类型(不指定则自动检测)
#[arg(long, value_enum, help = "指定远程服务协议类型(不指定则自动检测)")]
pub protocol: Option<crate::client::proxy_server::ProxyProtocol>,
}
/// 解析 KEY=VALUE 格式的辅助函数
pub fn parse_key_val(s: &str) -> Result<(String, String), String> {
let pos = s
.find('=')
.ok_or_else(|| format!("无效的 KEY=VALUE 格式: {}", s))?;
Ok((s[..pos].to_string(), s[pos + 1..].to_string()))
}

View File

@@ -0,0 +1,197 @@
//! MCP 配置解析
//!
//! 解析 JSON 配置文件,支持多种服务配置格式
use anyhow::{Result, bail};
use serde::Deserialize;
use std::collections::HashMap;
use super::args::ConvertArgs;
/// 解析后的配置源
#[derive(Debug, Clone)]
pub enum McpConfigSource {
/// 直接 URL 模式(命令行参数)
DirectUrl { url: String },
/// 远程服务配置JSON 配置)
RemoteService {
name: String,
url: String,
protocol: Option<crate::client::protocol::McpProtocol>,
headers: HashMap<String, String>,
timeout: Option<u64>,
},
/// 本地命令配置JSON 配置)
LocalCommand {
name: String,
command: String,
args: Vec<String>,
env: HashMap<String, String>,
},
}
/// MCP 配置格式
#[derive(Deserialize, Debug)]
struct McpConfig {
#[serde(rename = "mcpServers")]
mcp_servers: HashMap<String, McpServerInnerConfig>,
}
/// MCP 服务配置(支持 Command 和 Url 两种类型)
#[derive(Deserialize, Debug, Clone)]
#[serde(untagged)]
enum McpServerInnerConfig {
Command(StdioConfig),
Url(UrlConfig),
}
/// stdio 配置(本地命令)
#[derive(Deserialize, Debug, Clone)]
struct StdioConfig {
command: String,
args: Option<Vec<String>>,
env: Option<HashMap<String, String>>,
}
/// URL 配置(远程服务)
#[derive(Deserialize, Debug, Clone)]
struct UrlConfig {
#[serde(skip_serializing_if = "Option::is_none")]
url: Option<String>,
#[serde(
skip_serializing_if = "Option::is_none",
default,
rename = "baseUrl",
alias = "baseurl",
alias = "base_url"
)]
base_url: Option<String>,
#[serde(default, rename = "type", alias = "Type")]
r#type: Option<String>,
pub headers: Option<HashMap<String, String>>,
#[serde(default, alias = "authToken", alias = "auth_token")]
pub auth_token: Option<String>,
pub timeout: Option<u64>,
}
impl UrlConfig {
fn get_url(&self) -> Option<&str> {
self.url.as_deref().or(self.base_url.as_deref())
}
}
/// 解析 convert 命令的配置
pub fn parse_convert_config(args: &ConvertArgs) -> Result<McpConfigSource> {
// 优先级url > config > config_file
if let Some(ref url) = args.url {
return Ok(McpConfigSource::DirectUrl { url: url.clone() });
}
// 读取 JSON 配置
let json_str = if let Some(ref config) = args.config {
config.clone()
} else if let Some(ref path) = args.config_file {
std::fs::read_to_string(path).map_err(|e| anyhow::anyhow!("读取配置文件失败: {}", e))?
} else {
bail!("必须提供 URL、--config 或 --config-file 参数之一");
};
// 解析 JSON 配置
let mcp_config: McpConfig = serde_json::from_str(&json_str).map_err(|e| {
anyhow::anyhow!(
"配置解析失败: {}。配置必须是标准 MCP 格式,包含 mcpServers 字段",
e
)
})?;
let servers = mcp_config.mcp_servers;
if servers.is_empty() {
bail!("配置中没有找到任何 MCP 服务");
}
// 选择服务
let (name, inner_config) = if let Some(ref name) = args.name {
// 用户指定了服务名称,必须严格匹配
let config = servers.get(name).cloned().ok_or_else(|| {
anyhow::anyhow!(
"服务 '{}' 不存在。可用服务: {:?}",
name,
servers.keys().collect::<Vec<_>>()
)
})?;
(name.clone(), config)
} else if servers.len() == 1 {
// 单服务且未指定名称,自动使用
servers.into_iter().next().unwrap()
} else {
// 多服务且未指定名称
bail!(
"配置包含多个服务 {:?},请使用 --name 指定要使用的服务",
servers.keys().collect::<Vec<_>>()
);
};
// 根据配置类型返回
match inner_config {
McpServerInnerConfig::Command(stdio) => Ok(McpConfigSource::LocalCommand {
name,
command: stdio.command,
args: stdio.args.unwrap_or_default(),
env: stdio.env.unwrap_or_default(),
}),
McpServerInnerConfig::Url(url_config) => {
let url = url_config
.get_url()
.ok_or_else(|| anyhow::anyhow!("URL 配置缺少 url 或 baseUrl 字段"))?
.to_string();
// 解析协议类型
let protocol =
url_config
.r#type
.as_ref()
.and_then(|t| match t.to_ascii_lowercase().as_str() {
"sse" => Some(crate::client::protocol::McpProtocol::Sse),
"http" | "stream" | "streamablehttp" | "streamable-http"
| "streamable_http" => Some(crate::client::protocol::McpProtocol::Stream),
_ => None,
});
// 合并 headersJSON 配置中的 auth_token -> Authorization
let mut headers = url_config.headers.clone().unwrap_or_default();
if let Some(auth_token) = &url_config.auth_token {
headers.insert("Authorization".to_string(), auth_token.clone());
}
Ok(McpConfigSource::RemoteService {
name,
url,
protocol,
headers,
timeout: url_config.timeout,
})
}
}
}
/// 合并 headersJSON 配置 + 命令行参数(命令行优先)
pub fn merge_headers(
config_headers: HashMap<String, String>,
cli_headers: &[(String, String)],
cli_auth: Option<&String>,
) -> HashMap<String, String> {
let mut merged = config_headers;
// 命令行 -H 参数覆盖配置
for (key, value) in cli_headers {
merged.insert(key.clone(), value.clone());
}
// 命令行 --auth 参数优先级最高
if let Some(auth_value) = cli_auth {
merged.insert("Authorization".to_string(), auth_value.clone());
}
merged
}

View File

@@ -0,0 +1,628 @@
//! 配置解析单元测试
//!
//! 测试各种 MCP JSON 配置格式的解析
use super::args::{ConvertArgs, LoggingArgs};
use super::config::{McpConfigSource, parse_convert_config};
#[cfg(test)]
mod config_parsing_tests {
use super::*;
/// 测试 1: 解析简单的本地命令配置command 类型)
#[test]
fn test_parse_simple_command_config() {
let config_json = r#"{
"mcpServers": {
"test-service": {
"command": "node",
"args": ["server.js"]
}
}
}"#;
let args = ConvertArgs {
url: None,
config: Some(config_json.to_string()),
config_file: None,
name: None,
protocol: None,
auth: None,
header: vec![],
retries: 0,
allow_tools: None,
deny_tools: None,
ping_interval: 30,
ping_timeout: 10,
logging: LoggingArgs {
diagnostic: true,
log_dir: None,
log_file: None,
otlp_endpoint: None,
service_name: "mcp-proxy".to_string(),
},
};
let result = parse_convert_config(&args);
assert!(result.is_ok(), "解析应该成功");
let config_source = result.unwrap();
match config_source {
McpConfigSource::LocalCommand {
name,
command,
args,
..
} => {
assert_eq!(name, "test-service");
assert_eq!(command, "node");
assert_eq!(args, vec!["server.js"]);
}
_ => panic!("应该解析为 LocalCommand 类型"),
}
}
/// 测试 2: 解析带环境变量的本地命令配置
#[test]
fn test_parse_command_config_with_env() {
let config_json = r#"{
"mcpServers": {
"my-service": {
"command": "python",
"args": ["-m", "mcp_server"],
"env": {
"API_KEY": "test-key",
"DEBUG": "true"
}
}
}
}"#;
let args = ConvertArgs {
url: None,
config: Some(config_json.to_string()),
config_file: None,
name: None,
protocol: None,
auth: None,
header: vec![],
retries: 0,
allow_tools: None,
deny_tools: None,
ping_interval: 30,
ping_timeout: 10,
logging: LoggingArgs {
diagnostic: true,
log_dir: None,
log_file: None,
otlp_endpoint: None,
service_name: "mcp-proxy".to_string(),
},
};
let result = parse_convert_config(&args);
assert!(result.is_ok());
let config_source = result.unwrap();
match config_source {
McpConfigSource::LocalCommand {
name,
command,
args,
env,
} => {
assert_eq!(name, "my-service");
assert_eq!(command, "python");
assert_eq!(args, vec!["-m", "mcp_server"]);
assert_eq!(env.get("API_KEY"), Some(&"test-key".to_string()));
assert_eq!(env.get("DEBUG"), Some(&"true".to_string()));
}
_ => panic!("应该解析为 LocalCommand 类型"),
}
}
/// 测试 3: 解析远程 URL 配置(使用 baseUrl
#[test]
fn test_parse_remote_service_config_baseurl() {
let config_json = r#"{
"mcpServers": {
"remote-service": {
"baseUrl": "https://api.example.com/mcp",
"headers": {
"X-Custom-Header": "custom-value"
},
"authToken": "Bearer token123",
"timeout": 30
}
}
}"#;
let args = ConvertArgs {
url: None,
config: Some(config_json.to_string()),
config_file: None,
name: None,
protocol: None,
auth: None,
header: vec![],
retries: 0,
allow_tools: None,
deny_tools: None,
ping_interval: 30,
ping_timeout: 10,
logging: LoggingArgs {
diagnostic: true,
log_dir: None,
log_file: None,
otlp_endpoint: None,
service_name: "mcp-proxy".to_string(),
},
};
let result = parse_convert_config(&args);
assert!(result.is_ok());
let config_source = result.unwrap();
match config_source {
McpConfigSource::RemoteService {
name,
url,
headers,
timeout,
..
} => {
assert_eq!(name, "remote-service");
assert_eq!(url, "https://api.example.com/mcp");
assert_eq!(
headers.get("X-Custom-Header"),
Some(&"custom-value".to_string())
);
assert_eq!(
headers.get("Authorization"),
Some(&"Bearer token123".to_string())
);
assert_eq!(timeout, Some(30));
}
_ => panic!("应该解析为 RemoteService 类型"),
}
}
/// 测试 4: 解析远程 URL 配置(使用 url 字段)
#[test]
fn test_parse_remote_service_config_url() {
let config_json = r#"{
"mcpServers": {
"sse-service": {
"url": "https://api.example.com/sse",
"type": "sse"
}
}
}"#;
let args = ConvertArgs {
url: None,
config: Some(config_json.to_string()),
config_file: None,
name: None,
protocol: None,
auth: None,
header: vec![],
retries: 0,
allow_tools: None,
deny_tools: None,
ping_interval: 30,
ping_timeout: 10,
logging: LoggingArgs {
diagnostic: true,
log_dir: None,
log_file: None,
otlp_endpoint: None,
service_name: "mcp-proxy".to_string(),
},
};
let result = parse_convert_config(&args);
assert!(result.is_ok());
let config_source = result.unwrap();
match config_source {
McpConfigSource::RemoteService {
name,
url,
protocol,
..
} => {
assert_eq!(name, "sse-service");
assert_eq!(url, "https://api.example.com/sse");
assert_eq!(format!("{:?}", protocol), "Some(Sse)"); // SSE 协议
}
_ => panic!("应该解析为 RemoteService 类型"),
}
}
/// 测试 5: 多服务配置,使用 --name 参数选择
#[test]
fn test_parse_multi_server_config_with_name() {
let config_json = r#"{
"mcpServers": {
"service-a": {
"command": "node",
"args": ["a.js"]
},
"service-b": {
"command": "python",
"args": ["b.py"]
},
"service-c": {
"baseUrl": "https://api.example.com/mcp"
}
}
}"#;
let args = ConvertArgs {
url: None,
config: Some(config_json.to_string()),
config_file: None,
name: Some("service-b".to_string()),
protocol: None,
auth: None,
header: vec![],
retries: 0,
allow_tools: None,
deny_tools: None,
ping_interval: 30,
ping_timeout: 10,
logging: LoggingArgs {
diagnostic: true,
log_dir: None,
log_file: None,
otlp_endpoint: None,
service_name: "mcp-proxy".to_string(),
},
};
let result = parse_convert_config(&args);
assert!(result.is_ok());
let config_source = result.unwrap();
match config_source {
McpConfigSource::LocalCommand { name, command, .. } => {
assert_eq!(name, "service-b");
assert_eq!(command, "python");
}
_ => panic!("应该解析为 LocalCommand 类型"),
}
}
/// 测试 6: 多服务配置,未指定 --name 参数应该失败
#[test]
fn test_parse_multi_server_config_without_name_should_fail() {
let config_json = r#"{
"mcpServers": {
"service-a": {
"command": "node"
},
"service-b": {
"command": "python"
}
}
}"#;
let args = ConvertArgs {
url: None,
config: Some(config_json.to_string()),
config_file: None,
name: None, // 未指定 name
protocol: None,
auth: None,
header: vec![],
retries: 0,
allow_tools: None,
deny_tools: None,
ping_interval: 30,
ping_timeout: 10,
logging: LoggingArgs {
diagnostic: true,
log_dir: None,
log_file: None,
otlp_endpoint: None,
service_name: "mcp-proxy".to_string(),
},
};
let result = parse_convert_config(&args);
assert!(result.is_err(), "多服务配置未指定 --name 应该失败");
let error_msg = result.unwrap_err().to_string();
assert!(
error_msg.contains("请使用 --name 指定"),
"错误消息应该提示使用 --name"
);
}
/// 测试 7: 指定的服务不存在应该失败
#[test]
fn test_parse_nonexistent_service_should_fail() {
let config_json = r#"{
"mcpServers": {
"service-a": {
"command": "node"
}
}
}"#;
let args = ConvertArgs {
url: None,
config: Some(config_json.to_string()),
config_file: None,
name: Some("nonexistent".to_string()), // 不存在的服务
protocol: None,
auth: None,
header: vec![],
retries: 0,
allow_tools: None,
deny_tools: None,
ping_interval: 30,
ping_timeout: 10,
logging: LoggingArgs {
diagnostic: true,
log_dir: None,
log_file: None,
otlp_endpoint: None,
service_name: "mcp-proxy".to_string(),
},
};
let result = parse_convert_config(&args);
assert!(result.is_err(), "不存在的服务应该失败");
let error_msg = result.unwrap_err().to_string();
assert!(
error_msg.contains("'nonexistent' 不存在"),
"错误消息应该提示服务不存在"
);
}
/// 测试 8: 空配置应该失败
#[test]
fn test_parse_empty_config_should_fail() {
let config_json = r#"{
"mcpServers": {}
}"#;
let args = ConvertArgs {
url: None,
config: Some(config_json.to_string()),
config_file: None,
name: None,
protocol: None,
auth: None,
header: vec![],
retries: 0,
allow_tools: None,
deny_tools: None,
ping_interval: 30,
ping_timeout: 10,
logging: LoggingArgs {
diagnostic: true,
log_dir: None,
log_file: None,
otlp_endpoint: None,
service_name: "mcp-proxy".to_string(),
},
};
let result = parse_convert_config(&args);
assert!(result.is_err(), "空配置应该失败");
let error_msg = result.unwrap_err().to_string();
assert!(
error_msg.contains("没有找到任何 MCP 服务"),
"错误消息应该提示没有服务"
);
}
/// 测试 9: URL 配置缺少 url 和 baseUrl 应该失败
#[test]
fn test_parse_url_config_without_url_should_fail() {
let config_json = r#"{
"mcpServers": {
"invalid-service": {
"type": "sse"
}
}
}"#;
let args = ConvertArgs {
url: None,
config: Some(config_json.to_string()),
config_file: None,
name: None,
protocol: None,
auth: None,
header: vec![],
retries: 0,
allow_tools: None,
deny_tools: None,
ping_interval: 30,
ping_timeout: 10,
logging: LoggingArgs {
diagnostic: true,
log_dir: None,
log_file: None,
otlp_endpoint: None,
service_name: "mcp-proxy".to_string(),
},
};
let result = parse_convert_config(&args);
assert!(result.is_err(), "缺少 URL 的配置应该失败");
let error_msg = result.unwrap_err().to_string();
assert!(
error_msg.contains("缺少 url 或 baseUrl"),
"错误消息应该提示缺少 URL"
);
}
/// 测试 10: 直接 URL 模式(不使用 JSON 配置)
#[test]
fn test_parse_direct_url_mode() {
let args = ConvertArgs {
url: Some("https://api.example.com/mcp".to_string()),
config: None,
config_file: None,
name: None,
protocol: None,
auth: None,
header: vec![],
retries: 0,
allow_tools: None,
deny_tools: None,
ping_interval: 30,
ping_timeout: 10,
logging: LoggingArgs {
diagnostic: true,
log_dir: None,
log_file: None,
otlp_endpoint: None,
service_name: "mcp-proxy".to_string(),
},
};
let result = parse_convert_config(&args);
assert!(result.is_ok());
let config_source = result.unwrap();
match config_source {
McpConfigSource::DirectUrl { url } => {
assert_eq!(url, "https://api.example.com/mcp");
}
_ => panic!("应该解析为 DirectUrl 类型"),
}
}
/// 测试 11: 既没有 URL 也没有配置应该失败
#[test]
fn test_parse_no_url_no_config_should_fail() {
let args = ConvertArgs {
url: None,
config: None,
config_file: None,
name: None,
protocol: None,
auth: None,
header: vec![],
retries: 0,
allow_tools: None,
deny_tools: None,
ping_interval: 30,
ping_timeout: 10,
logging: LoggingArgs {
diagnostic: true,
log_dir: None,
log_file: None,
otlp_endpoint: None,
service_name: "mcp-proxy".to_string(),
},
};
let result = parse_convert_config(&args);
assert!(result.is_err(), "既没有 URL 也没有配置应该失败");
let error_msg = result.unwrap_err().to_string();
assert!(
error_msg.contains("必须提供 URL"),
"错误消息应该提示需要提供配置"
);
}
/// 测试 12: 无效的 JSON 应该失败
#[test]
fn test_parse_invalid_json_should_fail() {
let invalid_json = r#"{
"mcpServers": {
"test": {
"command": "node"
}
// 缺少闭合括号
}"#;
let args = ConvertArgs {
url: None,
config: Some(invalid_json.to_string()),
config_file: None,
name: None,
protocol: None,
auth: None,
header: vec![],
retries: 0,
allow_tools: None,
deny_tools: None,
ping_interval: 30,
ping_timeout: 10,
logging: LoggingArgs {
diagnostic: true,
log_dir: None,
log_file: None,
otlp_endpoint: None,
service_name: "mcp-proxy".to_string(),
},
};
let result = parse_convert_config(&args);
assert!(result.is_err(), "无效的 JSON 应该失败");
let error_msg = result.unwrap_err().to_string();
assert!(
error_msg.contains("配置解析失败"),
"错误消息应该提示解析失败"
);
}
/// 测试 13: Stream 协议类型解析
#[test]
fn test_parse_stream_protocol_type() {
let config_json = r#"{
"mcpServers": {
"stream-service": {
"url": "https://api.example.com/mcp",
"type": "stream"
}
}
}"#;
let args = ConvertArgs {
url: None,
config: Some(config_json.to_string()),
config_file: None,
name: None,
protocol: None,
auth: None,
header: vec![],
retries: 0,
allow_tools: None,
deny_tools: None,
ping_interval: 30,
ping_timeout: 10,
logging: LoggingArgs {
diagnostic: true,
log_dir: None,
log_file: None,
otlp_endpoint: None,
service_name: "mcp-proxy".to_string(),
},
};
let result = parse_convert_config(&args);
assert!(result.is_ok());
let config_source = result.unwrap();
match config_source {
McpConfigSource::RemoteService { protocol, .. } => {
assert_eq!(format!("{:?}", protocol), "Some(Stream)");
}
_ => panic!("应该解析为 RemoteService 类型"),
}
}
}

View File

@@ -0,0 +1,211 @@
//! 诊断和错误处理
//!
//! 提供错误分类、诊断报告生成等功能
/// 错误分类
pub fn classify_error(e: &anyhow::Error) -> String {
let err_str = e.to_string().to_lowercase();
// 特殊识别 30 秒超时(可能是服务器限制)
if (err_str.contains("30") || err_str.contains("thirty"))
&& (err_str.contains("timeout") || err_str.contains("second") || err_str.contains(""))
{
"30-second timeout".to_string()
}
// 识别 503 服务不可用
else if err_str.contains("503") || err_str.contains("service unavailable") {
"503 Service Unavailable".to_string()
}
// 识别其他 HTTP 5xx 错误
else if err_str.contains("500") || err_str.contains("internal server error") {
"500 Internal Server Error".to_string()
} else if err_str.contains("502") || err_str.contains("bad gateway") {
"502 Bad Gateway".to_string()
} else if err_str.contains("504") || err_str.contains("gateway timeout") {
"504 Gateway Timeout".to_string()
}
// 识别 HTTP 4xx 错误
else if err_str.contains("401") || err_str.contains("unauthorized") {
"401 Unauthorized".to_string()
} else if err_str.contains("403") || err_str.contains("forbidden") {
"403 Forbidden".to_string()
} else if err_str.contains("404") || err_str.contains("not found") {
"404 Not Found".to_string()
} else if err_str.contains("408") || err_str.contains("request timeout") {
"408 Request Timeout".to_string()
}
// 通用超时
else if err_str.contains("timeout") || err_str.contains("timed out") {
"Timeout".to_string()
}
// 连接相关错误
else if err_str.contains("connection refused") {
"Connection Refused".to_string()
} else if err_str.contains("connection reset") {
"Connection Reset".to_string()
} else if err_str.contains("eof") || err_str.contains("closed") || err_str.contains("shutdown")
{
"Connection Closed".to_string()
}
// 网络相关错误
else if err_str.contains("dns") || err_str.contains("resolve") {
"DNS Resolution Failed".to_string()
} else if err_str.contains("certificate") || err_str.contains("ssl") || err_str.contains("tls")
{
"SSL/TLS Error".to_string()
} else if err_str.contains("sending request") || err_str.contains("network") {
"Network Error".to_string()
}
// 会话相关
else if err_str.contains("session") {
"Session Error".to_string()
} else {
"Unknown Error".to_string()
}
}
/// 简化错误信息(用于单行日志)
pub fn summarize_error(e: &anyhow::Error) -> String {
let full = e.to_string();
// 截取第一行或前80个字符
let first_line = full.lines().next().unwrap_or(&full);
// 使用 chars() 安全处理 UTF-8 字符,避免在多字节字符中间截断
if first_line.chars().count() > 80 {
format!("{}...", first_line.chars().take(77).collect::<String>())
} else {
first_line.to_string()
}
}
/// 生成诊断报告
pub fn print_diagnostic_report(
protocol: &str,
url: &str,
alive_duration_secs: u64,
disconnect_reason: &str,
error_type: Option<&str>,
diagnostic: bool,
) {
if !diagnostic {
return;
}
eprintln!("\n=== Connection Diagnostic Report ===");
eprintln!("Protocol: {protocol}");
// 隐藏 URL 中的敏感信息(如 token/ak/key/secret 参数)
let masked_url = if url.contains("?") {
let parts: Vec<&str> = url.split('?').collect();
if parts.len() == 2 {
let base = parts[0];
let params: Vec<&str> = parts[1].split('&').collect();
let masked_params: Vec<String> = params
.iter()
.map(|p| {
let lower = p.to_lowercase();
let key_part = lower.split('=').next().unwrap_or("");
if key_part.contains("key")
|| key_part.contains("token")
|| key_part.contains("secret")
|| key_part.contains("auth")
|| key_part.contains("password")
|| key_part.contains("passwd")
|| key_part.contains("credential")
|| key_part == "ak"
|| key_part == "sk"
{
let original_key = p.split('=').next().unwrap_or("");
format!("{}=***", original_key)
} else {
p.to_string()
}
})
.collect();
format!("{}?{}", base, masked_params.join("&"))
} else {
url.to_string()
}
} else {
url.to_string()
};
eprintln!("Service URL: {masked_url}");
eprintln!("Connection duration: {}s", alive_duration_secs);
eprintln!("Disconnect reason: {disconnect_reason}");
if let Some(err_type) = error_type {
eprintln!("Error type: {err_type}");
}
// 分析可能的原因
eprintln!("\nPossible causes:");
if (28..=32).contains(&alive_duration_secs) {
eprintln!(" Connection dropped around 30 seconds:");
eprintln!(" 1. The backend may enforce a fixed session timeout");
eprintln!(" 2. A load balancer or gateway may be closing idle connections");
eprintln!(" 3. Keepalive/ping settings may be too weak for this environment");
} else if alive_duration_secs < 10 {
eprintln!(" Quick disconnect ({}s):", alive_duration_secs);
eprintln!(" 1. Service may be unavailable or misconfigured");
eprintln!(" 2. Authentication/headers may be invalid");
eprintln!(" 3. URL/path may point to a non-MCP endpoint");
} else if alive_duration_secs >= 60 {
eprintln!(" Long-lived connection ({}s):", alive_duration_secs);
eprintln!(" 1. Disconnect may be caused by transient network instability");
eprintln!(" 2. Backend restarts or rolling deployments may interrupt sessions");
}
let timeout_30s = "30-second timeout";
let service_unavailable = "503 Service Unavailable";
if error_type == Some(timeout_30s) || error_type == Some(service_unavailable) {
eprintln!("\nSuggestions:");
eprintln!(" 1. Increase backend/read timeout settings");
eprintln!(" 2. Increase client ping timeout if the backend is slow");
eprintln!(" 3. Consider async/task-based invocation patterns");
eprintln!(" 4. Increase ping interval to {}s to reduce pressure", 120);
} else if disconnect_reason.contains("Ping") || disconnect_reason.contains("ping") {
eprintln!("\nSuggestions:");
eprintln!(" 1. Increase ping timeout to {}s", 30);
eprintln!(" 2. Increase ping interval to {}s", 60);
eprintln!(" 3. Disable ping if the backend does not support stable probes");
}
eprintln!("==============================\n");
}
#[cfg(test)]
mod tests {
use super::*;
use anyhow::anyhow;
#[test]
fn test_classify_error() {
assert_eq!(classify_error(&anyhow!("connection timeout")), "Timeout");
assert_eq!(
classify_error(&anyhow!("connection refused")),
"Connection Refused"
);
assert_eq!(
classify_error(&anyhow!("503 Service Unavailable")),
"503 Service Unavailable"
);
assert_eq!(
classify_error(&anyhow!("401 Unauthorized")),
"401 Unauthorized"
);
}
#[test]
fn test_summarize_error() {
let short_err = anyhow!("short error");
assert_eq!(summarize_error(&short_err), "short error");
let long_err = anyhow!(
"this is a very long error message that exceeds eighty characters and should be truncated"
);
let summary = summarize_error(&long_err);
assert!(summary.len() <= 80);
assert!(summary.ends_with("..."));
}
}

View File

@@ -0,0 +1,268 @@
//! 日志系统初始化
//!
//! 处理日志文件的创建、日志级别配置和 OpenTelemetry 追踪初始化
use anyhow::Result;
use mcp_common::{TracingConfig, TracingGuard};
use once_cell::sync::OnceCell;
use tracing_subscriber::{EnvFilter, fmt, prelude::*};
use super::args::{ConvertArgs, LoggingArgs};
/// 全局追踪守卫,保持 OTLP exporter 存活
static TRACING_GUARD: OnceCell<TracingGuard> = OnceCell::new();
/// 初始化日志系统
/// 只有在 diagnostic=true 时才会创建日志文件并输出详细日志
pub fn init_logging(
args: &ConvertArgs,
mcp_name: Option<&str>,
quiet: bool,
verbose: bool,
) -> Result<()> {
init_logging_with_config(&args.logging, mcp_name, quiet, verbose)
}
/// 使用日志配置初始化日志系统(更通用的接口)
///
/// 这个函数可以被任何需要日志初始化的地方调用,不依赖完整的 ConvertArgs
///
/// # 功能
///
/// - 根据 `diagnostic` 参数控制日志级别
/// - 支持日志文件输出
/// - 支持 OTLP 追踪(当配置了 `otlp_endpoint` 时)
pub fn init_logging_with_config(
logging: &LoggingArgs,
mcp_name: Option<&str>,
quiet: bool,
verbose: bool,
) -> Result<()> {
// 检查是否需要启用 OTLP 追踪
let enable_otlp = logging.diagnostic && logging.otlp_endpoint.is_some();
// 如果启用 OTLP先初始化 tracer provider
if enable_otlp {
init_otlp_tracing(logging, mcp_name, quiet)?;
}
// 只有在 diagnostic=true 或用户明确指定日志文件时才创建日志文件
let log_file_path = determine_log_file_path(logging, mcp_name)?;
// 根据不同的配置组合初始化 subscriber
match (log_file_path, enable_otlp) {
(Some(file_path), true) => {
init_with_file_and_otlp(logging, &file_path, quiet, verbose)?;
}
(Some(file_path), false) => {
init_with_file_only(logging, &file_path, quiet, verbose)?;
}
(None, true) => {
init_stderr_with_otlp(logging, quiet, verbose)?;
}
(None, false) => {
init_stderr_only(logging, quiet, verbose)?;
}
}
Ok(())
}
/// 确定日志文件路径
fn determine_log_file_path(
logging: &LoggingArgs,
mcp_name: Option<&str>,
) -> Result<Option<std::path::PathBuf>> {
if let Some(log_file) = &logging.log_file {
// 手动指定文件
Ok(Some(log_file.clone()))
} else if let Some(log_dir) = &logging.log_dir {
// 指定日志目录
let session_id = generate_session_id();
let date = chrono::Local::now().format("%Y%m%d");
let name_part = mcp_name.unwrap_or("unknown");
let filename = format!("mcp-proxy-{}-{}-{}.log", name_part, date, session_id);
std::fs::create_dir_all(log_dir)
.map_err(|e| anyhow::anyhow!("Failed to create log directory: {}", e))?;
Ok(Some(log_dir.join(filename)))
} else if logging.diagnostic {
// diagnostic=true 时,使用系统临时目录
let session_id = generate_session_id();
let date = chrono::Local::now().format("%Y%m%d");
let name_part = mcp_name.unwrap_or("unknown");
let filename = format!("mcp-proxy-{}-{}-{}.log", name_part, date, session_id);
let temp_dir = std::env::temp_dir();
let file_path = temp_dir.join(filename);
// 尝试创建文件,验证目录可写
std::fs::File::create(&file_path).map_err(|e| {
anyhow::anyhow!(
"Failed to create log file: {} (path: {})",
e,
file_path.display()
)
})?;
Ok(Some(file_path))
} else {
Ok(None)
}
}
/// 创建日志过滤器
fn create_filter(logging: &LoggingArgs, verbose: bool) -> EnvFilter {
EnvFilter::try_from_default_env().unwrap_or_else(|_| {
if logging.diagnostic {
EnvFilter::new("debug")
} else if verbose {
EnvFilter::new("info")
} else {
EnvFilter::new("warn")
}
})
}
/// 初始化:文件 + OTLP
fn init_with_file_and_otlp(
logging: &LoggingArgs,
file_path: &std::path::Path,
quiet: bool,
verbose: bool,
) -> Result<()> {
let file = std::fs::File::create(file_path)
.map_err(|e| anyhow::anyhow!("Failed to create log file: {}", e))?;
if !quiet {
eprintln!("📝 Log file: {}", file_path.display());
eprintln!(
"📋 Diagnostic mode: {} (log level: {})",
if logging.diagnostic {
"enabled"
} else {
"disabled"
},
if logging.diagnostic { "DEBUG" } else { "WARN" }
);
}
let file_shared = std::sync::Arc::new(file);
let filter = create_filter(logging, verbose);
tracing_subscriber::registry()
.with(filter)
.with(
fmt::layer()
.with_writer(std::sync::Mutex::new(file_shared.clone()))
.with_ansi(false),
)
.with(fmt::layer().with_writer(std::io::stderr).with_ansi(true))
.with(tracing_opentelemetry::layer())
.init();
Ok(())
}
/// 初始化:仅文件
fn init_with_file_only(
logging: &LoggingArgs,
file_path: &std::path::Path,
quiet: bool,
verbose: bool,
) -> Result<()> {
let file = std::fs::File::create(file_path)
.map_err(|e| anyhow::anyhow!("Failed to create log file: {}", e))?;
if !quiet {
eprintln!("📝 Log file: {}", file_path.display());
eprintln!(
"📋 Diagnostic mode: {} (log level: {})",
if logging.diagnostic {
"enabled"
} else {
"disabled"
},
if logging.diagnostic { "DEBUG" } else { "WARN" }
);
}
let file_shared = std::sync::Arc::new(file);
let filter = create_filter(logging, verbose);
tracing_subscriber::registry()
.with(filter)
.with(
fmt::layer()
.with_writer(std::sync::Mutex::new(file_shared.clone()))
.with_ansi(false),
)
.with(fmt::layer().with_writer(std::io::stderr).with_ansi(true))
.init();
Ok(())
}
/// 初始化stderr + OTLP
fn init_stderr_with_otlp(logging: &LoggingArgs, quiet: bool, verbose: bool) -> Result<()> {
if !quiet && !logging.diagnostic {
eprintln!("📋 Diagnostic mode: disabled (no log file will be created)");
}
let filter = create_filter(logging, verbose);
tracing_subscriber::registry()
.with(filter)
.with(fmt::layer().with_writer(std::io::stderr))
.with(tracing_opentelemetry::layer())
.init();
Ok(())
}
/// 初始化:仅 stderr
fn init_stderr_only(logging: &LoggingArgs, quiet: bool, verbose: bool) -> Result<()> {
if !quiet && !logging.diagnostic {
eprintln!("📋 Diagnostic mode: disabled (no log file will be created)");
}
let filter = create_filter(logging, verbose);
tracing_subscriber::registry()
.with(filter)
.with(fmt::layer().with_writer(std::io::stderr))
.init();
Ok(())
}
/// 初始化 OTLP 追踪Jaeger 等)
fn init_otlp_tracing(logging: &LoggingArgs, mcp_name: Option<&str>, quiet: bool) -> Result<()> {
if let Some(endpoint) = &logging.otlp_endpoint {
let service_name = mcp_name.unwrap_or(&logging.service_name);
let config = TracingConfig::new(service_name)
.with_otlp(endpoint)
.with_version(env!("CARGO_PKG_VERSION"));
let guard = mcp_common::init_tracing(&config)?;
// 保存到全局静态变量,确保 guard 在程序运行期间保持存活
let _ = TRACING_GUARD.set(guard);
if !quiet {
eprintln!("🔭 OTLP tracing: enabled");
eprintln!(" Endpoint: {}", endpoint);
eprintln!(" Service: {}", service_name);
}
}
Ok(())
}
/// 生成随机会话 ID8 位十六进制)
pub fn generate_session_id() -> String {
use rand::Rng;
let mut rng = rand::rng();
format!("{:08x}", rng.random::<u32>())
}

View File

@@ -0,0 +1,19 @@
//! 支持功能模块
//!
//! 提供配置、日志、工具函数等基础设施支持
pub mod args;
pub mod config;
pub mod diagnostic;
pub mod logging;
pub mod utils;
#[cfg(test)]
mod config_tests;
// 导出常用类型
pub use args::{CheckArgs, ConvertArgs, DetectArgs, HealthArgs, LoggingArgs};
pub use config::{McpConfigSource, merge_headers, parse_convert_config};
pub use diagnostic::{classify_error, print_diagnostic_report, summarize_error};
pub use logging::{init_logging, init_logging_with_config};
pub use utils::{protocol_name, truncate_str};

View File

@@ -0,0 +1,40 @@
//! 通用工具函数
/// 获取协议名称
pub fn protocol_name(protocol: &crate::client::protocol::McpProtocol) -> &'static str {
match protocol {
crate::client::protocol::McpProtocol::Sse => "SSE",
crate::client::protocol::McpProtocol::Stream => "Streamable HTTP",
crate::client::protocol::McpProtocol::Stdio => "Stdio",
}
}
/// 截断字符串UTF-8 安全)
pub fn truncate_str(s: &str, max_len: usize) -> String {
if s.chars().count() > max_len {
format!("{}...", s.chars().take(max_len - 3).collect::<String>())
} else {
s.to_string()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::client::protocol::McpProtocol;
#[test]
fn test_protocol_name() {
assert_eq!(protocol_name(&McpProtocol::Sse), "SSE");
assert_eq!(protocol_name(&McpProtocol::Stream), "Streamable HTTP");
assert_eq!(protocol_name(&McpProtocol::Stdio), "Stdio");
}
#[test]
fn test_truncate_str() {
assert_eq!(truncate_str("hello", 10), "hello");
assert_eq!(truncate_str("hello world", 8), "hello...");
assert_eq!(truncate_str("你好世界", 3), "...");
assert_eq!(truncate_str("你好世界", 6), "你好世界");
}
}

View File

@@ -0,0 +1,874 @@
// MCP 客户端模块测试 - 集成测试
// ============== 共享测试工具 ==============
#[cfg(test)]
mod test_helpers {
use serde_json::json;
use std::process::Stdio;
use std::time::Duration;
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
use tokio::process::{Child, Command};
use tokio::time::timeout;
/// 测试端口分配
pub const TEST_PORT_INTEGRATION: u16 = 19880; // integration_tests 使用
pub const TEST_PORT_PROTOCOL: u16 = 19881; // protocol detection 使用
pub const TEST_PORT_RECONNECT: u16 = 19876; // reconnection_tests 使用
/// 获取预编译的 test_mcp_server 二进制路径
///
/// 注意test_mcp_server 现在是 mcp-sse-proxy 的 example
/// 编译输出在 target/debug/examples/test_mcp_server
pub fn get_test_mcp_server_path() -> String {
let manifest_dir = env!("CARGO_MANIFEST_DIR");
let workspace_root = std::path::Path::new(manifest_dir).parent().unwrap();
workspace_root
.join("target/debug/examples/test_mcp_server")
.to_string_lossy()
.to_string()
}
/// 获取预编译的 mcp-proxy 二进制路径
pub fn get_mcp_proxy_path() -> String {
let manifest_dir = env!("CARGO_MANIFEST_DIR");
let workspace_root = std::path::Path::new(manifest_dir).parent().unwrap();
workspace_root
.join("target/debug/mcp-proxy")
.to_string_lossy()
.to_string()
}
/// 创建 MCP 服务配置 JSON使用预编译二进制
pub fn create_test_config() -> String {
let binary_path = get_test_mcp_server_path();
json!({
"mcpServers": {
"test-server": {
"command": binary_path,
"args": []
}
}
})
.to_string()
}
/// 启动 proxy 服务器
pub async fn spawn_proxy_server(port: u16) -> anyhow::Result<Child> {
let config = create_test_config();
let mcp_proxy_path = get_mcp_proxy_path();
let child = Command::new(&mcp_proxy_path)
.args([
"proxy",
"--port",
&port.to_string(),
"--host",
"127.0.0.1",
"--config",
&config,
])
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.kill_on_drop(true)
.spawn()?;
Ok(child)
}
/// 等待服务器就绪TCP 轮询)
pub async fn wait_for_server_ready(addr: &str, max_retries: u32) -> anyhow::Result<()> {
for i in 0..max_retries {
match tokio::net::TcpStream::connect(addr).await {
Ok(_) => {
println!("✅ Server ready (try #{})", i + 1);
return Ok(());
}
Err(_) => {
if i < max_retries - 1 {
tokio::time::sleep(Duration::from_millis(500)).await;
}
}
}
}
anyhow::bail!("服务器在 {} 次尝试后未就绪", max_retries)
}
/// 启动 convert 客户端进程
pub async fn spawn_convert_client(
url: &str,
ping_interval: u64,
ping_timeout: u64,
) -> anyhow::Result<Child> {
let mcp_proxy_path = get_mcp_proxy_path();
let child = Command::new(&mcp_proxy_path)
.args([
"convert",
url,
"--ping-interval",
&ping_interval.to_string(),
"--ping-timeout",
&ping_timeout.to_string(),
])
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.kill_on_drop(true)
.spawn()?;
Ok(child)
}
/// 监控 stderr 输出,查找任一日志模式
pub async fn wait_for_stderr_patterns(
stderr: &mut BufReader<tokio::process::ChildStderr>,
patterns: &[&str],
timeout_duration: Duration,
) -> anyhow::Result<bool> {
let result = timeout(timeout_duration, async {
let mut line = String::new();
loop {
line.clear();
match stderr.read_line(&mut line).await {
Ok(0) => return false, // EOF
Ok(_) => {
print!("[stderr] {}", line);
if patterns.iter().any(|pattern| line.contains(pattern)) {
return true;
}
}
Err(_) => return false,
}
}
})
.await;
match result {
Ok(found) => Ok(found),
Err(_) => Ok(false), // timeout
}
}
/// 发送 JSON-RPC 请求并获取响应
pub async fn send_jsonrpc_and_receive(
stdin: &mut tokio::process::ChildStdin,
stdout: &mut BufReader<tokio::process::ChildStdout>,
request: serde_json::Value,
) -> anyhow::Result<serde_json::Value> {
let msg = format!("{}\n", serde_json::to_string(&request)?);
stdin.write_all(msg.as_bytes()).await?;
stdin.flush().await?;
let mut response = String::new();
stdout.read_line(&mut response).await?;
let parsed: serde_json::Value = serde_json::from_str(&response)?;
Ok(parsed)
}
/// 初始化 MCP 客户端(发送 initialize + initialized
pub async fn initialize_mcp_client(
stdin: &mut tokio::process::ChildStdin,
stdout: &mut BufReader<tokio::process::ChildStdout>,
) -> anyhow::Result<()> {
// 发送 initialize 请求
let init_request = json!({
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {
"protocolVersion": "2024-11-05",
"capabilities": {
"roots": {"listChanged": true},
"sampling": {}
},
"clientInfo": {
"name": "test-client",
"version": "1.0.0"
}
}
});
let init_response = send_jsonrpc_and_receive(stdin, stdout, init_request).await?;
if init_response["error"].is_object() {
anyhow::bail!("Initialize failed: {:?}", init_response["error"]);
}
// 发送 initialized 通知
let initialized_notification = json!({
"jsonrpc": "2.0",
"method": "notifications/initialized"
});
let msg = format!("{}\n", serde_json::to_string(&initialized_notification)?);
stdin.write_all(msg.as_bytes()).await?;
stdin.flush().await?;
// 等待一下让服务器处理
tokio::time::sleep(Duration::from_millis(100)).await;
Ok(())
}
}
// ============== 集成测试 ==============
#[cfg(test)]
mod integration_tests {
use super::test_helpers::*;
use serde_json::json;
use std::time::Duration;
use tokio::io::BufReader;
/// 测试本地 MCP 服务连接和通信
///
/// 使用本地 test-mcp-server + mcp-proxy proxy 进行测试
/// 验证完整的 MCP 通信流程initialize -> tools/list -> tools/call
#[tokio::test]
async fn test_real_mcp_service_communication() {
println!("\\n========== Test: MCP service connection and communication ==========");
// 1. 启动本地 proxy 服务器
println!("🚀 Start local proxy server...");
let mut proxy = spawn_proxy_server(TEST_PORT_INTEGRATION)
.await
.expect("启动 proxy 失败");
let addr = format!("127.0.0.1:{}", TEST_PORT_INTEGRATION);
wait_for_server_ready(&addr, 20)
.await
.expect("服务器启动超时");
// 等待 proxy 完全初始化后端连接
tokio::time::sleep(Duration::from_secs(3)).await;
// 2. 启动 convert 客户端连接到本地 proxy
println!("🔗 Start convert client...");
let url = format!("http://{}", addr);
let mut client = spawn_convert_client(&url, 30, 10)
.await
.expect("启动 convert 失败");
let mut stdin = client.stdin.take().expect("获取 stdin 失败");
let stdout = client.stdout.take().expect("获取 stdout 失败");
let stderr = client.stderr.take().expect("获取 stderr 失败");
let mut stdout_reader = BufReader::new(stdout);
let mut stderr_reader = BufReader::new(stderr);
// 等待客户端连接成功
println!("⏳ Waiting for client to connect...");
let connected = wait_for_stderr_patterns(
&mut stderr_reader,
&["开始代理转换", "proxying traffic", "Stdio server started"],
Duration::from_secs(15),
)
.await
.expect("监控 stderr 失败");
if !connected {
println!("⚠️ No connection success log detected, trying to communicate directly...");
// 额外等待以确保连接建立
tokio::time::sleep(Duration::from_secs(2)).await;
}
// 3. 初始化 MCP 客户端
println!("🤝 Initialize MCP client...");
initialize_mcp_client(&mut stdin, &mut stdout_reader)
.await
.expect("初始化失败");
// 4. 发送 tools/list 请求
println!("📋 Get tool list...");
let tools_request = json!({
"jsonrpc": "2.0",
"id": 2,
"method": "tools/list"
});
let tools_response =
send_jsonrpc_and_receive(&mut stdin, &mut stdout_reader, tools_request)
.await
.expect("tools/list 请求失败");
assert_eq!(tools_response["jsonrpc"], "2.0");
assert_eq!(tools_response["id"], 2);
assert!(tools_response["result"]["tools"].is_array());
let tools = tools_response["result"]["tools"].as_array().unwrap();
println!("✅ Obtained {} tools", tools.len());
// 验证本地测试工具存在
let tool_names: Vec<&str> = tools.iter().filter_map(|t| t["name"].as_str()).collect();
println!("Tool list: {:?}", tool_names);
assert!(tool_names.contains(&"echo"), "应该包含 echo 工具");
assert!(tool_names.contains(&"increment"), "应该包含 increment 工具");
// 5. 测试调用 echo 工具
println!("🔧 Call the echo tool...");
let call_tool_request = json!({
"jsonrpc": "2.0",
"id": 3,
"method": "tools/call",
"params": {
"name": "echo",
"arguments": {
"message": "Hello from integration test!"
}
}
});
let call_response =
send_jsonrpc_and_receive(&mut stdin, &mut stdout_reader, call_tool_request)
.await
.expect("tools/call 请求失败");
assert_eq!(call_response["jsonrpc"], "2.0");
assert_eq!(call_response["id"], 3);
// 验证返回结果
if call_response["error"].is_object() {
panic!("Tool call failed with error: {:?}", call_response["error"]);
}
let result = &call_response["result"];
assert!(
!result["isError"].as_bool().unwrap_or(true),
"echo 调用不应该出错"
);
assert!(result["content"].is_array(), "Content should be an array");
let content = result["content"].as_array().unwrap();
assert!(!content.is_empty(), "Content should not be empty");
let first_content = &content[0];
assert_eq!(first_content["type"], "text");
let text = first_content["text"]
.as_str()
.expect("Should have text field");
assert!(
text.contains("Hello from integration test!"),
"Should echo our message"
);
println!("✅ Tool call successful! Response: {}", text);
// 6. 测试调用 increment 工具
println!("🔧 Call the increment tool...");
let increment_request = json!({
"jsonrpc": "2.0",
"id": 4,
"method": "tools/call",
"params": {
"name": "increment",
"arguments": {}
}
});
let increment_response =
send_jsonrpc_and_receive(&mut stdin, &mut stdout_reader, increment_request)
.await
.expect("increment 请求失败");
assert!(
!increment_response["result"]["isError"]
.as_bool()
.unwrap_or(true),
"increment 调用不应该出错"
);
println!("✅ increment call successful");
// 清理:关闭进程
println!("🧹 Cleaning process...");
drop(stdin);
let _ = client.kill().await;
let _ = proxy.kill().await;
println!("========== Test completed ==========\\n");
}
/// 测试协议检测功能
///
/// 使用本地 mcp-proxy proxy 服务测试协议检测
/// 本地 proxy 默认使用 Streamable HTTP 协议
#[tokio::test]
async fn test_protocol_detection() {
println!("\\n========== Test: Protocol Detection ==========");
// 1. 启动本地 proxy 服务器Streamable HTTP 模式)
println!("🚀 Start local proxy server...");
let mut proxy = spawn_proxy_server(TEST_PORT_PROTOCOL)
.await
.expect("启动 proxy 失败");
let addr = format!("127.0.0.1:{}", TEST_PORT_PROTOCOL);
wait_for_server_ready(&addr, 20)
.await
.expect("服务器启动超时");
// 2. 测试协议检测
println!("🔍Detect protocol type...");
let url = format!("http://{}", addr);
let protocol = crate::client::protocol::detect_mcp_protocol(&url).await;
assert!(protocol.is_ok(), "协议检测应该成功");
let protocol = protocol.unwrap();
use crate::client::protocol::McpProtocol;
// 本地 proxy 默认使用 Streamable HTTP
assert_eq!(
protocol,
McpProtocol::Stream,
"应该检测到 Streamable HTTP 协议"
);
println!("✅ Protocol detected: {:?}", protocol);
// 清理
println!("🧹 Cleaning process...");
let _ = proxy.kill().await;
println!("========== Test completed ==========\\n");
}
}
/// 本地重连测试模块
///
/// 使用 `mcp-proxy proxy` 启动本地服务,`mcp-proxy convert` 连接测试
/// 验证通道检查和自动重连逻辑
#[cfg(test)]
mod reconnection_tests {
use super::test_helpers::*;
use serde_json::json;
use std::time::Duration;
use tokio::io::{AsyncBufReadExt, BufReader};
use tokio::time::timeout;
/// 测试配置
const CLIENT_CONNECT_TIMEOUT: Duration = Duration::from_secs(15);
const RECONNECT_DETECT_TIMEOUT: Duration = Duration::from_secs(30);
/// 测试 1: 正常连接和通信
///
/// 验证:
/// - proxy 服务启动正常
/// - convert 客户端连接成功
/// - tools/list 请求正常响应
#[tokio::test]
async fn test_reconnection_normal_connection() {
println!("\\n========== Test 1: Normal connection and communication ==========");
// 1. 启动 proxy 服务器
println!("🚀 Start proxy server...");
let mut proxy = spawn_proxy_server(TEST_PORT_RECONNECT)
.await
.expect("启动 proxy 失败");
// 等待服务器就绪
let addr = format!("127.0.0.1:{}", TEST_PORT_RECONNECT);
wait_for_server_ready(&addr, 20)
.await
.expect("服务器启动超时");
// 2. 启动 convert 客户端
println!("🔗 Start convert client...");
let url = format!("http://{}", addr);
let mut client = spawn_convert_client(&url, 5, 3)
.await
.expect("启动 convert 失败");
let mut stdin = client.stdin.take().expect("获取 stdin 失败");
let stdout = client.stdout.take().expect("获取 stdout 失败");
let stderr = client.stderr.take().expect("获取 stderr 失败");
let mut stdout_reader = BufReader::new(stdout);
let mut stderr_reader = BufReader::new(stderr);
// 等待客户端连接成功(监控 stderr
println!("⏳ Waiting for client to connect...");
let connected = wait_for_stderr_patterns(
&mut stderr_reader,
&["连接成功", "Backend connected successfully"],
CLIENT_CONNECT_TIMEOUT,
)
.await
.expect("监控 stderr 失败");
if !connected {
// 可能连接很快,直接尝试初始化
println!("⚠️ No connection success log detected, trying to communicate directly...");
}
// 3. 初始化 MCP 客户端
println!("🤝 Initialize MCP client...");
initialize_mcp_client(&mut stdin, &mut stdout_reader)
.await
.expect("初始化失败");
// 4. 发送 tools/list 请求
println!("📋 Get tool list...");
let tools_request = json!({
"jsonrpc": "2.0",
"id": 2,
"method": "tools/list"
});
let tools_response =
send_jsonrpc_and_receive(&mut stdin, &mut stdout_reader, tools_request)
.await
.expect("tools/list 请求失败");
assert!(
tools_response["result"]["tools"].is_array(),
"应该返回工具列表"
);
let tools = tools_response["result"]["tools"].as_array().unwrap();
println!("✅ Obtained {} tools", tools.len());
// 验证我们的测试工具存在
let tool_names: Vec<&str> = tools.iter().filter_map(|t| t["name"].as_str()).collect();
assert!(tool_names.contains(&"echo"), "应该包含 echo 工具");
assert!(tool_names.contains(&"increment"), "应该包含 increment 工具");
// 5. 测试调用 increment 工具
println!("🔧 Call the increment tool...");
let call_request = json!({
"jsonrpc": "2.0",
"id": 3,
"method": "tools/call",
"params": {
"name": "increment",
"arguments": {}
}
});
let call_response = send_jsonrpc_and_receive(&mut stdin, &mut stdout_reader, call_request)
.await
.expect("tools/call 请求失败");
assert!(
!call_response["result"]["isError"].as_bool().unwrap_or(true),
"increment 调用不应该出错"
);
println!("✅ increment call successful");
// 清理
println!("🧹 Cleaning process...");
drop(stdin);
let _ = client.kill().await;
let _ = proxy.kill().await;
println!("========== Test 1 Complete ==========\\n");
}
/// 测试 2: 服务器重启后自动重连
///
/// 验证:
/// - 杀死 proxy 服务后convert 检测到断开
/// - 重启 proxy 后convert 自动重连
/// - 重连后功能正常
#[tokio::test]
async fn test_reconnection_on_server_restart() {
println!("\\n========== Test 2: Automatically reconnect after server restart ==========");
// 1. 启动 proxy 服务器
println!("🚀 Start proxy server...");
let mut proxy = spawn_proxy_server(TEST_PORT_RECONNECT + 1)
.await
.expect("启动 proxy 失败");
let addr = format!("127.0.0.1:{}", TEST_PORT_RECONNECT + 1);
wait_for_server_ready(&addr, 20)
.await
.expect("服务器启动超时");
// 2. 启动 convert 客户端(短 ping 间隔以快速检测断开)
println!("🔗 Start convert client...");
let url = format!("http://{}", addr);
let mut client = spawn_convert_client(&url, 2, 2)
.await
.expect("启动 convert 失败");
let stderr = client.stderr.take().expect("获取 stderr 失败");
let mut stderr_reader = BufReader::new(stderr);
// 等待客户端连接成功
println!("⏳ Waiting for client to connect...");
tokio::time::sleep(Duration::from_secs(5)).await;
// 3. 杀死 proxy 服务器
println!("💀 Kill proxy server...");
let _ = proxy.kill().await;
// 4. 等待客户端检测到断开
println!("⏳ Waiting for the client to detect the disconnect...");
let disconnected = wait_for_stderr_patterns(
&mut stderr_reader,
&["连接断开", "EVENT_DISCONNECTED", "Connection disconnected"],
RECONNECT_DETECT_TIMEOUT,
)
.await
.expect("监控 stderr 失败");
// 也可能是 "Ping 检测" 或 "后端连接已关闭"
if !disconnected {
println!(
"⚠️ No clear disconnection log detected, check if there are any reconnection attempts..."
);
}
// 5. 重启 proxy 服务器
println!("🔄 Restart proxy server...");
let mut proxy = spawn_proxy_server(TEST_PORT_RECONNECT + 1)
.await
.expect("重启 proxy 失败");
wait_for_server_ready(&addr, 20)
.await
.expect("服务器重启超时");
// 6. 等待客户端重连成功
println!("⏳ Waiting for the client to reconnect...");
let reconnected = wait_for_stderr_patterns(
&mut stderr_reader,
&[
"重连成功",
"EVENT_RECONNECTED",
"Reconnected, proxy service resumed",
],
RECONNECT_DETECT_TIMEOUT,
)
.await
.expect("监控 stderr 失败");
if reconnected {
println!("✅ The client has been reconnected successfully");
} else {
println!(
"⚠️ No reconnection success log detected (may have reconnected before timeout)"
);
}
// 清理
println!("🧹 Cleaning process...");
let _ = client.kill().await;
let _ = proxy.kill().await;
println!("========== Test 2 Complete ==========\\n");
}
/// 测试 3: 指数退避验证
///
/// 验证退避时间递增1s, 2s, 4s...
#[tokio::test]
async fn test_reconnection_exponential_backoff() {
println!("\\n========== Test 3: Exponential Backoff Verification ==========");
// 不启动服务器,直接启动客户端
// 客户端应该不断尝试连接并显示退避时间
println!("🔗 Start convert client (serverless)...");
let url = format!("http://127.0.0.1:{}", TEST_PORT_RECONNECT + 2);
let mut client = spawn_convert_client(&url, 30, 10)
.await
.expect("启动 convert 失败");
let stderr = client.stderr.take().expect("获取 stderr 失败");
let mut stderr_reader = BufReader::new(stderr);
// 监控退避日志
println!("⏳ Monitor the backoff log (wait about 15 seconds)...");
let mut backoff_times: Vec<String> = Vec::new();
let result = timeout(Duration::from_secs(15), async {
let mut line = String::new();
loop {
line.clear();
match stderr_reader.read_line(&mut line).await {
Ok(0) => break,
Ok(_) => {
print!("[stderr] {}", line);
// 查找退避时间日志(兼容中英文/事件标记)
if line.contains("秒后重连")
|| line.contains("retrying in")
|| line.contains("EVENT_RETRY_BACKOFF")
{
backoff_times.push(line.clone());
if backoff_times.len() >= 3 {
break;
}
}
}
Err(_) => break,
}
}
})
.await;
if result.is_err() {
println!("⚠️ Monitoring timeout");
}
println!("📊 Detected backoff log: {:?}", backoff_times);
// 验证退避时间递增
if backoff_times.len() >= 2 {
println!("✅ It has been detected that the backoff mechanism is working");
} else {
println!("⚠️ Not enough backoff logs detected");
}
// 清理
println!("🧹 Cleaning process...");
let _ = client.kill().await;
println!("========== Test 3 Complete ==========\\n");
}
/// 测试 4: 通道断开后请求是否立即返回错误
///
/// 验证:
/// - 服务器停止后,客户端发送请求能否立即返回错误
/// - 而不是空等超时
#[tokio::test]
async fn test_request_returns_error_when_connection_closed() {
println!(
"\\n========== Test 4: The request returns an error immediately after the channel is disconnected =========="
);
// 1. 启动 proxy 服务器
println!("🚀 Start proxy server...");
let mut proxy = spawn_proxy_server(TEST_PORT_RECONNECT + 3)
.await
.expect("启动 proxy 失败");
let addr = format!("127.0.0.1:{}", TEST_PORT_RECONNECT + 3);
wait_for_server_ready(&addr, 20)
.await
.expect("服务器启动超时");
// 2. 启动 convert 客户端(短 ping 间隔以快速检测断开)
println!("🔗 Start convert client...");
let url = format!("http://{}", addr);
let mut client = spawn_convert_client(&url, 2, 2)
.await
.expect("启动 convert 失败");
let mut stdin = client.stdin.take().expect("获取 stdin 失败");
let stdout = client.stdout.take().expect("获取 stdout 失败");
let stderr = client.stderr.take().expect("获取 stderr 失败");
let mut stdout_reader = BufReader::new(stdout);
let mut stderr_reader = BufReader::new(stderr);
// 后台监控 stderr
let stderr_monitor = tokio::spawn(async move {
let mut line = String::new();
loop {
line.clear();
match stderr_reader.read_line(&mut line).await {
Ok(0) => break,
Ok(_) => {
print!("[stderr] {}", line);
}
Err(_) => break,
}
}
});
// 等待连接建立
tokio::time::sleep(Duration::from_secs(3)).await;
// 3. 初始化 MCP 客户端
println!("🤝 Initialize MCP client...");
initialize_mcp_client(&mut stdin, &mut stdout_reader)
.await
.expect("初始化失败");
// 4. 发送第一个 tools/list 请求确认通信正常
println!("📋 Send first tools/list request (should succeed)...");
let tools_request = json!({
"jsonrpc": "2.0",
"id": 2,
"method": "tools/list"
});
let start = std::time::Instant::now();
let tools_response =
send_jsonrpc_and_receive(&mut stdin, &mut stdout_reader, tools_request)
.await
.expect("tools/list 请求失败");
let elapsed = start.elapsed();
assert!(
tools_response["result"]["tools"].is_array(),
"第一个请求应该成功返回工具列表"
);
println!(
"✅ The first request was successful and took time: {:?}",
elapsed
);
// 5. 杀死 proxy 服务器
println!("💀 Kill proxy server...");
let _ = proxy.kill().await;
// 等待 ping 检测发现连接断开ping 间隔是 2s超时也是 2s
println!("⏳ Wait for the ping to detect the disconnection (about 5 seconds)...");
tokio::time::sleep(Duration::from_secs(5)).await;
// 6. 发送第二个 tools/list 请求
println!("📋 Send a second tools/list request (should return an error quickly)...");
let tools_request2 = json!({
"jsonrpc": "2.0",
"id": 3,
"method": "tools/list"
});
let start = std::time::Instant::now();
let result = timeout(
Duration::from_secs(5),
send_jsonrpc_and_receive(&mut stdin, &mut stdout_reader, tools_request2),
)
.await;
let elapsed = start.elapsed();
match result {
Ok(Ok(response)) => {
// 检查是否是错误响应
if response["error"].is_object() {
println!("✅ Error response received, time taken: {:?}", elapsed);
println!("Error message: {:?}", response["error"]);
assert!(
elapsed < Duration::from_secs(3),
"错误响应应该在 3 秒内返回,实际耗时: {:?}",
elapsed
);
} else {
// 如果返回了成功响应,说明可能重连了
println!(
"⚠️ Successful response received (may have been reconnected), time taken: {:?}",
elapsed
);
}
}
Ok(Err(e)) => {
println!(
"✅ The request failed (as expected), time taken: {:?}",
elapsed
);
println!("Error: {}", e);
assert!(
elapsed < Duration::from_secs(3),
"错误应该在 3 秒内返回,实际耗时: {:?}",
elapsed
);
}
Err(_) => {
println!(
"❌ The request times out (5 seconds), indicating that the client is waiting!"
);
panic!("请求应该快速返回错误,而不是超时空等");
}
}
// 清理
println!("🧹 Cleaning process...");
drop(stdin);
stderr_monitor.abort();
let _ = client.kill().await;
println!("========== Test 4 Complete ==========\\n");
}
}

View File

@@ -0,0 +1,106 @@
use anyhow::Result;
use serde::Deserialize;
use std::env;
use std::fs::File;
use std::path::Path;
/// The default config file
const DEFAULT_CONFIG_YAML: &str = include_str!("../config.yml");
/// config.yml 中 mirror 段的结构
#[allow(dead_code)]
#[derive(Debug, Deserialize, Clone, Default)]
pub struct MirrorYamlConfig {
#[serde(default)]
pub npm_registry: String,
#[serde(default)]
pub pypi_index_url: String,
}
#[allow(dead_code)]
#[derive(Debug, Deserialize, Clone)]
pub struct AppConfig {
pub server: ServerConfig,
pub log: LogConfig,
#[serde(default)]
pub mirror: MirrorYamlConfig,
}
#[allow(dead_code)]
#[derive(Debug, Deserialize, Clone)]
pub struct ServerConfig {
/// The port to listen on for incoming connections
pub port: u16,
}
#[allow(dead_code)]
#[derive(Debug, Deserialize, Clone)]
pub struct LogConfig {
/// The log level to use
pub level: String,
/// The path to the log file
pub path: String,
/// The number of log files to retain (default: 20)
#[serde(default = "default_retain_days")]
pub retain_days: u32,
}
/// Default log files to retain
fn default_retain_days() -> u32 {
5
}
#[allow(dead_code)]
impl AppConfig {
/// Load the config file from the following sources:
/// 1. /app/config.yml
/// 2. config.yml
/// 3. BOT_SERVER_CONFIG environment variable
///
/// Environment variables can override config values:
/// - MCP_PROXY_PORT: Override server port
/// - MCP_PROXY_LOG_DIR: Override log directory path
/// - MCP_PROXY_LOG_LEVEL: Override log level
pub fn load_config() -> Result<Self> {
let mut config = match (
File::open("/app/config.yml"),
File::open("config.yml"),
env::var("BOT_SERVER_CONFIG"),
) {
(Ok(file), _, _) => serde_yaml::from_reader(file),
(_, Ok(file), _) => serde_yaml::from_reader(file),
(_, _, Ok(file_path)) => serde_yaml::from_reader(File::open(file_path)?),
_ => {
// 如果都没有,则使用默认配置
serde_yaml::from_str::<AppConfig>(DEFAULT_CONFIG_YAML)
}
}?;
// 环境变量覆盖配置(优先级最高)
if let Ok(port) = env::var("MCP_PROXY_PORT")
&& let Ok(port_num) = port.parse::<u16>()
{
config.server.port = port_num;
}
if let Ok(log_dir) = env::var("MCP_PROXY_LOG_DIR") {
config.log.path = log_dir;
}
if let Ok(log_level) = env::var("MCP_PROXY_LOG_LEVEL") {
config.log.level = log_level;
}
Ok(config)
}
pub fn log_path_init(&self) -> Result<()> {
let log_path = &self.log.path;
// 获取日志文件的父目录
if let Some(parent) = Path::new(log_path).parent()
&& !parent.exists()
{
std::fs::create_dir_all(parent)?;
}
Ok(())
}
}

View File

@@ -0,0 +1,44 @@
//! 进程环境初始化
//!
//! 在 mcp-proxy 启动早期调用,设置进程级环境变量:
//! - 镜像源npm_config_registry / UV_INDEX_URL
use crate::config::{AppConfig, MirrorYamlConfig};
/// 初始化进程环境(镜像源)
///
/// 在 main() 启动早期、日志初始化前调用。
/// 设置的环境变量会被所有子进程npx/uvx 等)自动继承。
pub fn init(app_config: &AppConfig) {
// 1. 镜像源配置
init_mirror(&app_config.mirror);
// 2. 汇总诊断日志
mcp_common::diagnostic::eprint_env_summary();
}
/// 从 config.yml + 环境变量合并镜像配置,设为进程级环境变量
fn init_mirror(yml: &MirrorYamlConfig) {
let mut config = mcp_common::mirror::MirrorConfig::from_env();
// config.yml 非空值作为默认(环境变量优先级更高)
if config.npm_registry.is_none() && !yml.npm_registry.is_empty() {
config.npm_registry = Some(yml.npm_registry.clone());
}
if config.pypi_index_url.is_none() && !yml.pypi_index_url.is_empty() {
config.pypi_index_url = Some(yml.pypi_index_url.clone());
}
if config.is_empty() {
eprintln!(" - Mirror: not configured");
return;
}
if let Some(ref npm) = config.npm_registry {
eprintln!(" - npm registry: {npm}");
}
if let Some(ref pypi) = config.pypi_index_url {
eprintln!(" - PyPI index: {pypi}");
}
config.apply_to_process_env();
}

View File

@@ -0,0 +1,40 @@
// 初始化 i18n使用 crate 内置翻译文件
#[macro_use]
extern crate rust_i18n;
// 初始化翻译文件,使用 crate 内置 locales支持独立发布
i18n!("locales", fallback = "en");
mod client;
mod config;
pub mod env_init;
mod mcp_error;
mod model;
mod proxy;
mod server;
#[cfg(test)]
mod tests;
// 导出基础功能
pub use config::AppConfig;
pub use mcp_error::AppError;
pub use model::{
AppState, DynamicRouterService, McpConfig, McpProtocol, McpType, ProxyHandlerManager,
get_proxy_manager,
};
pub use proxy::{McpHandler, ProxyHandler, StreamProxyHandler};
pub use proxy::{SseBackendConfig, SseServerBuilder, StreamBackendConfig, StreamServerBuilder};
pub use server::{
create_telemetry_layer, get_health, get_ready, get_router, init_tracer_provider,
log_service_info, mcp_start_task, schedule_check_mcp_live, set_layer, shutdown_telemetry,
start_schedule_task,
};
// 导出 CLI 功能
pub use client::{Cli, Commands, run_cli};
// 导出 i18n 功能
pub use mcp_common::{current_locale, init_locale_from_env, set_locale, t};
// 导出用于基准测试的组件
pub use server::handlers::run_code_handler::{RunCodeMessageRequest, run_code_handler};

View File

@@ -0,0 +1,355 @@
// Windows 平台配置:隐藏控制台窗口
// 当从 Tauri 等 GUI 应用启动时,不显示 CMD 窗口
// 注意:这会影响所有 Windows 平台的运行,独立运行时也不会有控制台输出
// 日志会写入文件(默认 ./logs/),可以通过日志文件查看运行状态
mod config;
use anyhow::Result;
use backtrace::Backtrace;
use clap::Parser;
use log::{error, info, warn};
use mcp_stdio_proxy::{
AppConfig, AppState, Cli, get_proxy_manager, get_router, init_locale_from_env,
init_tracer_provider, log_service_info, run_cli, start_schedule_task,
};
use run_code_rmcp::warm_up_all_envs;
use tokio::net::TcpListener;
use tokio::signal;
use tracing_appender::rolling::{Builder, Rotation};
use tracing_subscriber::layer::SubscriberExt as _;
use tracing_subscriber::util::SubscriberInitExt as _;
use tracing_subscriber::{EnvFilter, Layer as _};
#[tokio::main]
async fn main() -> Result<()> {
// 在最早期注册 panic hook确保 panic 信息一定输出到 stderr
// 当作为 stdio 子进程运行时(如 mcp-proxy convert父进程通过 stderr pipe 捕获此输出
std::panic::set_hook(Box::new(|info| {
let msg = if let Some(s) = info.payload().downcast_ref::<String>() {
s.clone()
} else if let Some(s) = info.payload().downcast_ref::<&str>() {
s.to_string()
} else {
"unknown panic".to_string()
};
let location = info
.location()
.map(|l| format!(" at {}:{}", l.file(), l.line()))
.unwrap_or_default();
eprintln!("❌ PANIC{}: {}", location, msg);
}));
// 初始化 Rustls CryptoProvider必须在任何使用 TLS 的代码之前)
rustls::crypto::ring::default_provider()
.install_default()
.expect("Failed to install rustls crypto provider");
// 解析命令行参数
let cli = Cli::parse();
// 如果有子命令,运行 CLI 模式
if cli.command.is_some() || cli.url.is_some() {
return run_cli_mode(cli).await;
}
// 否则运行传统的服务器模式
run_server_mode().await
}
/// 运行 CLI 模式
async fn run_cli_mode(cli: Cli) -> Result<()> {
// 初始化语言设置
init_locale_from_env();
// 检查是否是需要自定义日志初始化的命令
// convert 和 proxy 命令会根据自己的参数(--log-dir、--log-file初始化日志所以这里跳过
let is_convert_command = matches!(cli.command, Some(mcp_stdio_proxy::Commands::Convert(_)));
let is_proxy_command = matches!(cli.command, Some(mcp_stdio_proxy::Commands::Proxy(_)));
let is_health_command = matches!(cli.command, Some(mcp_stdio_proxy::Commands::Health(_)));
let has_custom_logging = is_convert_command || is_proxy_command;
// CLI 模式独立的日志配置
// 跳过会自己初始化日志的命令,避免重复初始化导致 panic
if !has_custom_logging && !cli.quiet {
// CLI 模式默认只显示错误,避免 info/debug 日志污染输出
let log_level = if cli.verbose {
"debug"
} else if is_health_command {
// health 命令使用更严格的过滤,屏蔽 rmcp 库的噪音日志
"off"
} else {
"error" // 默认只显示错误,屏蔽 info/warn/debug
};
// CLI 模式的日志配置:
// 1. 禁用 ANSI 颜色(避免污染 JSON
// 2. 输出到 stderrstdout 用于 JSON-RPC 通信)
// 3. 简化格式(无时间戳、无目标)
// 4. 优先使用 RUST_LOG 环境变量,否则使用默认日志级别
let env_filter = tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new(log_level));
tracing_subscriber::fmt()
.with_env_filter(env_filter)
.with_target(false)
.without_time()
.with_ansi(false)
.with_writer(std::io::stderr)
.compact()
.init();
}
// 运行 CLI 命令
let result = run_cli(cli).await;
if let Err(ref e) = result {
// 确保错误信息在进程退出前写到 stderr方便排查 stdio bridge 启动失败问题
eprintln!("❌ CLI command failed: {:?}", e);
}
result
}
/// 运行传统的服务器模式
async fn run_server_mode() -> Result<()> {
// 初始化语言设置
init_locale_from_env();
// 配置日志(保持原有的完整日志配置)
let app_config = AppConfig::load_config()?;
// 打印配置信息到 stderr在日志系统初始化之前
eprintln!("========================================");
eprintln!("Starting MCP proxy service...");
eprintln!("Version: {}", env!("CARGO_PKG_VERSION"));
eprintln!("Configuration loaded:");
eprintln!(" - Port: {}", app_config.server.port);
eprintln!(" - Log directory: {}", &app_config.log.path);
eprintln!(" - Log level: {}", &app_config.log.level);
eprintln!(" - Log retention days: {}", app_config.log.retain_days);
mcp_stdio_proxy::env_init::init(&app_config);
eprintln!("========================================");
app_config.log_path_init()?;
let log_level = app_config.log.level.clone();
let log_path = app_config.log.path.clone();
let server_port = app_config.server.port;
let retain_days = app_config.log.retain_days;
// 解析 RUST_LOG 环境变量
let log_level_for_console = log_level.clone();
let mut console_filter =
EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new(log_level_for_console));
// 修复 rmcp 库的 span clone panic 问题
// 过滤掉 rmcp 的 trace/debug 级别日志,只保留 warn/error避免 span 生命周期管理问题
// see: https://github.com/tokio-rs/tracing/issues/2778
console_filter = console_filter
.add_directive("rmcp=warn".parse().unwrap())
.add_directive("run_code_rmcp=warn".parse().unwrap());
// 使用 tracing-subscriber 初始化日志记录器
let console_layer = tracing_subscriber::fmt::layer()
.pretty()
.with_writer(std::io::stdout)
.with_filter(console_filter);
// 日志写入到文件,使用 Builder 模式配置日志轮转和保留策略
let log_path_for_file = log_path.clone();
let file_appender = Builder::new()
.rotation(Rotation::DAILY) // 按天滚动
.filename_prefix("log") // 文件名前缀
.max_log_files(retain_days as usize) // 保留最近 N 个日志文件
.build(&log_path_for_file)?;
let (non_blocking, _guard) = tracing_appender::non_blocking(file_appender);
let mut log_filter =
EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new(log_level));
// 修复 rmcp 库的 span clone panic 问题(同样应用于文件日志)
log_filter = log_filter
.add_directive("rmcp=warn".parse().unwrap())
.add_directive("run_code_rmcp=warn".parse().unwrap());
// 配置文件日志层:使用 compact 格式,避免显示完整的 span 嵌套链,减少日志膨胀
let file_layer = tracing_subscriber::fmt::layer()
.compact()
.with_ansi(false)
.with_writer(non_blocking)
.with_filter(log_filter);
// 初始化 OpenTelemetry tracer provider
init_tracer_provider("mcp-proxy", "0.1.0")?;
// 修复 rmcp 库的 span clone panic 问题(同样应用于 OpenTelemetry 层)
// 只为 warn 和以上级别创建 span避免过多的 span 导致 clone 问题
let telemetry_filter = EnvFilter::new("warn")
.add_directive("mcp_proxy=debug".parse().unwrap())
.add_directive("mcp_stdio_proxy=debug".parse().unwrap())
.add_directive("rmcp=error".parse().unwrap())
.add_directive("run_code_rmcp=error".parse().unwrap());
// 配置 OpenTelemetry添加过滤器以避免 span clone panic
let telemetry_layer = tracing_opentelemetry::layer().with_filter(telemetry_filter);
// 初始化 tracing 订阅器
tracing_subscriber::registry()
.with(console_layer)
.with(file_layer)
.with(telemetry_layer)
.init();
// 记录服务信息
log_service_info("mcp-proxy", "0.1.0")?;
tracing::info!("========================================");
tracing::info!("Starting MCP proxy service...");
tracing::info!("Running in proxy server mode");
tracing::info!("Version: {}", env!("CARGO_PKG_VERSION"));
tracing::info!("Configuration:");
tracing::info!(" - Listen port: {}", server_port);
tracing::info!(" - Log directory: {}", log_path);
tracing::info!(" - Log level: {}", &app_config.log.level);
tracing::info!(" - Log retention days: {}", retain_days);
tracing::info!("Environment overrides:");
if std::env::var("MCP_PROXY_PORT").is_ok() {
tracing::info!(" - MCP_PROXY_PORT override detected: {}", server_port);
}
if let Ok(log_dir) = std::env::var("MCP_PROXY_LOG_DIR") {
tracing::info!(" - MCP_PROXY_LOG_DIR override detected: {}", log_dir);
}
if let Ok(level) = std::env::var("MCP_PROXY_LOG_LEVEL") {
tracing::info!(" - MCP_PROXY_LOG_LEVEL override detected: {}", level);
}
tracing::info!("========================================");
// 监听地址
let addr = format!("0.0.0.0:{server_port}");
tracing::info!("Binding to {addr}...");
let listener = TcpListener::bind(&addr).await.map_err(|e| {
tracing::error!("Failed to bind to {addr}: {e}");
e
})?;
tracing::info!("Successfully bound to {addr}");
// 构建 axum 路由
tracing::info!("Initializing application state...");
let state = AppState::new(app_config.clone()).await;
tracing::info!("Application state initialized");
// 初始化 MCP 路由
tracing::info!("Initializing MCP router...");
let app = get_router(state.clone()).await?;
tracing::info!("MCP router initialized");
info!("MCP proxy started: {addr}");
info!("Health endpoint: http://{addr}/health");
info!("MCP list endpoint: http://{addr}/mcp/list");
// 启动定时任务定期检查MCP服务状态
tokio::spawn(start_schedule_task());
info!("Background schedule task started");
info!("Log rotation configured (keep last {retain_days} files)");
// 打印系统信息
tracing::info!("System info:");
tracing::info!(" - OS: {}", std::env::consts::OS);
tracing::info!(" - Arch: {}", std::env::consts::ARCH);
tracing::info!(
" - Working directory: {:?}",
std::env::current_dir().unwrap_or_default()
);
// 注册关闭处理函数,确保在程序退出前执行清理
tokio::spawn(async move {
// 确保在程序退出前执行清理
std::panic::set_hook(Box::new(move |panic_info| {
// 记录详细的 panic 信息
warn!("Panic hook triggered");
// 记录 panic 消息
if let Some(s) = panic_info.payload().downcast_ref::<String>() {
error!("Panic reason: {s}");
} else if let Some(s) = panic_info.payload().downcast_ref::<&str>() {
error!("Panic reason: {s}");
} else {
error!("Panic reason: <unknown>");
}
// 记录 panic 位置
if let Some(location) = panic_info.location() {
error!("Panic location: {}:{}", location.file(), location.line());
}
// 尝试获取堆栈跟踪
error!("Panic backtrace:");
let backtrace = Backtrace::new();
error!("{backtrace:?}");
}));
});
// 预热 uv/deno 环境依赖
tokio::spawn(async move {
info!("Warming up uv/deno runtime dependencies...");
match warm_up_all_envs(None, None, None, None).await {
Ok(_) => info!("Runtime dependency warm-up completed"),
Err(e) => error!("Runtime dependency warm-up failed: {e}"),
}
});
// 启动服务器,监听多种信号以实现优雅关闭
info!("Starting HTTP server...");
let server =
axum::serve(listener, app.into_make_service()).with_graceful_shutdown(shutdown_signal());
// 运行服务器
if let Err(e) = server.await {
error!("HTTP server exited with error: {e}");
}
// 服务器关闭后执行清理逻辑
warn!("Shutdown signal received, starting cleanup...");
// 清理所有SSE服务
match get_proxy_manager().cleanup_all_resources().await {
Ok(_) => info!("Resource cleanup completed"),
Err(e) => error!("Resource cleanup failed: {e}"),
}
// 等待一小段时间确保所有资源都被清理
tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
info!("Shutdown complete");
Ok(())
}
// 监听多种终止信号
async fn shutdown_signal() {
signal::ctrl_c()
.await
.expect("Failed to install Ctrl+C handler");
}
#[cfg(test)]
mod tests {
#[test]
fn test_crypto_provider_install() {
// 测试 CryptoProvider 可以正常安装(或已被安装)
// 注意CryptoProvider 全局只能安装一次,多次安装会返回错误
let _ = rustls::crypto::ring::default_provider().install_default();
// 无论首次安装还是已安装,只要能获取到默认 provider 即表示成功
let provider = rustls::crypto::CryptoProvider::get_default();
assert!(provider.is_some(), "CryptoProvider should be available");
}
#[test]
fn test_crypto_provider_get_default() {
// 首先确保 CryptoProvider 已安装(忽略已安装的错误)
let _ = rustls::crypto::ring::default_provider().install_default();
// 测试可以正常获取默认 CryptoProvider
let provider = rustls::crypto::CryptoProvider::get_default();
assert!(
provider.is_some(),
"CryptoProvider should be available after installation"
);
}
}

View File

@@ -0,0 +1,245 @@
use axum::{
Json,
response::{IntoResponse, Response},
};
use http::StatusCode;
use mcp_common::t;
use serde::{Deserialize, Serialize};
use thiserror::Error;
/// MCP 应用错误类型
///
/// 包含错误码和详细错误信息,便于程序化处理
#[derive(Error, Debug)]
pub enum AppError {
/// 服务未找到 (0001)
#[error("{0}")]
ServiceNotFound(String),
/// 服务在重启冷却期内 (0002)
#[error("{0}")]
ServiceRestartCooldown(String),
/// 服务正在启动中 (0003)
#[error("{0}")]
ServiceStartupInProgress(String),
/// 服务启动失败 (0004)
#[error("{0}")]
ServiceStartupFailed(String),
/// 后端连接错误 (0005)
#[error("{0}")]
BackendConnection(String),
/// 配置解析错误 (0006)
#[error("{0}")]
ConfigParse(String),
/// MCP 服务器错误 (0007)
#[error("{0}")]
McpServerError(String),
/// JSON 序列化错误 (0008)
#[error("{0}")]
SerdeJsonError(String),
/// IO 错误 (0009)
#[error("{0}")]
IoError(String),
/// 路由未找到 (0010)
#[error("{0}")]
RouteNotFound(String),
/// 无效的请求参数 (0011)
#[error("{0}")]
InvalidParameter(String),
}
impl AppError {
// ===========================================
// 工厂方法 - 创建带国际化消息的错误
// ===========================================
/// 创建服务未找到错误
pub fn service_not_found(service: impl Into<String>) -> Self {
Self::ServiceNotFound(
t!(
"errors.mcp_proxy.service_not_found",
service = service.into()
)
.to_string(),
)
}
/// 创建服务重启冷却期错误
pub fn service_restart_cooldown(service: impl Into<String>) -> Self {
Self::ServiceRestartCooldown(
t!(
"errors.mcp_proxy.service_restart_cooldown",
service = service.into()
)
.to_string(),
)
}
/// 创建服务启动中错误
pub fn service_startup_in_progress(service: impl Into<String>) -> Self {
Self::ServiceStartupInProgress(
t!(
"errors.mcp_proxy.service_startup_in_progress",
service = service.into()
)
.to_string(),
)
}
/// 创建服务启动失败错误
pub fn service_startup_failed(mcp_id: impl Into<String>, reason: impl Into<String>) -> Self {
Self::ServiceStartupFailed(
t!(
"errors.mcp_proxy.service_startup_failed",
mcp_id = mcp_id.into(),
reason = reason.into()
)
.to_string(),
)
}
/// 创建后端连接错误
pub fn backend_connection(detail: impl Into<String>) -> Self {
Self::BackendConnection(
t!(
"errors.mcp_proxy.backend_connection",
detail = detail.into()
)
.to_string(),
)
}
/// 创建配置解析错误
pub fn config_parse(detail: impl Into<String>) -> Self {
Self::ConfigParse(t!("errors.mcp_proxy.config_parse", detail = detail.into()).to_string())
}
/// 创建 MCP 服务器错误
pub fn mcp_server_error(detail: impl Into<String>) -> Self {
Self::McpServerError(
t!("errors.mcp_proxy.mcp_server_error", detail = detail.into()).to_string(),
)
}
/// 创建 JSON 序列化错误
pub fn json_serialization(detail: impl Into<String>) -> Self {
Self::SerdeJsonError(
t!(
"errors.mcp_proxy.json_serialization",
detail = detail.into()
)
.to_string(),
)
}
/// 创建 IO 错误
pub fn io_error(detail: impl Into<String>) -> Self {
Self::IoError(t!("errors.mcp_proxy.io_error", detail = detail.into()).to_string())
}
/// 创建路由未找到错误
pub fn route_not_found(path: impl Into<String>) -> Self {
Self::RouteNotFound(t!("errors.mcp_proxy.route_not_found", path = path.into()).to_string())
}
/// 创建无效参数错误
pub fn invalid_parameter(detail: impl Into<String>) -> Self {
Self::InvalidParameter(
t!("errors.mcp_proxy.invalid_parameter", detail = detail.into()).to_string(),
)
}
// ===========================================
// 错误码和状态码
// ===========================================
/// 获取错误码
pub fn error_code(&self) -> &'static str {
match self {
Self::ServiceNotFound(_) => "0001",
Self::ServiceRestartCooldown(_) => "0002",
Self::ServiceStartupInProgress(_) => "0003",
Self::ServiceStartupFailed { .. } => "0004",
Self::BackendConnection(_) => "0005",
Self::ConfigParse(_) => "0006",
Self::McpServerError(_) => "0007",
Self::SerdeJsonError(_) => "0008",
Self::IoError(_) => "0009",
Self::RouteNotFound(_) => "0010",
Self::InvalidParameter(_) => "0011",
}
}
/// 获取 HTTP 状态码
pub fn status_code(&self) -> StatusCode {
match self {
Self::ServiceNotFound(_) => StatusCode::NOT_FOUND,
Self::ServiceRestartCooldown(_) => StatusCode::TOO_MANY_REQUESTS,
Self::ServiceStartupInProgress(_) => StatusCode::SERVICE_UNAVAILABLE,
Self::ServiceStartupFailed { .. } => StatusCode::INTERNAL_SERVER_ERROR,
Self::BackendConnection(_) => StatusCode::BAD_GATEWAY,
Self::ConfigParse(_) => StatusCode::BAD_REQUEST,
Self::McpServerError(_) => StatusCode::INTERNAL_SERVER_ERROR,
Self::SerdeJsonError(_) => StatusCode::BAD_REQUEST,
Self::IoError(_) => StatusCode::INTERNAL_SERVER_ERROR,
Self::RouteNotFound(_) => StatusCode::NOT_FOUND,
Self::InvalidParameter(_) => StatusCode::BAD_REQUEST,
}
}
}
// ===========================================
// 从外部错误类型转换
// ===========================================
impl From<anyhow::Error> for AppError {
fn from(err: anyhow::Error) -> Self {
Self::mcp_server_error(err.to_string())
}
}
impl From<serde_json::Error> for AppError {
fn from(err: serde_json::Error) -> Self {
Self::json_serialization(err.to_string())
}
}
impl From<std::io::Error> for AppError {
fn from(err: std::io::Error) -> Self {
Self::io_error(err.to_string())
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct ErrorOutput {
pub code: String,
pub error: String,
}
impl ErrorOutput {
pub fn with_code(code: &'static str, error: impl Into<String>) -> Self {
Self {
code: code.to_string(),
error: error.into(),
}
}
}
impl IntoResponse for AppError {
fn into_response(self) -> Response<axum::body::Body> {
(
self.status_code(),
Json(ErrorOutput::with_code(self.error_code(), self.to_string())),
)
.into_response()
}
}

View File

@@ -0,0 +1,33 @@
use std::{ops::Deref, sync::Arc};
use crate::AppConfig;
#[derive(Debug, Clone)]
pub struct AppState {
inner: Arc<AppStateInner>,
}
#[allow(unused)]
#[derive(Debug)]
pub struct AppStateInner {
pub addr: String,
pub config: AppConfig,
}
impl Deref for AppState {
type Target = AppStateInner;
fn deref(&self) -> &Self::Target {
&self.inner
}
}
impl AppState {
pub async fn new(config: AppConfig) -> Self {
Self {
inner: Arc::new(AppStateInner {
addr: format!("0.0.0.0:{}", config.server.port),
config,
}),
}
}
}

View File

@@ -0,0 +1,978 @@
use axum::Router;
use dashmap::DashMap;
use log::{debug, error, info};
use moka::future::Cache;
use once_cell::sync::Lazy;
use std::sync::Arc;
use tokio::sync::{Mutex, OwnedMutexGuard};
use tokio::time::{Duration, Instant};
use tokio_util::sync::CancellationToken;
use tracing::warn;
use anyhow::Result;
use crate::proxy::McpHandler;
use super::{CheckMcpStatusResponseStatus, McpConfig, McpProtocol, McpRouterPath, McpType};
// 全局单例路由表
pub static GLOBAL_ROUTES: Lazy<Arc<DashMap<String, Router>>> =
Lazy::new(|| Arc::new(DashMap::new()));
// 全局单例 ProxyHandlerManager
pub static GLOBAL_PROXY_MANAGER: Lazy<ProxyHandlerManager> =
Lazy::new(ProxyHandlerManager::default);
/// 动态路由服务
#[derive(Clone)]
pub struct DynamicRouterService(pub McpProtocol);
impl DynamicRouterService {
// 注册动态 handler
pub fn register_route(path: &str, handler: Router) {
debug!("=== Register Route ===");
debug!("Registration path: {}", path);
GLOBAL_ROUTES.insert(path.to_string(), handler);
debug!("=== Route registration completed ===");
}
// 删除动态 handler
pub fn delete_route(path: &str) {
debug!("=== Delete route ===");
debug!("Delete path: {}", path);
GLOBAL_ROUTES.remove(path);
debug!("=== Route deletion completed ===");
}
// 获取动态 handler
pub fn get_route(path: &str) -> Option<Router> {
let result = GLOBAL_ROUTES.get(path).map(|entry| entry.value().clone());
if result.is_some() {
debug!("get_route('{}') = Some(Router)", path);
} else {
debug!("get_route('{}') = None", path);
}
result
}
// 获取所有已注册的路由debug用
pub fn get_all_routes() -> Vec<String> {
GLOBAL_ROUTES
.iter()
.map(|entry| entry.key().clone())
.collect()
}
}
impl std::fmt::Debug for DynamicRouterService {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let routes = GLOBAL_ROUTES
.iter()
.map(|entry| entry.key().clone())
.collect::<Vec<_>>();
write!(f, "DynamicRouterService {{ routes: {routes:?} }}")
}
}
// =============================================================================
// RAII 进程管理设计
// =============================================================================
//
// 设计目标:当 mcp_id 从 map 中移除时,自动释放对应的 MCP 进程资源
//
// 核心结构:
// - McpProcessGuard: 进程生命周期守护器,实现 Drop trait 自动取消 CancellationToken
// - McpService: 封装 McpHandler + McpProcessGuard + 服务状态,作为 map 的 value
// - ProxyHandlerManager: 使用单一 DashMap<String, McpService> 管理所有服务
//
// 资源释放流程:
// 1. 从 map 中 remove mcp_id
// 2. McpService 被 drop
// 3. McpProcessGuard::drop() 被调用
// 4. CancellationToken 被 cancel
// 5. 监听该 token 的 SseServer/子进程收到信号,自动退出
// =============================================================================
/// MCP 进程生命周期守护器
///
/// 实现 RAII 模式:当此结构体被 drop 时,自动取消 CancellationToken
/// 触发关联的 SseServer 和子进程退出。
///
/// # 使用场景
///
/// 1. 从 `ProxyHandlerManager` 移除 mcp_id 时,自动清理进程
/// 2. 服务重启时,旧服务自动被清理
/// 3. 系统关闭时,所有服务自动清理
pub struct McpProcessGuard {
mcp_id: String,
cancellation_token: CancellationToken,
}
impl McpProcessGuard {
pub fn new(mcp_id: String, cancellation_token: CancellationToken) -> Self {
debug!("[RAII] Create process daemon: mcp_id={}", mcp_id);
Self {
mcp_id,
cancellation_token,
}
}
/// 克隆 CancellationToken用于传递给异步任务
pub fn clone_token(&self) -> CancellationToken {
self.cancellation_token.clone()
}
}
impl Drop for McpProcessGuard {
fn drop(&mut self) {
info!(
"[RAII] The process daemon was dropped and canceled CancellationToken: mcp_id={}",
self.mcp_id
);
self.cancellation_token.cancel();
}
}
// McpProcessGuard 不实现 Clone确保唯一所有权
impl std::fmt::Debug for McpProcessGuard {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("McpProcessGuard")
.field("mcp_id", &self.mcp_id)
.field("is_cancelled", &self.cancellation_token.is_cancelled())
.finish()
}
}
/// MCP 服务封装
///
/// 将 McpHandler、McpProcessGuard 和服务状态封装在一起,
/// 作为 `ProxyHandlerManager` 中 DashMap 的 value。
///
/// # RAII 保证
///
/// 当 McpService 被 drop 时:
/// 1. McpProcessGuard 被 drop触发 CancellationToken 取消
/// 2. 关联的子进程收到信号,自动退出
pub struct McpService {
/// 进程守护器RAII 核心)
process_guard: McpProcessGuard,
/// MCP 透明代理处理器(可选,启动中时为 None
handler: Option<McpHandler>,
/// 服务状态信息
status: McpServiceStatusInfo,
}
/// MCP 服务状态信息(不包含 CancellationToken由 McpProcessGuard 管理)
#[derive(Debug, Clone)]
pub struct McpServiceStatusInfo {
pub mcp_id: String,
pub mcp_type: McpType,
pub mcp_router_path: McpRouterPath,
pub check_mcp_status_response_status: CheckMcpStatusResponseStatus,
pub last_accessed: Instant,
pub mcp_config: Option<McpConfig>,
/// 连续健康检查失败次数(用于容错机制)
pub consecutive_probe_failures: u32,
}
impl McpServiceStatusInfo {
pub fn new(
mcp_id: String,
mcp_type: McpType,
mcp_router_path: McpRouterPath,
check_mcp_status_response_status: CheckMcpStatusResponseStatus,
) -> Self {
Self {
mcp_id,
mcp_type,
mcp_router_path,
check_mcp_status_response_status,
last_accessed: Instant::now(),
mcp_config: None,
consecutive_probe_failures: 0,
}
}
pub fn update_last_accessed(&mut self) {
self.last_accessed = Instant::now();
}
/// 重置健康检查失败计数
pub fn reset_probe_failures(&mut self) {
self.consecutive_probe_failures = 0;
}
/// 增加健康检查失败计数,返回增加后的值
pub fn increment_probe_failures(&mut self) -> u32 {
self.consecutive_probe_failures += 1;
self.consecutive_probe_failures
}
}
impl McpService {
/// 创建新的 MCP 服务
///
/// # 参数
/// - `mcp_id`: 服务唯一标识
/// - `mcp_type`: 服务类型
/// - `mcp_router_path`: 路由路径
/// - `cancellation_token`: 用于控制进程生命周期的取消令牌
pub fn new(
mcp_id: String,
mcp_type: McpType,
mcp_router_path: McpRouterPath,
cancellation_token: CancellationToken,
) -> Self {
let process_guard = McpProcessGuard::new(mcp_id.clone(), cancellation_token);
let status = McpServiceStatusInfo::new(
mcp_id,
mcp_type,
mcp_router_path,
CheckMcpStatusResponseStatus::Pending,
);
Self {
process_guard,
handler: None,
status,
}
}
/// 设置 MCP Handler
pub fn set_handler(&mut self, handler: McpHandler) {
self.handler = Some(handler);
}
/// 获取 MCP Handler
pub fn handler(&self) -> Option<&McpHandler> {
self.handler.as_ref()
}
/// 获取服务状态
pub fn status(&self) -> &McpServiceStatusInfo {
&self.status
}
/// 获取可变服务状态
pub fn status_mut(&mut self) -> &mut McpServiceStatusInfo {
&mut self.status
}
/// 克隆 CancellationToken
pub fn clone_token(&self) -> CancellationToken {
self.process_guard.clone_token()
}
/// 更新服务状态
pub fn update_status(&mut self, status: CheckMcpStatusResponseStatus) {
self.status.check_mcp_status_response_status = status;
}
/// 更新最后访问时间
pub fn update_last_accessed(&mut self) {
self.status.update_last_accessed();
}
/// 设置 MCP 配置
pub fn set_mcp_config(&mut self, config: McpConfig) {
self.status.mcp_config = Some(config);
}
}
impl std::fmt::Debug for McpService {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("McpService")
.field("process_guard", &self.process_guard)
.field("handler", &self.handler.is_some())
.field("status", &self.status)
.finish()
}
}
// =============================================================================
// 兼容层:保留 McpServiceStatus 以兼容现有代码
// =============================================================================
/// MCP 服务状态(兼容层)
///
/// 保留此结构体以兼容现有代码,内部委托给 McpServiceStatusInfo
#[derive(Debug, Clone)]
pub struct McpServiceStatus {
pub mcp_id: String,
pub mcp_type: McpType,
pub mcp_router_path: McpRouterPath,
pub cancellation_token: CancellationToken,
pub check_mcp_status_response_status: CheckMcpStatusResponseStatus,
pub last_accessed: Instant,
pub mcp_config: Option<McpConfig>,
/// 连续健康检查失败次数
pub consecutive_probe_failures: u32,
}
impl McpServiceStatus {
pub fn new(
mcp_id: String,
mcp_type: McpType,
mcp_router_path: McpRouterPath,
cancellation_token: CancellationToken,
check_mcp_status_response_status: CheckMcpStatusResponseStatus,
) -> Self {
Self {
mcp_id,
mcp_type,
mcp_router_path,
cancellation_token,
check_mcp_status_response_status,
last_accessed: Instant::now(),
mcp_config: None,
consecutive_probe_failures: 0,
}
}
pub fn with_mcp_config(mut self, mcp_config: McpConfig) -> Self {
self.mcp_config = Some(mcp_config);
self
}
pub fn update_last_accessed(&mut self) {
self.last_accessed = Instant::now();
}
}
// =============================================================================
// ProxyHandlerManager使用 RAII 模式管理 MCP 服务
// =============================================================================
/// MCP 代理管理器
///
/// 使用 RAII 模式管理 MCP 服务:
/// - 从 map 中移除 mcp_id 时,自动释放对应的进程资源
/// - 不需要显式调用 cleanup 方法(但仍提供显式清理接口)
#[derive(Debug)]
pub struct ProxyHandlerManager {
/// 使用单一 DashMap 管理所有 MCP 服务RAII 核心)
services: DashMap<String, McpService>,
}
impl Default for ProxyHandlerManager {
fn default() -> Self {
ProxyHandlerManager {
services: DashMap::new(),
}
}
}
impl ProxyHandlerManager {
/// 添加 MCP 服务RAII 模式)
///
/// 使用新的 RAII 结构创建服务,当服务被移除时会自动清理资源
pub fn add_mcp_service(
&self,
mcp_id: String,
mcp_type: McpType,
mcp_router_path: McpRouterPath,
cancellation_token: CancellationToken,
) {
let service = McpService::new(
mcp_id.clone(),
mcp_type,
mcp_router_path,
cancellation_token,
);
// RAII: 如果已存在同名服务insert 会返回旧服务,旧服务被 drop 时自动清理
if let Some(old_service) = self.services.insert(mcp_id.clone(), service) {
info!(
"[RAII] Overwrite existing services, old services will be automatically cleaned up: mcp_id={}",
mcp_id
);
drop(old_service);
}
}
/// 添加 MCP 服务状态(兼容旧 API
///
/// 保持与现有代码的兼容性,内部转换为新的 RAII 结构
///
/// 注意:`last_accessed` 会被重置为当前时间(插入视为新访问)
pub fn add_mcp_service_status_and_proxy(
&self,
mcp_service_status: McpServiceStatus,
proxy_handler: Option<McpHandler>,
) {
let mcp_id = mcp_service_status.mcp_id.clone();
// 创建 McpService 使用 RAII 模式
// 注意last_accessed 会在 McpServiceStatusInfo::new() 中重置为 Instant::now()
let mut service = McpService::new(
mcp_id.clone(),
mcp_service_status.mcp_type,
mcp_service_status.mcp_router_path,
mcp_service_status.cancellation_token,
);
// 设置初始状态
service.status_mut().check_mcp_status_response_status =
mcp_service_status.check_mcp_status_response_status;
// 设置配置(如果有)
if let Some(config) = mcp_service_status.mcp_config {
service.set_mcp_config(config);
}
// 设置 handler如果有
if let Some(handler) = proxy_handler {
service.set_handler(handler);
}
// RAII: 如果已存在同名服务insert 会返回旧服务,旧服务被 drop 时自动清理
if let Some(old_service) = self.services.insert(mcp_id.clone(), service) {
info!(
"[RAII] Overwrite existing services, old services will be automatically cleaned up: mcp_id={}",
mcp_id
);
// old_service 在此作用域结束时 drop触发 McpProcessGuard::drop()
drop(old_service);
}
}
/// 获取所有的 MCP 服务状态(兼容旧 API
///
/// 优化:先快速收集所有 keys然后逐个获取详细信息
/// 避免 iter() 长时间锁住多个分片,让其他写操作有机会执行
pub fn get_all_mcp_service_status(&self) -> Vec<McpServiceStatus> {
// 第一步:快速收集所有 keys只 clone String锁持有时间短
let keys: Vec<String> = self
.services
.iter()
.map(|entry| entry.key().clone())
.collect();
// 第二步:逐个获取详细信息(每次只锁一个分片)
keys.into_iter()
.filter_map(|mcp_id| self.get_mcp_service_status(&mcp_id))
.collect()
}
/// 获取 MCP 服务状态(兼容旧 API
pub fn get_mcp_service_status(&self, mcp_id: &str) -> Option<McpServiceStatus> {
self.services.get(mcp_id).map(|entry| {
let service = entry.value();
let status = service.status();
McpServiceStatus {
mcp_id: status.mcp_id.clone(),
mcp_type: status.mcp_type.clone(),
mcp_router_path: status.mcp_router_path.clone(),
cancellation_token: service.clone_token(),
check_mcp_status_response_status: status.check_mcp_status_response_status.clone(),
last_accessed: status.last_accessed,
mcp_config: status.mcp_config.clone(),
consecutive_probe_failures: status.consecutive_probe_failures,
}
})
}
/// 更新最后访问时间
///
/// 使用 entry API 确保原子性操作
pub fn update_last_accessed(&self, mcp_id: &str) {
self.services
.entry(mcp_id.to_string())
.and_modify(|service| service.update_last_accessed());
}
/// 修改 MCP 服务状态 (Ready/Pending/Error)
///
/// 使用 entry API 确保原子性操作
pub fn update_mcp_service_status(&self, mcp_id: &str, status: CheckMcpStatusResponseStatus) {
self.services
.entry(mcp_id.to_string())
.and_modify(|service| service.update_status(status));
}
/// 获取 MCP Handler
pub fn get_proxy_handler(&self, mcp_id: &str) -> Option<McpHandler> {
self.services
.get(mcp_id)
.and_then(|entry| entry.value().handler().cloned())
}
/// 获取服务的 MCP 配置(用于自动重启)
pub fn get_mcp_config(&self, mcp_id: &str) -> Option<McpConfig> {
self.services
.get(mcp_id)
.and_then(|entry| entry.value().status().mcp_config.clone())
}
/// 添加 MCP Handler 到已存在的服务
///
/// 使用 entry API 确保原子性操作
pub fn add_proxy_handler(&self, mcp_id: &str, proxy_handler: McpHandler) {
match self.services.entry(mcp_id.to_string()) {
dashmap::mapref::entry::Entry::Occupied(mut entry) => {
entry.get_mut().set_handler(proxy_handler);
}
dashmap::mapref::entry::Entry::Vacant(_) => {
warn!(
"[RAII] Trying to add handler to non-existent service: mcp_id={}",
mcp_id
);
}
}
}
/// 检查服务是否存在
pub fn contains_service(&self, mcp_id: &str) -> bool {
self.services.contains_key(mcp_id)
}
/// 获取服务数量
pub fn service_count(&self) -> usize {
self.services.len()
}
/// 注册 MCP 配置到缓存
pub async fn register_mcp_config(&self, mcp_id: &str, config: McpConfig) {
GLOBAL_MCP_CONFIG_CACHE
.insert(mcp_id.to_string(), config)
.await;
info!("MCP configuration registered in cache: {}", mcp_id);
}
/// 从缓存获取 MCP 配置
pub async fn get_mcp_config_from_cache(&self, mcp_id: &str) -> Option<McpConfig> {
if let Some(config) = GLOBAL_MCP_CONFIG_CACHE.get(mcp_id).await {
debug!("Get MCP configuration from cache: {}", mcp_id);
Some(config)
} else {
debug!("MCP configuration not found in cache: {}", mcp_id);
None
}
}
/// 从缓存删除 MCP 配置
pub async fn unregister_mcp_config(&self, mcp_id: &str) {
GLOBAL_MCP_CONFIG_CACHE.invalidate(mcp_id).await;
info!("MCP configuration removed from cache: {}", mcp_id);
}
/// 清理资源 (RAII 模式简化版)
///
/// 通过 RAII 模式,从 DashMap 中移除服务会自动:
/// 1. 触发 McpProcessGuard::drop()
/// 2. 取消 CancellationToken
/// 3. 关联的子进程收到信号退出
///
/// 此方法额外清理路由和缓存
pub async fn cleanup_resources(&self, mcp_id: &str) -> Result<()> {
info!("[RAII] Start cleaning up resources: mcp_id={}", mcp_id);
// 创建路径以构建要删除的路由路径
let mcp_sse_router_path = McpRouterPath::new(mcp_id.to_string(), McpProtocol::Sse)
.map_err(|e| {
anyhow::anyhow!("Failed to create SSE router path for {}: {}", mcp_id, e)
})?;
let base_sse_path = mcp_sse_router_path.base_path;
let mcp_stream_router_path = McpRouterPath::new(mcp_id.to_string(), McpProtocol::Stream)
.map_err(|e| {
anyhow::anyhow!("Failed to create Stream router path for {}: {}", mcp_id, e)
})?;
let base_stream_path = mcp_stream_router_path.base_path;
// 移除相关路由
DynamicRouterService::delete_route(&base_sse_path);
DynamicRouterService::delete_route(&base_stream_path);
// RAII 核心:从 DashMap 移除会触发 McpProcessGuard::drop()
// 这会自动取消 CancellationToken进而触发子进程退出
if self.services.remove(mcp_id).is_some() {
info!(
"[RAII] The service has been removed from the map, McpProcessGuard will automatically cancel the token: mcp_id={}",
mcp_id
);
} else {
debug!(
"[RAII] Service does not exist, skip removal: mcp_id={}",
mcp_id
);
}
// 清理配置缓存
self.unregister_mcp_config(mcp_id).await;
// 清理健康状态缓存
GLOBAL_RESTART_TRACKER.clear_health_status(mcp_id);
info!(
"[RAII] MCP service resource cleanup completed: mcp_id={}",
mcp_id
);
Ok(())
}
/// 系统关闭,清理所有资源
///
/// RAII 模式下,清除 DashMap 会自动释放所有资源
pub async fn cleanup_all_resources(&self) -> Result<()> {
info!("[RAII] Start cleaning up all MCP service resources");
// 收集所有 mcp_id
let mcp_ids: Vec<String> = self
.services
.iter()
.map(|entry| entry.key().clone())
.collect();
let count = mcp_ids.len();
// 逐个清理(包括路由和缓存)
for mcp_id in mcp_ids {
if let Err(e) = self.cleanup_resources(&mcp_id).await {
error!(
"[RAII] Failed to clean up resources: mcp_id={}, error={}",
mcp_id, e
);
// 继续清理其他资源
}
}
info!(
"[RAII] All MCP service resources have been cleaned up, and a total of {} services have been cleaned up.",
count
);
Ok(())
}
/// 仅移除服务(依赖 RAII 自动清理进程)
///
/// 从 DashMap 中移除服务,触发 RAII 自动清理。
/// 不会清理路由和缓存,适用于需要快速移除服务的场景。
pub fn remove_service(&self, mcp_id: &str) -> bool {
if self.services.remove(mcp_id).is_some() {
info!(
"[RAII] The service has been removed and the process will be automatically cleaned up: mcp_id={}",
mcp_id
);
true
} else {
debug!("[RAII] Service does not exist: mcp_id={}", mcp_id);
false
}
}
/// 重置健康检查失败计数
///
/// 使用 get_mut 避免为不存在的服务创建空 entry
pub fn reset_probe_failures(&self, mcp_id: &str) {
if let Some(mut entry) = self.services.get_mut(mcp_id) {
entry.value_mut().status_mut().reset_probe_failures();
}
}
/// 增加健康检查失败计数,返回增加后的值
///
/// 使用 entry API 确保原子性操作
pub fn increment_probe_failures(&self, mcp_id: &str) -> u32 {
self.services
.get_mut(mcp_id)
.map(|mut entry| entry.value_mut().status_mut().increment_probe_failures())
.unwrap_or(0)
}
/// 清理资源用于重启(保留配置缓存)
///
/// 与 cleanup_resources 不同,此方法不会清理配置缓存,
/// 允许后续使用缓存的配置重新启动服务。
pub async fn cleanup_resources_for_restart(&self, mcp_id: &str) -> Result<()> {
info!(
"[RAII] Start cleaning up resources for restart: mcp_id={}",
mcp_id
);
// 创建路径以构建要删除的路由路径
let mcp_sse_router_path = McpRouterPath::new(mcp_id.to_string(), McpProtocol::Sse)
.map_err(|e| {
anyhow::anyhow!("Failed to create SSE router path for {}: {}", mcp_id, e)
})?;
let base_sse_path = mcp_sse_router_path.base_path;
let mcp_stream_router_path = McpRouterPath::new(mcp_id.to_string(), McpProtocol::Stream)
.map_err(|e| {
anyhow::anyhow!("Failed to create Stream router path for {}: {}", mcp_id, e)
})?;
let base_stream_path = mcp_stream_router_path.base_path;
// 移除相关路由
DynamicRouterService::delete_route(&base_sse_path);
DynamicRouterService::delete_route(&base_stream_path);
// RAII 核心:从 DashMap 移除会触发 McpProcessGuard::drop()
if self.services.remove(mcp_id).is_some() {
info!(
"[RAII] The service has been removed from the map (for restart), McpProcessGuard will automatically cancel the token: mcp_id={}",
mcp_id
);
} else {
debug!(
"[RAII] Service does not exist, skip removal: mcp_id={}",
mcp_id
);
}
// 注意:不清理配置缓存,保留用于重启
// self.unregister_mcp_config(mcp_id).await; // 不调用
// 清理健康状态缓存
GLOBAL_RESTART_TRACKER.clear_health_status(mcp_id);
info!(
"[RAII] MCP service resource cleanup completed (for restart): mcp_id={}",
mcp_id
);
Ok(())
}
}
/// MCP 配置缓存(使用 moka 实现 TTL
///
/// ## 存储架构说明
///
/// MCP 配置存储在两个位置:
///
/// 1. **McpServiceStatus.mcp_config**(服务状态中)
/// - 存储当前运行服务的配置
/// - 随服务清理而被删除
/// - 用于快速访问当前服务的配置
///
/// 2. **GLOBAL_MCP_CONFIG_CACHE**(全局缓存)
/// - 独立于服务状态存储
/// - 有 TTL24 小时)
/// - 用于服务重启时恢复配置
///
/// ## 为什么需要两处存储?
///
/// - 服务清理后McpServiceStatus 被删除,但配置仍在缓存中
/// - 下次请求到来时,可以从缓存恢复配置并重启服务
/// - 实现了服务的自动重启能力
///
/// ## 优先级
///
/// 1. 请求 header 中的配置(最新)
/// 2. 缓存中的配置(兜底)
///
/// ## TTL
///
/// - 24 小时(可配置)
/// - max_capacity: 1000防止内存溢出
pub struct McpConfigCache {
cache: Cache<String, McpConfig>,
}
impl McpConfigCache {
pub fn new() -> Self {
Self {
cache: Cache::builder()
.time_to_live(Duration::from_secs(24 * 60 * 60)) // 24 小时 TTL
.max_capacity(1000) // 最多缓存 1000 个配置,防止内存溢出
.build(),
}
}
pub async fn insert(&self, mcp_id: String, config: McpConfig) {
self.cache.insert(mcp_id.clone(), config).await;
info!("MCP configuration cached: {} (TTL: 24h)", mcp_id);
}
pub async fn get(&self, mcp_id: &str) -> Option<McpConfig> {
self.cache.get(mcp_id).await
}
pub async fn invalidate(&self, mcp_id: &str) {
self.cache.invalidate(mcp_id).await;
}
#[allow(dead_code)]
pub fn invalidate_all(&self) {
self.cache.invalidate_all();
}
}
impl Default for McpConfigCache {
fn default() -> Self {
Self::new()
}
}
// 全局配置缓存单例
pub static GLOBAL_MCP_CONFIG_CACHE: Lazy<McpConfigCache> = Lazy::new(McpConfigCache::default);
/// MCP 服务重启追踪器
///
/// 用于防止服务频繁重启导致的无限循环
///
/// ## 重启限制
///
/// - 最小重启间隔30 秒
/// - 如果服务在 30 秒内被标记为需要重启,将跳过重启
/// - 这防止了服务启动失败时的无限重启循环
///
/// ## 健康状态缓存
///
/// - 缓存后端健康状态,避免频繁检查
/// - 缓存时间5 秒(可配置)
/// - 用于减少 `is_mcp_server_ready()` 调用频率
pub struct RestartTracker {
// mcp_id -> 最后重启时间
last_restart: DashMap<String, Instant>,
// mcp_id -> (健康状态, 检查时间)
health_status: DashMap<String, (bool, Instant)>,
// mcp_id -> 启动锁,防止并发启动同一服务
startup_locks: DashMap<String, Arc<Mutex<()>>>,
}
impl RestartTracker {
pub fn new() -> Self {
Self {
last_restart: DashMap::new(),
health_status: DashMap::new(),
startup_locks: DashMap::new(),
}
}
/// 获取缓存的健康状态
///
/// 如果缓存未过期5秒内返回缓存值
/// 否则返回 None表示需要重新检查
pub fn get_cached_health_status(&self, mcp_id: &str) -> Option<bool> {
let cache_duration = Duration::from_secs(5); // 5 秒缓存
let now = Instant::now();
self.health_status.get(mcp_id).and_then(|entry| {
let (is_healthy, check_time) = *entry.value();
if now.duration_since(check_time) < cache_duration {
Some(is_healthy)
} else {
None
}
})
}
/// 更新健康状态缓存
pub fn update_health_status(&self, mcp_id: &str, is_healthy: bool) {
self.health_status
.insert(mcp_id.to_string(), (is_healthy, Instant::now()));
}
/// 清除健康状态缓存
pub fn clear_health_status(&self, mcp_id: &str) {
self.health_status.remove(mcp_id);
}
/// 检查是否可以重启服务
///
/// 返回 true 表示可以重启false 表示在冷却期内
///
/// 注意:此方法仅检查是否可以重启,不会自动插入时间戳。
/// 时间戳只在服务成功启动后通过 `record_restart()` 方法记录。
pub fn can_restart(&self, mcp_id: &str) -> bool {
let now = Instant::now();
let min_restart_interval = Duration::from_secs(30); // 30 秒最小重启间隔
// 只检查,不自动插入时间戳
if let Some(last_restart) = self.last_restart.get(mcp_id) {
let elapsed = now.duration_since(*last_restart);
if elapsed < min_restart_interval {
warn!(
"Service {} is in the cooldown period and is only {} seconds since its last restart. Restart is skipped.",
mcp_id,
elapsed.as_secs()
);
return false;
}
}
// 不在冷却期内,但不自动更新时间戳
true
}
/// 记录服务成功重启
///
/// 此方法应在服务成功启动后调用,用于记录重启时间戳。
/// 配合 `can_restart()` 使用,避免在服务启动失败时插入时间戳。
pub fn record_restart(&self, mcp_id: &str) {
self.last_restart.insert(mcp_id.to_string(), Instant::now());
info!(
"The service started successfully and the restart time was recorded: {}",
mcp_id
);
}
/// 清除重启时间戳
///
/// 当服务启动失败时,可选择调用此方法清除时间戳,
/// 允许立即重试而不必等待冷却期。
#[allow(dead_code)]
pub fn clear_restart(&self, mcp_id: &str) {
self.last_restart.remove(mcp_id);
info!("Restart timestamp cleared for service {}", mcp_id);
}
/// 尝试获取服务启动锁
///
/// 返回 Some(OwnedMutexGuard) 表示获取成功,可以继续启动服务
/// 返回 None 表示服务正在启动中,应该跳过本次启动
///
/// # 使用方式
///
/// ```ignore
/// if let Some(_guard) = GLOBAL_RESTART_TRACKER.try_acquire_startup_lock(&mcp_id) {
/// // 获取到锁,可以启动服务
/// let result = start_service().await;
/// // _guard 在作用域结束时自动释放
/// } else {
/// // 未获取到锁,服务正在启动中
/// return Ok(Response::503);
/// }
/// ```
pub fn try_acquire_startup_lock(&self, mcp_id: &str) -> Option<OwnedMutexGuard<()>> {
// 使用 entry API 确保原子性,避免竞态条件
let lock = self
.startup_locks
.entry(mcp_id.to_string())
.or_insert_with(|| Arc::new(Mutex::new(())))
.clone();
// 尝试获取 owned 锁,锁会一直保持到返回的 guard 被 drop
match lock.try_lock_owned() {
Ok(guard) => Some(guard),
Err(_) => {
// 锁被占用,服务正在启动中
debug!("Service {} is starting, skip this startup", mcp_id);
None
}
}
}
/// 清理服务启动锁
///
/// 当服务启动完成或失败后,应该清理启动锁以允许后续重试
/// 注意:正常情况下锁会随 MutexGuard 自动释放,此方法用于异常清理
#[allow(dead_code)]
pub fn cleanup_startup_lock(&self, mcp_id: &str) {
self.startup_locks.remove(mcp_id);
debug!("Cleaned startup lock for service {}", mcp_id);
}
}
impl Default for RestartTracker {
fn default() -> Self {
Self::new()
}
}
// 全局重启追踪器单例
pub static GLOBAL_RESTART_TRACKER: Lazy<RestartTracker> = Lazy::new(RestartTracker::default);
// 提供一个便捷的函数来获取全局 ProxyHandlerManager
pub fn get_proxy_manager() -> &'static ProxyHandlerManager {
&GLOBAL_PROXY_MANAGER
}

View File

@@ -0,0 +1,71 @@
use axum::http::StatusCode;
use axum::response::{IntoResponse, Response};
use serde::{Deserialize, Serialize, Serializer, ser::SerializeStruct};
#[allow(dead_code)]
#[derive(Debug, Deserialize)]
pub struct HttpResult<T> {
pub code: String,
pub message: String,
pub data: Option<T>,
pub tid: Option<String>,
#[serde(skip)]
pub success: bool,
}
impl<T> HttpResult<T> {
pub fn success(data: T, tid: Option<String>) -> Self {
HttpResult {
code: "0000".to_string(),
message: "成功".to_string(),
data: Some(data),
tid,
success: true,
}
}
pub fn error(code: &str, message: &str, tid: Option<String>) -> Self {
HttpResult {
code: code.to_string(),
message: message.to_string(),
data: None,
tid,
success: false,
}
}
}
impl<T: Serialize> Serialize for HttpResult<T> {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let mut state = serializer.serialize_struct("HttpResult", 5)?;
state.serialize_field("code", &self.code)?;
state.serialize_field("message", &self.message)?;
state.serialize_field("data", &self.data)?;
state.serialize_field("tid", &self.tid)?;
let is_success = self.code == "0000";
state.serialize_field("success", &is_success)?;
state.end()
}
}
impl<T: Serialize> IntoResponse for HttpResult<T> {
fn into_response(self) -> Response {
match serde_json::to_string(&self) {
Ok(body) => (
StatusCode::OK,
[(axum::http::header::CONTENT_TYPE, "application/json")],
body,
)
.into_response(),
Err(_) => (
StatusCode::INTERNAL_SERVER_ERROR,
[(axum::http::header::CONTENT_TYPE, "text/plain")],
"Internal Server Error",
)
.into_response(),
}
}
}

View File

@@ -0,0 +1,104 @@
use axum::response::{IntoResponse, Response};
use http::StatusCode;
use serde::{Deserialize, Serialize};
use super::{McpProtocol, McpType};
//check mcp服务状态的请求参数
#[derive(Deserialize, Debug, Clone)]
pub struct CheckMcpStatusRequestParams {
//mcp的id,必须有
#[serde(rename = "mcpId")]
pub mcp_id: String,
//mcp的json配置,必须有
#[serde(rename = "mcpJsonConfig")]
pub mcp_json_config: String,
//mcp类型,必须有,默认:OneShot
#[serde(rename = "mcpType", default = "default_mcp_type")]
pub mcp_type: McpType,
//后端MCP服务的协议类型可选用于指定连接到后端服务时使用的协议
//如果不指定,则使用客户端协议(由路由路径决定)
#[serde(rename = "backendProtocol")]
#[allow(dead_code)] // 为未来的功能预留
pub backend_protocol: Option<McpProtocol>,
}
//默认的mcp类型
fn default_mcp_type() -> McpType {
McpType::OneShot
}
//check mcp服务状态的响应参数
#[derive(Deserialize, Debug, Serialize)]
pub struct CheckMcpStatusResponseParams {
//是否就绪, READY 状态,表示 true
pub ready: bool,
//状态
pub status: McpStatusResponseEnum,
//消息
pub message: Option<String>,
}
impl CheckMcpStatusResponseParams {
pub fn new(ready: bool, status: CheckMcpStatusResponseStatus, message: Option<String>) -> Self {
//检查是否error,是的话,取error枚举里面的错误,放在 message里
let mut message = message;
if let CheckMcpStatusResponseStatus::Error(err) = status.clone() {
message = Some(err.to_string());
}
let status = McpStatusResponseEnum::from(status);
Self {
ready,
status,
message,
}
}
}
//check mcp服务状态的响应 status 枚举: READY,PENDING,ERROR
#[derive(Deserialize, Debug, Serialize, Clone)]
pub enum McpStatusResponseEnum {
//就绪
Ready,
//处理中
Pending,
//错误
Error,
}
//check mcp服务状态的响应 status 枚举: READY,PENDING,ERROR
#[derive(Deserialize, Debug, Serialize, Clone, Default)]
pub enum CheckMcpStatusResponseStatus {
//就绪
Ready,
//处理中
#[default]
Pending,
//错误
Error(String),
}
impl From<CheckMcpStatusResponseStatus> for McpStatusResponseEnum {
fn from(value: CheckMcpStatusResponseStatus) -> Self {
match value {
CheckMcpStatusResponseStatus::Ready => Self::Ready,
CheckMcpStatusResponseStatus::Pending => Self::Pending,
CheckMcpStatusResponseStatus::Error(_) => Self::Error,
}
}
}
impl IntoResponse for CheckMcpStatusResponseParams {
fn into_response(self) -> Response {
if let Ok(body) = serde_json::to_string(&self) {
(StatusCode::OK, body).into_response()
} else {
(
StatusCode::INTERNAL_SERVER_ERROR,
"serde_json::to_string error".to_string(),
)
.into_response()
}
}
}

View File

@@ -0,0 +1,98 @@
use std::str::FromStr;
use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
use super::{McpProtocol, mcp_router_model::McpServerConfig};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct McpConfig {
//mcp_id
#[serde(rename = "mcpId")]
pub mcp_id: String,
//mcp_json_config,可能没有
#[serde(rename = "mcpJsonConfig")]
pub mcp_json_config: Option<String>,
//mcp类型默认为持续运行
#[serde(default = "default_mcp_type", rename = "mcpType")]
pub mcp_type: McpType,
//客户端协议用于暴露给客户端的API接口类型
#[serde(default = "default_mcp_protocol", rename = "clientProtocol")]
pub client_protocol: McpProtocol,
// 解析后的服务器配置(可选)
#[serde(skip_serializing, skip_deserializing)]
pub server_config: Option<McpServerConfig>,
}
fn default_mcp_protocol() -> McpProtocol {
McpProtocol::Sse
}
fn default_mcp_type() -> McpType {
McpType::OneShot
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub enum McpType {
// 持续运行
Persistent,
// 一次性任务
#[default]
OneShot,
}
impl FromStr for McpType {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"persistent" => Ok(McpType::Persistent),
"oneShot" => Ok(McpType::OneShot),
_ => Err(anyhow::anyhow!("无效的 MCP 类型: {}", s)),
}
}
}
impl McpConfig {
pub fn new(
mcp_id: String,
mcp_json_config: Option<String>,
mcp_type: McpType,
client_protocol: McpProtocol,
) -> Self {
Self {
mcp_id,
mcp_json_config,
mcp_type,
client_protocol,
server_config: None,
}
}
pub fn from_json(json: &str) -> Result<Self> {
let config: McpConfig = serde_json::from_str(json)?;
Ok(config)
}
/// 从 JSON 字符串创建并解析服务器配置
pub fn from_json_with_server(
mcp_id: String,
mcp_json_config: String,
mcp_type: McpType,
client_protocol: McpProtocol,
) -> Result<Self> {
let mcp_json_server_parameters =
crate::model::McpJsonServerParameters::from(mcp_json_config.clone());
let server_config = mcp_json_server_parameters
.try_get_first_mcp_server()
.context("Failed to parse MCP server config")?;
Ok(Self {
mcp_id,
mcp_json_config: Some(mcp_json_config),
mcp_type,
client_protocol,
server_config: Some(server_config),
})
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,22 @@
mod app_state_model;
mod global;
mod http_result;
mod mcp_check_status_model;
mod mcp_config;
mod mcp_router_model;
pub use app_state_model::AppState;
pub use global::{
DynamicRouterService, GLOBAL_RESTART_TRACKER, McpServiceStatus, ProxyHandlerManager,
get_proxy_manager,
};
pub use http_result::HttpResult;
pub use mcp_check_status_model::{
CheckMcpStatusRequestParams, CheckMcpStatusResponseParams, CheckMcpStatusResponseStatus,
};
pub use mcp_config::{McpConfig, McpType};
pub use mcp_router_model::{
AddRouteParams, GLOBAL_SSE_MCP_ROUTES_PREFIX, GLOBAL_STREAM_MCP_ROUTES_PREFIX,
McpJsonServerParameters, McpProtocol, McpProtocolPath, McpRouterPath, McpServerCommandConfig,
McpServerConfig,
};

View File

@@ -0,0 +1,78 @@
//! Proxy module - re-exports handler types from proxy libraries
//!
//! This module provides a unified interface for proxy handlers by re-exporting
//! types from mcp-sse-proxy, mcp-streamable-proxy, and mcp-common libraries.
// Re-export SseHandler as ProxyHandler for backward compatibility
// SseHandler is used because it's based on rmcp 0.10 which supports both
// SSE server mode and CLI stdio mode used in the main project
pub use mcp_sse_proxy::SseHandler as ProxyHandler;
// Re-export SseServerHandler (unified handler for SSE server mode)
pub use mcp_sse_proxy::SseServerHandler;
// Re-export StreamProxyHandler with an alias to distinguish from SSE ProxyHandler
// Both mcp-sse-proxy and mcp-streamable-proxy export ProxyHandler, so we use an alias
pub use mcp_streamable_proxy::ProxyHandler as StreamProxyHandler;
// Re-export ToolFilter from mcp-common
pub use mcp_common::ToolFilter;
// Re-export client connection types for high-level API (each from its own library)
pub use mcp_sse_proxy::{McpClientConfig, SseClientConnection};
pub use mcp_streamable_proxy::StreamClientConnection;
// Re-export Builder APIs for server creation
pub use mcp_sse_proxy::server_builder::{BackendConfig as SseBackendConfig, SseServerBuilder};
pub use mcp_streamable_proxy::server_builder::{
BackendConfig as StreamBackendConfig, StreamServerBuilder,
};
/// Unified handler enum that can hold either SSE or Stream handler
///
/// This allows ProxyHandlerManager to store handlers of either type
/// while providing a common interface for status checks.
#[derive(Clone, Debug)]
pub enum McpHandler {
/// SSE protocol handler (from mcp-sse-proxy)
/// Holds SseServerHandler which unifies SseHandler and BackendSessionHandler
Sse(Box<SseServerHandler>),
/// Streamable HTTP protocol handler (from mcp-streamable-proxy)
Stream(Box<StreamProxyHandler>),
}
impl McpHandler {
/// Check if the underlying MCP server is ready
pub async fn is_mcp_server_ready(&self) -> bool {
match self {
McpHandler::Sse(h) => h.is_mcp_server_ready().await,
McpHandler::Stream(h) => h.is_mcp_server_ready().await,
}
}
/// Check if the backend connection is terminated
pub async fn is_terminated_async(&self) -> bool {
match self {
McpHandler::Sse(h) => h.is_terminated_async().await,
McpHandler::Stream(h) => h.is_terminated_async().await,
}
}
}
impl From<ProxyHandler> for McpHandler {
fn from(handler: ProxyHandler) -> Self {
McpHandler::Sse(Box::new(SseServerHandler::Sse(handler)))
}
}
impl From<SseServerHandler> for McpHandler {
fn from(handler: SseServerHandler) -> Self {
McpHandler::Sse(Box::new(handler))
}
}
impl From<StreamProxyHandler> for McpHandler {
fn from(handler: StreamProxyHandler) -> Self {
McpHandler::Stream(Box::new(handler))
}
}

View File

@@ -0,0 +1,46 @@
use axum::extract::Path;
use log::{info, warn};
use crate::{
AppError, get_proxy_manager,
model::{CheckMcpStatusResponseParams, CheckMcpStatusResponseStatus, HttpResult},
};
///根据 mcpId检查 mcp 透明代理服务是否正在运行的状态
// #[axum::debug_handler]
pub async fn check_mcp_is_status_handler(
Path(mcp_id): Path<String>,
) -> Result<HttpResult<CheckMcpStatusResponseParams>, AppError> {
info!("mcp_id: {mcp_id}");
let status = get_proxy_manager()
.get_mcp_service_status(&mcp_id)
.map(|mcp_service_status| mcp_service_status.check_mcp_status_response_status.clone());
if let Some(status) = status {
match status.clone() {
CheckMcpStatusResponseStatus::Ready => Ok(HttpResult::success(
CheckMcpStatusResponseParams::new(true, status, None),
None,
)),
CheckMcpStatusResponseStatus::Pending => Ok(HttpResult::success(
CheckMcpStatusResponseParams::new(false, status, None),
None,
)),
CheckMcpStatusResponseStatus::Error(err) => Ok(HttpResult::success(
CheckMcpStatusResponseParams::new(false, status, Some(err)),
None,
)),
}
} else {
warn!("mcp_id: {mcp_id} does not exist");
Ok(HttpResult::success(
CheckMcpStatusResponseParams::new(
false,
CheckMcpStatusResponseStatus::Error("mcp_id 不存在".to_string()),
None,
),
None,
))
}
}

View File

@@ -0,0 +1,24 @@
use axum::{extract::Path, response::IntoResponse};
use crate::{AppError, get_proxy_manager, model::HttpResult};
use anyhow::Result;
use serde_json::json;
// #[axum::debug_handler]
pub async fn delete_route_handler(
Path(mcp_id): Path<String>,
) -> Result<impl IntoResponse, AppError> {
// 删除动态路由,以及清理资源
get_proxy_manager()
.cleanup_resources(&mcp_id)
.await
.map_err(|e| AppError::mcp_server_error(e.to_string()))?;
// 返回成功信息
let data = json!({
"mcp_id": mcp_id,
"message": format!("已删除路由: {}", mcp_id)
});
Ok(HttpResult::success(data, None))
}

View File

@@ -0,0 +1,11 @@
use crate::AppError;
use axum::response::IntoResponse;
///健康检查:health
pub async fn get_health() -> Result<impl IntoResponse, AppError> {
Ok("health".to_string())
}
pub async fn get_ready() -> Result<impl IntoResponse, AppError> {
Ok("ready".to_string())
}

View File

@@ -0,0 +1,98 @@
use axum::{Json, extract::State, response::IntoResponse};
use http::Uri;
use log::{debug, error};
use tracing::instrument;
use uuid::Uuid;
use crate::AppError;
use crate::model::{
AddRouteParams, HttpResult, McpConfig, McpProtocolPath, McpServerConfig, McpType,
};
use crate::model::{AppState, McpRouterPath};
use crate::server::task::integrate_server_with_axum;
use serde_json::json;
// 修改 add_route_handler 函数,使用新的集成方法
#[instrument]
// #[axum::debug_handler]
pub async fn add_route_handler(
State(state): State<AppState>,
uri: Uri,
Json(params): Json<AddRouteParams>,
) -> Result<impl IntoResponse, AppError> {
// 获取请求路径
let request_path = uri.path().to_string();
debug!("Request path: {}", request_path);
debug!("Full URI: {:?}", uri);
let mcp_protocol = McpRouterPath::from_uri_prefix_protocol(&request_path);
if let Some(mcp_protocol) = mcp_protocol {
// 生成mcp_id, 使用uuid,去掉-
let mcp_id = Uuid::now_v7().to_string().replace("-", "");
//根据 mcp_id 和协议,生成 mcp_router_path
let mcp_router_path = McpRouterPath::new(mcp_id, mcp_protocol)
.map_err(|e| AppError::mcp_server_error(e.to_string()))?;
let mcp_plugin_json = params.mcp_json_config;
// 将mcp_plugin_json转换为 McpServerConfig 结构体
let mcp_server_config =
McpServerConfig::try_from(mcp_plugin_json.clone()).expect("解析 MCP 配置失败");
let mcp_type = params.mcp_type.unwrap_or(McpType::default());
debug!("Client protocol: {:?}", mcp_router_path.mcp_protocol);
// 构建完整的 McpConfig用于自动重启
let full_mcp_config = McpConfig {
mcp_id: mcp_router_path.mcp_id.clone(),
client_protocol: mcp_router_path.mcp_protocol.clone(),
mcp_type: mcp_type.clone(),
mcp_json_config: Some(mcp_plugin_json),
server_config: Some(mcp_server_config.clone()),
};
// 使用新的集成方法,后端协议在函数内部解析
let _ = integrate_server_with_axum(
mcp_server_config.clone(),
mcp_router_path.clone(),
full_mcp_config,
)
.await
.map_err(|e| {
error!("Failed to start MCP service: {e}");
AppError::mcp_server_error(e.to_string())
})?;
//区分 mcp协议
let mcp_protocol = mcp_router_path.mcp_protocol_path.clone();
let data = match mcp_protocol {
McpProtocolPath::SsePath(sse_path) => {
// 返回 mcp_id 和 sse_path
let data = json!({
"mcp_id": mcp_router_path.mcp_id,
"sse_path": sse_path.sse_path,
"message_path": sse_path.message_path,
"mcp_type": mcp_type
});
data
}
McpProtocolPath::StreamPath(stream_path) => {
// 返回 mcp_id 和 stream_path
let data = json!({
"mcp_id": mcp_router_path.mcp_id,
"stream_path": stream_path.stream_path,
"mcp_type": mcp_type
});
data
}
};
Ok(HttpResult::success(data, None))
} else {
//返回 bad request,400 错误
return Err(AppError::invalid_parameter("无效的请求路径"));
}
}

View File

@@ -0,0 +1,281 @@
use anyhow::Result;
use axum::{Json, extract::State, http::uri::Uri};
use log::{debug, error, info};
use tokio_util::sync::CancellationToken;
use tracing::instrument;
use crate::{
AppError, get_proxy_manager,
model::{
AppState, CheckMcpStatusRequestParams, CheckMcpStatusResponseParams,
CheckMcpStatusResponseStatus, GLOBAL_RESTART_TRACKER, HttpResult, McpConfig, McpProtocol,
McpRouterPath, McpServiceStatus, McpType,
},
server::mcp_start_task,
};
/// 创建响应结果的辅助函数
fn create_response(
ready: bool,
status: CheckMcpStatusResponseStatus,
message: Option<String>,
) -> Result<HttpResult<CheckMcpStatusResponseParams>, AppError> {
let response = CheckMcpStatusResponseParams::new(ready, status, message);
Ok(HttpResult::success(response, None))
}
/// 检查mcp服务状态,根据 mcp_id 获取有无对应的mcp透明代理服务,如果没有则取 mcp_json_config 中的配置,生成mcp透明代理服务;
/// 这里根据 mcp_json_config配置,启动服务需要异步,不要阻塞,如果服务没准备好,返回 PENDING 状态;
/// 如果服务启动失败,返回 ERROR 状态;
/// 如果服务启动成功,返回 READY 状态;
#[instrument]
pub async fn check_mcp_status_handler(
State(state): State<AppState>,
uri: Uri,
Json(params): Json<CheckMcpStatusRequestParams>,
mcp_protocol: McpProtocol,
) -> Result<HttpResult<CheckMcpStatusResponseParams>, AppError> {
// 使用全局 ProxyHandlerManager
let proxy_manager = get_proxy_manager();
// 检查服务状态
let status = proxy_manager
.get_mcp_service_status(&params.mcp_id)
.map(|mcp_service_status| mcp_service_status.check_mcp_status_response_status.clone());
if let Some(status) = status {
match status {
CheckMcpStatusResponseStatus::Error(error_msg) => {
// 如果有错误状态,返回错误信息,另外删除掉 ERROR 的记录,方便下次检查状态,重新启动服务
// Error 状态不更新 last_accessed因为服务已失败
if let Err(e) = proxy_manager.cleanup_resources(&params.mcp_id).await {
error!("Failed to cleanup resources for {}: {}", params.mcp_id, e);
}
// 返回错误信息
return create_response(
false,
CheckMcpStatusResponseStatus::Error(error_msg),
None,
);
}
CheckMcpStatusResponseStatus::Pending => {
// 如果状态是 Pending说明服务正在启动中直接返回 Pending
// 不要再次尝试启动!
debug!(
"[check_mcp_status] mcp_id={} status is Pending and the service is starting",
params.mcp_id
);
// 更新最后访问时间,避免启动过程中因超时被清理
// 用户调用 check_status 表明对服务有兴趣,应该延长超时
proxy_manager.update_last_accessed(&params.mcp_id);
return create_response(
false,
CheckMcpStatusResponseStatus::Pending,
Some("服务正在启动中...".to_string()),
);
}
CheckMcpStatusResponseStatus::Ready => {
// 如果已经在运行,继续检查服务是否真的可用
debug!(
"[check_mcp_status] mcp_id={} status is Ready, check the backend health status",
params.mcp_id
);
}
}
}
// 检查 proxy_handler 是否存在
let proxy_handler = proxy_manager.get_proxy_handler(&params.mcp_id);
if let Some(handler) = proxy_handler {
// 调用透明代理的 is_mcp_server_ready 方法检查健康状态
let ready_status = handler.is_mcp_server_ready().await;
// 使用 update 方法更新状态(不是修改克隆)
if ready_status {
proxy_manager
.update_mcp_service_status(&params.mcp_id, CheckMcpStatusResponseStatus::Ready);
}
// 更新最后访问时间
proxy_manager.update_last_accessed(&params.mcp_id);
let status = if ready_status {
CheckMcpStatusResponseStatus::Ready
} else {
CheckMcpStatusResponseStatus::Pending
};
return create_response(ready_status, status, None);
}
// ===== 服务不存在,需要启动 =====
// 使用启动锁防止并发启动同一服务
let _startup_guard = match GLOBAL_RESTART_TRACKER.try_acquire_startup_lock(&params.mcp_id) {
Some(guard) => {
debug!(
"[check_mcp_status] mcp_id={} Obtained the startup lock successfully and started to start the service",
params.mcp_id
);
guard
}
None => {
// 锁被占用,服务正在启动中
debug!(
"[check_mcp_status] mcp_id={} The startup lock is occupied and the service is starting. Return to Pending.",
params.mcp_id
);
return create_response(
false,
CheckMcpStatusResponseStatus::Pending,
Some("服务正在启动中...".to_string()),
);
}
};
// 双重检查:获取锁后再次检查服务是否已存在
if proxy_manager.get_proxy_handler(&params.mcp_id).is_some() {
debug!(
"[check_mcp_status] mcp_id={} Double check found that the service already exists, return Ready",
params.mcp_id
);
return create_response(true, CheckMcpStatusResponseStatus::Ready, None);
}
// 如果服务状态已存在(可能是 Pending也不要重复启动
if proxy_manager
.get_mcp_service_status(&params.mcp_id)
.is_some()
{
debug!(
"[check_mcp_status] mcp_id={} The service status already exists and may be starting. Return to Pending.",
params.mcp_id
);
return create_response(
false,
CheckMcpStatusResponseStatus::Pending,
Some("服务正在启动中...".to_string()),
);
}
// 启动服务(持有锁的情况下)
spawn_mcp_service(
&params.mcp_id,
params.mcp_json_config,
params.mcp_type,
mcp_protocol.clone(),
)?;
// 返回 PENDING 状态,锁会在函数返回时自动释放
create_response(
false,
CheckMcpStatusResponseStatus::Pending,
Some("服务正在启动中...".to_string()),
)
}
// SSE协议专用的状态检查处理函数
#[instrument]
// #[axum::debug_handler]
pub async fn check_mcp_status_handler_sse(
state: State<AppState>,
uri: Uri,
params: Json<CheckMcpStatusRequestParams>,
) -> Result<HttpResult<CheckMcpStatusResponseParams>, AppError> {
check_mcp_status_handler(state, uri, params, McpProtocol::Sse).await
}
// Stream协议专用的状态检查处理函数
#[instrument]
// #[axum::debug_handler]
pub async fn check_mcp_status_handler_stream(
state: State<AppState>,
uri: Uri,
params: Json<CheckMcpStatusRequestParams>,
) -> Result<HttpResult<CheckMcpStatusResponseParams>, AppError> {
check_mcp_status_handler(state, uri, params, McpProtocol::Stream).await
}
/// 异步启动MCP服务
///
/// 注意:此函数假设调用方已持有启动锁,不会重复检查!
///
/// # 参数
/// - `mcp_id`: MCP服务的唯一标识
/// - `mcp_json_config`: MCP服务的JSON配置
/// - `mcp_type`: MCP服务类型OneShot或Persistent
/// - `client_protocol`: 客户端使用的协议决定暴露的API接口类型
fn spawn_mcp_service(
mcp_id: &str,
mcp_json_config: String,
mcp_type: McpType,
client_protocol: McpProtocol,
) -> Result<(), AppError> {
let mcp_id = mcp_id.to_string();
info!(
"[spawn_mcp_service] mcp_id={} Start starting the service",
mcp_id
);
// 使用全局 ProxyHandlerManager
let proxy_manager = get_proxy_manager();
// 设置初始化状态 - 使用客户端协议创建路由路径
let mcp_router_path = McpRouterPath::new(mcp_id.clone(), client_protocol.clone())
.map_err(|e| AppError::mcp_server_error(e.to_string()))?;
let mcp_service_status = McpServiceStatus::new(
mcp_id.clone(),
mcp_type.clone(),
mcp_router_path,
CancellationToken::new(),
CheckMcpStatusResponseStatus::Pending,
);
// RAII: 如果已存在同名服务,会自动清理旧服务
proxy_manager.add_mcp_service_status_and_proxy(mcp_service_status, None);
// 异步启动 mcp 透明代理服务
let mcp_id_clone = mcp_id.clone();
// 使用客户端协议创建配置
let mcp_config: McpConfig = McpConfig::new(
mcp_id_clone.clone(),
Some(mcp_json_config),
mcp_type,
client_protocol,
);
tokio::spawn(async move {
info!(
"[spawn_mcp_service] mcp_id={} tokio::spawn starts executing mcp_start_task",
mcp_id_clone
);
match mcp_start_task(mcp_config).await {
Ok(_) => {
info!(
"[spawn_mcp_service] mcp_id={} mcp_start_task successful, set status to Ready",
mcp_id_clone
);
get_proxy_manager()
.update_mcp_service_status(&mcp_id_clone, CheckMcpStatusResponseStatus::Ready);
}
Err(e) => {
let error_msg = format!("启动MCP服务失败: {e}");
error!(
"[spawn_mcp_service] mcp_id={} mcp_start_task failed: {}",
mcp_id_clone, e
);
get_proxy_manager().update_mcp_service_status(
&mcp_id_clone,
CheckMcpStatusResponseStatus::Error(error_msg),
);
}
}
});
info!(
"[spawn_mcp_service] mcp_id={} Service startup task has been submitted",
mcp_id
);
Ok(())
}

View File

@@ -0,0 +1,13 @@
mod check_mcp_is_status;
mod delete_route_handler;
mod health;
mod mcp_add_handler;
mod mcp_check_status_handler;
pub mod run_code_handler;
pub use check_mcp_is_status::check_mcp_is_status_handler;
pub use delete_route_handler::delete_route_handler;
pub use health::get_health;
pub use health::get_ready;
pub use mcp_add_handler::add_route_handler;
pub use mcp_check_status_handler::{check_mcp_status_handler_sse, check_mcp_status_handler_stream};
pub use run_code_handler::run_code_handler;

View File

@@ -0,0 +1,92 @@
use std::collections::HashMap;
use axum::{Json, response::IntoResponse};
use http::StatusCode;
use log::{debug, error, info};
use run_code_rmcp::{CodeExecutor, LanguageScript, RunCodeHttpResult};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use crate::AppError;
///代码运行请求
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RunCodeMessageRequest {
//js运行参数
pub json_param: HashMap<String, Value>,
//运行的代码
pub code: String,
//前端生成的随机uid,用于查找websocket连接,发送执行过程中的log日志
pub uid: String,
pub engine_type: String,
}
impl RunCodeMessageRequest {
//获取语言脚本
pub fn get_language_script(&self) -> LanguageScript {
match self.engine_type.as_str() {
"js" => LanguageScript::Js,
"ts" => LanguageScript::Ts,
"python" => LanguageScript::Python,
_ => LanguageScript::Js,
}
}
}
/// 执行js/ts/python代码,通过 uv/deno 命令方式执行
// #[axum::debug_handler]
pub async fn run_code_handler(
Json(run_code_message_request): Json<RunCodeMessageRequest>,
) -> Result<impl IntoResponse, AppError> {
//json_param: HashMap<String, Value> 转换为 json 对象 Value
let params = match serde_json::to_value(run_code_message_request.json_param.clone()) {
Ok(v) => v,
Err(e) => {
error!("Run_code_handler parameter serialization failed: {e:?}");
return Err(AppError::from(e));
}
};
//执行代码
let code = run_code_message_request.code.clone();
let language = run_code_message_request.get_language_script();
debug!("run_code_handler language:{language:?}");
debug!("run_code_handler code:{code:?}");
debug!("run_code_handler params:{params:?}");
let result = match CodeExecutor::execute_with_params(&code, language, Some(params), None).await
{
Ok(result) => result,
Err(e) => {
error!("run_code_handler execution failed: {e:?}");
return Err(AppError::from(e));
}
};
if !result.success {
let error_message = result
.error
.as_deref()
.unwrap_or("run_code_handler执行失败但未提供错误信息");
error!("run_code_handler execution returns failure: {error_message}");
}
let data = match serde_json::to_value(&result) {
Ok(data) => data,
Err(e) => {
error!("run_code_handler serialization result failed: {e:?}");
return Err(AppError::from(e));
}
};
//打印结果
info!("run_code_handler result:{:?}", &result.success);
debug!("run_code_handler result:{:?}", &data);
//返回结果,使用 RunCodeHttpResult 封装执行结果
let run_code_http_result = RunCodeHttpResult {
data,
success: result.success,
error: result.error.clone(),
};
let body = serde_json::to_string(&run_code_http_result)?;
Ok((StatusCode::OK, body).into_response())
}

View File

@@ -0,0 +1,647 @@
use std::{
convert::Infallible,
task::{Context, Poll},
};
use axum::{
body::Body,
extract::Request,
response::{IntoResponse, Response},
};
use futures::future::BoxFuture;
use log::{debug, error, info, warn};
use tower::Service;
use crate::{
DynamicRouterService, get_proxy_manager, mcp_start_task,
model::{
CheckMcpStatusResponseStatus, GLOBAL_RESTART_TRACKER, HttpResult, McpConfig, McpRouterPath,
McpType,
},
server::middlewares::extract_trace_id,
};
impl Service<Request<Body>> for DynamicRouterService {
type Response = Response;
type Error = Infallible;
type Future = BoxFuture<'static, Result<Self::Response, Self::Error>>;
fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
Poll::Ready(Ok(()))
}
fn call(&mut self, req: Request<Body>) -> Self::Future {
let path = req.uri().path().to_string();
let method = req.method().clone();
let headers = req.headers().clone();
// DEBUG: 详细路径解析日志
debug!("=== Path analysis begins ===");
debug!("Original request path: {}", path);
debug!("Path contains wildcard parameters: {:?}", req.extensions());
// 提取 trace_id
let trace_id = extract_trace_id();
// 创建根 span (使用 debug_span 减少日志量)
let span = tracing::debug_span!(
"DynamicRouterService",
otel.name = "HTTP Request",
http.method = %method,
http.route = %path,
http.url = %req.uri(),
mcp.protocol = format!("{:?}", self.0),
trace_id = %trace_id,
);
// 记录请求头信息
if let Some(content_type) = headers.get("content-type") {
span.record("http.request.content_type", format!("{:?}", content_type));
}
if let Some(content_length) = headers.get("content-length") {
span.record(
"http.request.content_length",
format!("{:?}", content_length),
);
}
debug!("Request path: {path}");
// 解析路由路径
let mcp_router_path = McpRouterPath::from_url(&path);
match mcp_router_path {
Some(mcp_router_path) => {
let mcp_id = mcp_router_path.mcp_id.clone();
let base_path = mcp_router_path.base_path.clone();
span.record("mcp.id", &mcp_id);
span.record("mcp.base_path", &base_path);
debug!("=== Path analysis results ===");
debug!("Parsed mcp_id: {}", mcp_id);
debug!("Parsed base_path: {}", base_path);
debug!("Request path: {} vs base_path: {}", path, base_path);
debug!("=== Path analysis ends ===");
Box::pin(async move {
let _guard = span.enter();
// 先尝试查找已注册的路由
debug!("===Route lookup process ===");
debug!("Find base_path: '{}'", base_path);
if let Some(router_entry) = DynamicRouterService::get_route(&base_path) {
debug!(
"✅ Find the registered route: base_path={}, path={}",
base_path, path
);
// ===== 检查后端健康状态 =====
let mcp_id_for_check = McpRouterPath::from_url(&path);
if let Some(router_path) = mcp_id_for_check {
let proxy_manager = get_proxy_manager();
// ===== 首先检查服务状态 =====
// 如果服务状态是 Pending说明服务正在初始化中uvx/npx 下载中)
// 此时不应该做健康检查,应该等待
if let Some(service_status) =
proxy_manager.get_mcp_service_status(&router_path.mcp_id)
{
match &service_status.check_mcp_status_response_status {
CheckMcpStatusResponseStatus::Pending => {
debug!(
"[MCP status check] mcp_id={} The status is Pending, the service is being initialized, and 503 is returned.",
router_path.mcp_id
);
let message = format!(
"服务 {} 正在初始化中,请稍后再试",
router_path.mcp_id
);
let http_result: HttpResult<String> =
HttpResult::error("0003", &message, None);
return Ok(http_result.into_response());
}
CheckMcpStatusResponseStatus::Error(err) => {
// Error 状态:只清理,不重启
// 避免有问题的 MCP 服务无限重启循环
warn!(
"[MCP status check] mcp_id={} status is Error: {}, clean up resources and return error",
router_path.mcp_id, err
);
// 清理资源
if let Err(e) = proxy_manager
.cleanup_resources(&router_path.mcp_id)
.await
{
error!(
"[MCP status check] mcp_id={} Failed to clean up resources: {}",
router_path.mcp_id, e
);
}
// 返回错误,不尝试重启
let message = format!(
"服务 {} 启动失败: {}",
router_path.mcp_id, err
);
let http_result: HttpResult<String> =
HttpResult::error("0005", &message, None);
return Ok(http_result.into_response());
}
CheckMcpStatusResponseStatus::Ready => {
debug!(
"[MCP status check] mcp_id={} status is Ready, continue to check the backend health status",
router_path.mcp_id
);
}
}
}
// ===== 检查启动锁状态 =====
// 如果锁被占用,说明服务正在启动中
let startup_guard = GLOBAL_RESTART_TRACKER
.try_acquire_startup_lock(&router_path.mcp_id);
if startup_guard.is_none() {
// 锁被占用,服务正在启动中,返回 503
debug!(
"[Startup lock check] mcp_id={} The startup lock is occupied, the service is starting, and 503 is returned.",
router_path.mcp_id
);
span.record("mcp.startup_in_progress", true);
let message =
format!("服务 {} 正在启动中,请稍后再试", router_path.mcp_id);
let http_result: HttpResult<String> =
HttpResult::error("0003", &message, None);
span.record("http.response.status_code", 503u16);
return Ok(http_result.into_response());
}
// 获取到锁,现在可以安全地检查健康状态
let _startup_guard = startup_guard.unwrap();
debug!(
"[Start lock check] mcp_id={} Successfully obtained the startup lock and started health check",
router_path.mcp_id
);
if let Some(handler) =
proxy_manager.get_proxy_handler(&router_path.mcp_id)
{
// ===== 健康检查(带缓存)=====
let is_healthy = if let Some(cached) = GLOBAL_RESTART_TRACKER
.get_cached_health_status(&router_path.mcp_id)
{
debug!(
"[Health Check] mcp_id={} Use cache status: is_healthy={}",
router_path.mcp_id, cached
);
cached
} else {
debug!(
"[Health Check] mcp_id={} Cache miss, start actual health check...",
router_path.mcp_id
);
let status = handler.is_mcp_server_ready().await;
GLOBAL_RESTART_TRACKER
.update_health_status(&router_path.mcp_id, status);
debug!(
"[Health Check] mcp_id={} Actual health check result: is_healthy={}",
router_path.mcp_id, status
);
status
};
if is_healthy {
debug!(
"[Health Check] mcp_id={} The backend service is normal, release the lock and use routing",
router_path.mcp_id
);
// 释放锁,使用路由
drop(_startup_guard);
debug!("=== Route search ended (successful) ===");
return handle_request_with_router(req, router_entry, &path)
.await;
}
// 不健康,获取服务类型以决定是否重启
let mcp_type = proxy_manager
.get_mcp_service_status(&router_path.mcp_id)
.map(|s| s.mcp_type.clone());
// 清理资源
warn!(
"[Health check] mcp_id={} The backend service is unhealthy, clean up resources.",
router_path.mcp_id
);
if let Err(e) =
proxy_manager.cleanup_resources(&router_path.mcp_id).await
{
error!(
"[Clean up resources] mcp_id={} Failed to clean up resources: error={}",
router_path.mcp_id, e
);
} else {
debug!(
"[Clean up resources] mcp_id={} Clean up resources successfully",
router_path.mcp_id
);
}
// OneShot 类型:只清理,不重启
// OneShot 服务执行完成后进程会退出,这是正常行为,不应该自动重启
// 用户需要通过 check_status 接口显式启动新的 OneShot 服务
if matches!(mcp_type, Some(McpType::OneShot)) {
debug!(
"[Health Check] mcp_id={} is a OneShot type, does not automatically restart, and returns that the service has ended",
router_path.mcp_id
);
let message = format!(
"OneShot 服务 {} 已结束,请重新启动",
router_path.mcp_id
);
let http_result: HttpResult<String> =
HttpResult::error("0006", &message, None);
return Ok(http_result.into_response());
}
// Persistent 类型:清理后重启
info!(
"[Restart process] mcp_id={} is Persistent type, start to restart the service",
router_path.mcp_id
);
// 从配置获取 mcp_config 并启动服务
// 优先从请求 header 获取配置
if let Some(mcp_config) =
req.extensions().get::<McpConfig>().cloned()
&& mcp_config.mcp_json_config.is_some()
{
info!(
"[Restart process] mcp_id={} Use the request header to configure the restart service",
mcp_config.mcp_id
);
proxy_manager
.register_mcp_config(&mcp_config.mcp_id, mcp_config.clone())
.await;
return start_mcp_and_handle_request(req, mcp_config).await;
}
// 从缓存获取配置
if let Some(mcp_config) = proxy_manager
.get_mcp_config_from_cache(&router_path.mcp_id)
.await
{
info!(
"[Restart process] mcp_id={} Restart the service using cache configuration",
router_path.mcp_id
);
return start_mcp_and_handle_request(req, mcp_config).await;
}
// 无法获取配置
warn!(
"[Restart Process] mcp_id={} Unable to obtain the configuration and unable to restart the service",
router_path.mcp_id
);
let message =
format!("服务 {} 不健康且无法获取配置", router_path.mcp_id);
let http_result: HttpResult<String> =
HttpResult::error("0004", &message, None);
return Ok(http_result.into_response());
} else {
// handler 不存在,但路由存在
// 检查服务类型OneShot 不自动重启
let mcp_type = proxy_manager
.get_mcp_service_status(&router_path.mcp_id)
.map(|s| s.mcp_type.clone());
if matches!(mcp_type, Some(McpType::OneShot)) {
debug!(
"[Service Check] mcp_id={} is OneShot type and the handler does not exist, so it will not restart automatically.",
router_path.mcp_id
);
// 清理残留状态
if let Err(e) =
proxy_manager.cleanup_resources(&router_path.mcp_id).await
{
error!(
"[Clean up resources] mcp_id={} Failed to clean up resources: {}",
router_path.mcp_id, e
);
}
let message = format!(
"OneShot 服务 {} 已结束,请重新启动",
router_path.mcp_id
);
let http_result: HttpResult<String> =
HttpResult::error("0006", &message, None);
return Ok(http_result.into_response());
}
// Persistent 类型:继续进入启动流程
warn!(
"The route exists but the handler does not exist. Enter the restart process: base_path={}",
base_path
);
}
} else {
// 无法解析路由路径,直接使用路由
debug!("=== Route search ended (successful) ===");
return handle_request_with_router(req, router_entry, &path).await;
}
} else {
debug!(
"❌ No registered route found: base_path='{}', path='{}'",
base_path, path
);
// 显示所有已注册的路由
let all_routes = DynamicRouterService::get_all_routes();
debug!("Currently registered route: {:?}", all_routes);
debug!("=== Route search ended (failed) ===");
}
// 未找到路由,尝试启动服务
warn!(
"No matching path found, try to start the service: base_path={base_path}, path={path}"
);
span.record("error.route_not_found", true);
// ===== 提前解析 mcp_id 用于配置获取 =====
let mcp_router_path_for_config = McpRouterPath::from_url(&path);
// ===== 配置获取优先级 =====
let proxy_manager = get_proxy_manager();
// 优先级 1: 从请求 header 中获取配置(最新)
if let Some(mcp_config) = req.extensions().get::<McpConfig>().cloned()
&& mcp_config.mcp_json_config.is_some()
{
// 检查重启限制(防止无限循环)
if !GLOBAL_RESTART_TRACKER.can_restart(&mcp_config.mcp_id) {
warn!(
"Service {} skips startup during restart cooldown period",
mcp_config.mcp_id
);
span.record("error.restart_in_cooldown", true);
let message =
format!("服务 {} 在重启冷却期内,请稍后再试", mcp_config.mcp_id);
let http_result: HttpResult<String> =
HttpResult::error("0002", &message, None);
span.record("http.response.status_code", 429u16); // Too Many Requests
return Ok(http_result.into_response());
}
// 尝试获取启动锁,防止并发启动同一服务
let _startup_guard = match GLOBAL_RESTART_TRACKER
.try_acquire_startup_lock(&mcp_config.mcp_id)
{
Some(guard) => guard,
None => {
warn!(
"Service {} is starting, skip this startup",
mcp_config.mcp_id
);
span.record("error.startup_in_progress", true);
let message =
format!("服务 {} 正在启动中,请稍后再试", mcp_config.mcp_id);
let http_result: HttpResult<String> =
HttpResult::error("0003", &message, None);
span.record("http.response.status_code", 503u16); // Service Unavailable
return Ok(http_result.into_response());
}
};
info!(
"Use request header configuration to start the service: {}",
mcp_config.mcp_id
);
// 同时更新缓存
proxy_manager
.register_mcp_config(&mcp_config.mcp_id, mcp_config.clone())
.await;
// _startup_guard 会在作用域结束时自动释放
return start_mcp_and_handle_request(req, mcp_config).await;
}
// 优先级 2: 从 moka 缓存中获取配置(兜底)
// 注意OneShot 类型不从缓存自动启动,需要用户显式请求(带 header 配置)
if let Some(mcp_id_for_cache) =
mcp_router_path_for_config.as_ref().map(|p| &p.mcp_id)
&& let Some(mcp_config) = proxy_manager
.get_mcp_config_from_cache(mcp_id_for_cache)
.await
{
// OneShot 类型不从缓存自动启动
// 避免已回收的 OneShot 服务被意外重启
if matches!(mcp_config.mcp_type, McpType::OneShot) {
info!(
"[Startup check] mcp_id={} is a OneShot type, does not automatically start from the cache, and requires an explicit request from the user",
mcp_id_for_cache
);
let message = format!(
"OneShot 服务 {} 需要通过 check_status 接口启动",
mcp_id_for_cache
);
let http_result: HttpResult<String> =
HttpResult::error("0007", &message, None);
return Ok(http_result.into_response());
}
// 检查重启限制(防止无限循环)
if !GLOBAL_RESTART_TRACKER.can_restart(mcp_id_for_cache) {
warn!(
"Service {} skips startup during restart cooldown period",
mcp_id_for_cache
);
span.record("error.restart_in_cooldown", true);
let message =
format!("服务 {} 在重启冷却期内,请稍后再试", mcp_id_for_cache);
let http_result: HttpResult<String> =
HttpResult::error("0002", &message, None);
span.record("http.response.status_code", 429u16); // Too Many Requests
return Ok(http_result.into_response());
}
// 尝试获取启动锁,防止并发启动同一服务
let _startup_guard = match GLOBAL_RESTART_TRACKER
.try_acquire_startup_lock(mcp_id_for_cache)
{
Some(guard) => guard,
None => {
warn!(
"Service {} is starting, skip this startup",
mcp_id_for_cache
);
span.record("error.startup_in_progress", true);
let message =
format!("服务 {} 正在启动中,请稍后再试", mcp_id_for_cache);
let http_result: HttpResult<String> =
HttpResult::error("0003", &message, None);
span.record("http.response.status_code", 503u16); // Service Unavailable
return Ok(http_result.into_response());
}
};
info!(
"Start the service using cached configuration: {}",
mcp_id_for_cache
);
// _startup_guard 会在作用域结束时自动释放
return start_mcp_and_handle_request(req, mcp_config).await;
}
// 优先级 3: 无法获取配置,返回错误
warn!(
"No matching path was found, and the configuration was not obtained, so the MCP service could not be started: {path}"
);
span.record("error.mcp_config_missing", true);
let message =
format!("未找到匹配的路径,且未获取到配置,无法启动MCP服务: {path}");
let http_result: HttpResult<String> = HttpResult::error("0001", &message, None);
span.record("http.response.status_code", 404u16);
span.record("error.message", &message);
Ok(http_result.into_response())
})
}
None => {
warn!("Request path resolution failed: {path}");
span.record("error.path_parse_failed", true);
let message = format!("请求路径解析失败: {path}");
let http_result: HttpResult<String> = HttpResult::error("0001", &message, None);
Box::pin(async move {
let _guard = span.enter();
span.record("http.response.status_code", 400u16);
span.record("error.message", &message);
Ok(http_result.into_response())
})
}
}
}
}
/// 使用给定的路由处理请求
async fn handle_request_with_router(
req: Request<Body>,
router_entry: axum::Router,
path: &str,
) -> Result<Response, Infallible> {
// 获取匹配路径的Router并处理请求
let trace_id = extract_trace_id();
let method = req.method().clone();
let uri = req.uri().clone();
info!(
"[handle_request_with_router] Handle request: {} {}",
method, path
);
// 记录请求头中的关键信息
if let Some(content_type) = req.headers().get("content-type")
&& let Ok(content_type_str) = content_type.to_str()
{
debug!(
"[handle_request_with_router] Content-Type: {}",
content_type_str
);
}
if let Some(content_length) = req.headers().get("content-length")
&& let Ok(content_length_str) = content_length.to_str()
{
debug!(
"[handle_request_with_router] Content-Length: {}",
content_length_str
);
}
// 记录 x-mcp-json 头信息(如果存在)
if let Some(mcp_json) = req.headers().get("x-mcp-json")
&& let Ok(mcp_json_str) = mcp_json.to_str()
{
debug!(
"[handle_request_with_router] MCP-JSON Header: {}",
mcp_json_str
);
}
// 记录查询参数
if let Some(query) = uri.query() {
debug!("[handle_request_with_router] Query: {}", query);
}
// 使用 debug_span 减少日志量,因为 DynamicRouterService 已经记录了请求信息
// 移除 #[tracing::instrument] 避免 span 嵌套导致的日志膨胀问题
let span = tracing::debug_span!(
"handle_request_with_router",
otel.name = "Handle Request with Router",
component = "router",
trace_id = %trace_id,
http.method = %method,
http.path = %path,
);
let _guard = span.enter();
let mut service = router_entry.into_service();
match service.call(req).await {
Ok(response) => {
let status = response.status();
// 记录响应头信息
debug!(
"[handle_request_with_router]Response status: {}, response header: {response:?}",
status
);
span.record("http.response.status_code", status.as_u16());
Ok(response)
}
Err(error) => {
span.record("error.router_service_error", true);
span.record("error.message", format!("{:?}", error));
error!("[handle_request_with_router] error: {error:?}");
Ok(axum::http::StatusCode::INTERNAL_SERVER_ERROR.into_response())
}
}
}
/// 启动MCP服务并处理请求
async fn start_mcp_and_handle_request(
req: Request<Body>,
mcp_config: McpConfig,
) -> Result<Response, Infallible> {
let request_path = req.uri().path().to_string();
let trace_id = extract_trace_id();
debug!("Request path: {request_path}");
// 使用 debug_span 减少日志量,移除 #[tracing::instrument] 避免 span 嵌套
let span = tracing::debug_span!(
"start_mcp_and_handle_request",
otel.name = "Start MCP and Handle Request",
component = "mcp_startup",
mcp.id = %mcp_config.mcp_id,
mcp.type = ?mcp_config.mcp_type,
mcp.config.has_config = mcp_config.mcp_json_config.is_some(),
trace_id = %trace_id,
);
let _guard = span.enter();
let ret = mcp_start_task(mcp_config).await;
if let Ok((router, _)) = ret {
span.record("mcp.startup.success", true);
handle_request_with_router(req, router, &request_path).await
} else {
span.record("mcp.startup.failed", true);
span.record("error.mcp_startup_failed", true);
span.record("error.message", format!("{:?}", ret));
warn!("MCP service startup failed: {ret:?}");
Ok(axum::http::StatusCode::INTERNAL_SERVER_ERROR.into_response())
}
}

View File

@@ -0,0 +1,177 @@
// use super::TokenVerify;
// use axum::{
// extract::{FromRequestParts, Query, Request, State},
// http::{request::Parts, StatusCode},
// middleware::Next,
// response::{IntoResponse, Response},
// };
// use axum_extra::{
// headers::{authorization::Bearer, Authorization},
// TypedHeader,
// };
// use serde::Deserialize;
// use tracing::warn;
//
// #[derive(Debug, Deserialize)]
// struct Params {
// token: String,
// }
//
// pub async fn verify_token<T>(State(state): State<T>, req: Request, next: Next) -> Response
// where
// T: TokenVerify + Clone + Send + Sync + 'static,
// {
// let (mut parts, body) = req.into_parts();
// match extract_token(&state, &mut parts).await {
// Ok(token) => {
// let mut req = Request::from_parts(parts, body);
// match set_user(&state, &token, &mut req) {
// Ok(_) => next.run(req).await,
// Err(msg) => (StatusCode::FORBIDDEN, msg).into_response(),
// }
// }
// Err(msg) => (StatusCode::UNAUTHORIZED, msg).into_response(),
// }
// }
//
// pub async fn extract_user<T>(State(state): State<T>, req: Request, next: Next) -> Response
// where
// T: TokenVerify + Clone + Send + Sync + 'static,
// {
// let (mut parts, body) = req.into_parts();
// let req = if let Ok(token) = extract_token(&state, &mut parts).await {
// let mut req = Request::from_parts(parts, body);
// let _ = set_user(&state, &token, &mut req);
// req
// } else {
// Request::from_parts(parts, body)
// };
//
// next.run(req).await
// }
//
// async fn extract_token<T>(state: &T, parts: &mut Parts) -> Result<String, String>
// where
// T: TokenVerify + Clone + Send + Sync + 'static,
// {
// match TypedHeader::<Authorization<Bearer>>::from_request_parts(parts, &state).await {
// Ok(TypedHeader(Authorization(bearer))) => Ok(bearer.token().to_string()),
// Err(e) => {
// if e.is_missing() {
// match Query::<Params>::from_request_parts(parts, &state).await {
// Ok(params) => Ok(params.token.clone()),
// Err(e) => {
// let msg = format!("parse query params failed: {}", e);
// warn!(msg);
// Err(msg)
// }
// }
// } else {
// let msg = format!("parse Authorization header failed: {}", e);
// warn!(msg);
// Err(msg)
// }
// }
// }
// }
//
// fn set_user<T>(state: &T, token: &str, req: &mut Request) -> Result<(), String>
// where
// T: TokenVerify + Clone + Send + Sync + 'static,
// {
// match state.verify(token) {
// Ok(user) => {
// req.extensions_mut().insert(user);
// Ok(())
// }
// Err(e) => {
// let msg = format!("verify token failed: {:?}", e);
// warn!(msg);
// Err(msg)
// }
// }
// }
//
// #[cfg(test)]
// mod tests {
// use super::*;
// use crate::{DecodingKey, EncodingKey, User};
// use anyhow::Result;
// use axum::{body::Body, middleware::from_fn_with_state, routing::get, Router};
// use std::sync::Arc;
// use tower::ServiceExt;
//
// #[derive(Clone)]
// struct AppState(Arc<AppStateInner>);
//
// struct AppStateInner {
// ek: EncodingKey,
// dk: DecodingKey,
// }
//
// impl TokenVerify for AppState {
// type Error = ();
//
// fn verify(&self, token: &str) -> Result<User, Self::Error> {
// self.0.dk.verify(token).map_err(|_| ())
// }
// }
//
// async fn handler(_req: Request) -> impl IntoResponse {
// (StatusCode::OK, "ok")
// }
//
// #[tokio::test]
// async fn verify_token_middleware_should_work() -> Result<()> {
// let encoding_pem = include_str!("../../fixtures/encoding.pem");
// let decoding_pem = include_str!("../../fixtures/decoding.pem");
// // let ek = EncodingKey::load(encoding_pem)?;
// // let dk = DecodingKey::load(decoding_pem)?;
// let state = AppState(Arc::new(AppStateInner { ek, dk }));
//
// let user = User::new(1, "soddy", "soddygo@acme.org");
// let token = state.0.ek.sign(user)?;
//
// let app = Router::new()
// .route("/", get(handler))
// .layer(from_fn_with_state(state.clone(), verify_token::<AppState>))
// .with_state(state);
//
// // good token
// let req = Request::builder()
// .uri("/")
// .header("Authorization", format!("Bearer {}", token))
// .body(Body::empty())?;
// let res = app.clone().oneshot(req).await?;
// assert_eq!(res.status(), StatusCode::OK);
//
// // good token in query params
// let req = Request::builder()
// .uri(format!("/?token={}", token))
// .body(Body::empty())?;
// let res = app.clone().oneshot(req).await?;
// assert_eq!(res.status(), StatusCode::OK);
//
// // no token
// let req = Request::builder().uri("/").body(Body::empty())?;
// let res = app.clone().oneshot(req).await?;
// assert_eq!(res.status(), StatusCode::UNAUTHORIZED);
//
// // bad token
// let req = Request::builder()
// .uri("/")
// .header("Authorization", "Bearer bad-token")
// .body(Body::empty())?;
// let res = app.clone().oneshot(req).await?;
// assert_eq!(res.status(), StatusCode::FORBIDDEN);
//
// // bad token in query params
// let req = Request::builder()
// .uri("/?token=bad-token")
// .body(Body::empty())?;
// let res = app.oneshot(req).await?;
// assert_eq!(res.status(), StatusCode::FORBIDDEN);
//
// Ok(())
// }
// }

View File

@@ -0,0 +1,147 @@
//! HTTP request/response logging middleware
//!
//! This middleware logs all HTTP requests and responses at TRACE level.
//! Using TRACE level avoids excessive log output for frequent MCP API calls.
//!
//! To enable HTTP logging, set:
//! - `RUST_LOG=trace` (global)
//! - `RUST_LOG=mcp_proxy=trace` (module-specific)
use axum::{
extract::{MatchedPath, Request},
middleware::Next,
response::Response,
};
use tracing::trace;
/// HTTP request/response logging middleware
///
/// Logs incoming requests with details (method, uri, route, headers)
/// and outgoing responses (status, duration, content length).
///
/// # Log Level
/// Uses TRACE level because MCP API calls are very frequent.
/// Enable with `RUST_LOG=trace` or `RUST_LOG=mcp_proxy=trace`.
///
/// # Middleware Order
/// Should be placed before `opentelemetry_tracing_middleware` to log
/// request entry first, while avoiding duplication of completion logs.
pub async fn http_logging_middleware(request: Request, next: Next) -> Response {
// Extract all needed data before moving the request
let method = request.method().clone();
let uri = request.uri().clone();
let version = request.version();
// Get matched route path (extract to owned String to avoid borrow)
let route = request
.extensions()
.get::<MatchedPath>()
.map(|path| path.as_str().to_owned())
.unwrap_or_else(|| "<unknown>".to_owned());
// Get request headers of interest (extract to owned Strings to avoid borrow)
let headers = request.headers();
let content_type = headers
.get("content-type")
.and_then(|v| v.to_str().ok())
.unwrap_or("<none>")
.to_owned();
let content_length = headers
.get("content-length")
.and_then(|v| v.to_str().ok())
.unwrap_or("<none>")
.to_owned();
let user_agent = headers
.get("user-agent")
.and_then(|v| v.to_str().ok())
.unwrap_or("<none>")
.to_owned();
// Log incoming request at TRACE level (for frequent MCP API calls)
trace!(
method = %method,
uri = %uri,
route = %route,
version = ?version,
content_type = %content_type,
content_length = %content_length,
user_agent = %user_agent,
"HTTP request received"
);
let start = std::time::Instant::now();
let response = next.run(request).await;
let duration = start.elapsed();
// Get response details
let status = response.status();
let response_content_length = response
.headers()
.get("content-length")
.and_then(|v| v.to_str().ok())
.unwrap_or("<none>");
// Log response at TRACE level
trace!(
method = %method,
uri = %uri,
route = %route,
status = %status,
duration_ms = %duration.as_millis(),
response_content_length = %response_content_length,
"HTTP response sent"
);
response
}
#[cfg(test)]
mod tests {
use super::*;
use axum::{Router, body::Body, http::StatusCode, routing::get};
use tower::ServiceExt;
#[tokio::test]
async fn test_http_logging_middleware() {
// Create a test handler
async fn test_handler() -> &'static str {
"OK"
}
let app = Router::new()
.route("/test", get(test_handler))
.layer(axum::middleware::from_fn(http_logging_middleware));
let response = app
.oneshot(Request::builder().uri("/test").body(Body::empty()).unwrap())
.await
.unwrap();
assert_eq!(response.status(), StatusCode::OK);
}
#[tokio::test]
async fn test_http_logging_with_headers() {
async fn test_handler() -> &'static str {
"OK"
}
let app = Router::new()
.route("/test", get(test_handler))
.layer(axum::middleware::from_fn(http_logging_middleware));
let response = app
.oneshot(
Request::builder()
.uri("/test")
.header("content-type", "application/json")
.header("user-agent", "test-agent")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::OK);
}
}

View File

@@ -0,0 +1,55 @@
use std::str::FromStr;
use axum::{extract::Request, http::StatusCode, middleware::Next, response::Response};
use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64};
use tracing::debug;
use crate::model::{McpConfig, McpRouterPath, McpType};
/// 提取mcp的json配置,从请求的header上,可能没有,也可能有
pub(crate) async fn mcp_json_config_extract(
mut req: Request,
next: Next,
) -> Result<Response, StatusCode> {
let path = req.uri().path().to_string();
debug!("Request path: {path}");
//检查请求路径,是否 /mcp 开头
let check_mcp_path = McpRouterPath::check_mcp_path(&path);
if check_mcp_path {
//请求路径,可能是: /mcp/{mcp_id}/sse,或者 /mcp/{mcp_id}/message
let mcp_router_path = McpRouterPath::from_url(&path);
if let Some(mcp_router_path) = mcp_router_path {
let mcp_id = mcp_router_path.mcp_id.clone();
// 解析header中的 x-mcp-json 字段,这个对应的是 mcp 的json配置
// 现在这个字段是base64编码过的需要先解码
let mcp_json_config = req
.headers()
.get("x-mcp-json")
.and_then(|value| value.to_str().ok())
.and_then(|encoded| {
// 将 base64 编码的值解码为原始 JSON 字符串
let decoded = BASE64
.decode(encoded)
.ok()
.and_then(|bytes| String::from_utf8(bytes).ok());
debug!("Parsed MCP configuration, x-mcp-json={:?}", &decoded);
decoded
});
// 解析header中的 x-mcp-type 字段,这个对应的是 mcp 的类型
let mcp_type = req
.headers()
.get("x-mcp-type")
.and_then(|value| value.to_str().ok())
.and_then(|s| McpType::from_str(s).ok())
.unwrap_or_default();
let mcp_protocol = mcp_router_path.mcp_protocol.clone();
let mcp_config = McpConfig::new(mcp_id, mcp_json_config, mcp_type, mcp_protocol);
req.extensions_mut().insert(mcp_config);
}
}
Ok(next.run(req).await)
}

View File

@@ -0,0 +1,67 @@
use std::task::{Context, Poll};
use axum::extract::Request;
use log::debug;
use tower::{Layer, Service};
use crate::{
get_proxy_manager,
model::{AppState, McpRouterPath},
};
#[derive(Clone)]
pub struct MySseRouterLayer {
state: AppState,
}
#[allow(dead_code)]
#[derive(Clone)]
pub struct MySseRouterService<S> {
inner: S,
state: AppState,
}
impl MySseRouterLayer {
pub fn new(state: AppState) -> Self {
Self { state }
}
}
impl<S> Layer<S> for MySseRouterLayer {
type Service = MySseRouterService<S>;
fn layer(&self, inner: S) -> Self::Service {
MySseRouterService {
inner,
state: self.state.clone(),
}
}
}
impl<S, B> Service<Request<B>> for MySseRouterService<S>
where
S: Service<Request<B>>,
{
type Response = S::Response;
type Error = S::Error;
type Future = S::Future;
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
self.inner.poll_ready(cx)
}
fn call(&mut self, req: Request<B>) -> Self::Future {
let path = req.uri().path().to_string();
// ===== 复用现有逻辑:检查是否为 MCP 请求 =====
let check_mcp_path = McpRouterPath::check_mcp_path(&path);
if check_mcp_path && let Some(mcp_router_path) = McpRouterPath::from_url(&path) {
let mcp_id = mcp_router_path.mcp_id.clone();
// ===== 更新最后访问时间 =====
debug!("Update last access time, request access MCP ID: {}", mcp_id);
get_proxy_manager().update_last_accessed(&mcp_id);
}
self.inner.call(req)
}
}

View File

@@ -0,0 +1,47 @@
mod auth;
mod http_logging;
mod mcp_router_json;
mod mcp_update_latest_layer;
mod opentelemetry_middleware;
mod server_time;
use crate::model::AppState;
use axum::Router;
use axum::middleware::from_fn;
use http_logging::http_logging_middleware;
use mcp_router_json::mcp_json_config_extract;
use opentelemetry_middleware::opentelemetry_tracing_middleware;
use server_time::ServerTimeLayer;
use tower::ServiceBuilder;
use tower_http::compression::CompressionLayer;
pub use mcp_update_latest_layer::MySseRouterLayer;
pub use opentelemetry_middleware::extract_trace_id;
// pub use auth::{extract_user, verify_token};
// pub trait TokenVerify {
// type Error: fmt::Debug;
// fn verify(&self, token: &str) -> Result<User, Self::Error>;
// }
const REQUEST_ID_HEADER: &str = "x-request-id";
const SERVER_TIME_HEADER: &str = "x-server-time";
pub fn set_layer(app: Router, state: AppState) -> Router {
app.layer(
ServiceBuilder::new()
// HTTP 请求/响应日志中间件 (TRACE level, 用于调试频繁的 MCP API 调用)
.layer(from_fn(http_logging_middleware))
// OpenTelemetry 追踪中间件 - 自动生成 trace_id 和 span
.layer(from_fn(opentelemetry_tracing_middleware))
// MCP 配置提取中间件
.layer(from_fn(mcp_json_config_extract))
// HTTP 压缩
.layer(CompressionLayer::new().gzip(true).br(true).deflate(true))
// 服务器时间响应头
.layer(ServerTimeLayer)
// SSE 路由层
.layer(MySseRouterLayer::new(state.clone())),
)
}

View File

@@ -0,0 +1,199 @@
use axum::{
extract::{MatchedPath, Request},
http::{HeaderMap, HeaderValue},
middleware::Next,
response::Response,
};
use opentelemetry::{Context, trace::TraceContextExt};
use std::time::Instant;
use tracing::{Instrument, debug_span};
use tracing_opentelemetry::OpenTelemetrySpanExt;
use super::{REQUEST_ID_HEADER, SERVER_TIME_HEADER};
/// OpenTelemetry 追踪中间件
///
/// 功能:
/// 1. 自动创建 OpenTelemetry span 和 trace
/// 2. 在响应头中添加 x-request-id (trace_id)
/// 3. 在响应头中添加 x-server-time (请求处理时间)
/// 4. 记录 HTTP 请求的语义化属性
pub async fn opentelemetry_tracing_middleware(request: Request, next: Next) -> Response {
let start_time = Instant::now();
// 提取请求信息
let method = request.method().to_string();
let uri = request.uri().to_string();
let route = request
.extensions()
.get::<MatchedPath>()
.map(|matched_path| matched_path.as_str().to_owned())
.unwrap_or_else(|| request.uri().path().to_owned());
let version = format!("{:?}", request.version());
let user_agent = request
.headers()
.get("user-agent")
.and_then(|h| h.to_str().ok())
.unwrap_or("unknown");
// 创建 OpenTelemetry span
let span = debug_span!(
"http_request",
otel.name = format!("{} {}", method, route).as_str(),
otel.kind = "server",
http.method = method.as_str(),
http.url = uri.as_str(),
http.route = route.as_str(),
http.scheme = "http",
http.version = version.as_str(),
http.user_agent = user_agent,
);
// 设置 OpenTelemetry 属性
let otel_cx = Context::current();
if let Err(error) = span.set_parent(otel_cx) {
tracing::warn!(target: "telemetry", %method, %uri, ?error, "failed to attach OpenTelemetry parent context");
}
// 获取 trace_id
let trace_id = span.context().span().span_context().trace_id().to_string();
// 如果 trace_id 全为0生成一个随机的 trace_id
let trace_id = if trace_id == "00000000000000000000000000000000" {
use uuid::Uuid;
Uuid::new_v4().simple().to_string()
} else {
trace_id
};
async move {
// 执行请求处理
let mut response = next.run(request).await;
// 计算处理时间
let duration = start_time.elapsed();
let duration_micros = duration.as_micros();
// 记录响应状态码
let status_code = response.status().as_u16();
// 添加响应头
let headers = response.headers_mut();
// 添加 trace_id 到响应头
if let Ok(trace_header) = HeaderValue::from_str(&trace_id) {
headers.insert(REQUEST_ID_HEADER, trace_header);
}
// 添加服务器处理时间到响应头 (微秒)
if let Ok(time_header) = HeaderValue::from_str(&duration_micros.to_string()) {
headers.insert(SERVER_TIME_HEADER, time_header);
}
// 记录请求完成日志(改为 debug 级别,减少日志量)
tracing::debug!(
method = %method,
uri = %uri,
status = %status_code,
duration_micros = %duration_micros,
trace_id = %trace_id,
"HTTP request completed"
);
response
}
.instrument(span)
.await
}
/// 从当前 OpenTelemetry 上下文中提取 trace_id
///
/// 这个函数可以在任何地方调用来获取当前请求的 trace_id
pub fn extract_trace_id() -> String {
let current_span = tracing::Span::current();
let context = current_span.context();
let span_ref = context.span();
let span_context = span_ref.span_context();
let trace_id = if span_context.is_valid() {
span_context.trace_id().to_string()
} else {
"00000000000000000000000000000000".to_string()
};
// 如果 trace_id 全为0生成一个随机的 trace_id
if trace_id == "00000000000000000000000000000000" {
use uuid::Uuid;
Uuid::new_v4().simple().to_string()
} else {
trace_id
}
}
/// 从请求头中提取现有的 trace_id如果有的话
///
/// 支持标准的 OpenTelemetry 传播头:
/// - traceparent (W3C Trace Context)
/// - x-trace-id (自定义)
#[allow(dead_code)]
pub fn extract_trace_from_headers(headers: &HeaderMap) -> Option<String> {
// 尝试从 W3C Trace Context 中提取
if let Some(traceparent) = headers.get("traceparent")
&& let Ok(traceparent_str) = traceparent.to_str()
{
// traceparent 格式: 00-{trace_id}-{span_id}-{flags}
let parts: Vec<&str> = traceparent_str.split('-').collect();
if parts.len() >= 2 {
return Some(parts[1].to_string());
}
}
// 尝试从自定义头中提取
if let Some(trace_id) = headers.get("x-trace-id")
&& let Ok(trace_id_str) = trace_id.to_str()
{
return Some(trace_id_str.to_string());
}
None
}
#[cfg(test)]
mod tests {
use super::*;
use axum::http::{HeaderMap, HeaderValue};
#[test]
fn test_extract_trace_from_headers_traceparent() {
let mut headers = HeaderMap::new();
headers.insert(
"traceparent",
HeaderValue::from_static("00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01"),
);
let trace_id = extract_trace_from_headers(&headers);
assert_eq!(
trace_id,
Some("4bf92f3577b34da6a3ce929d0e0e4736".to_string())
);
}
#[test]
fn test_extract_trace_from_headers_custom() {
let mut headers = HeaderMap::new();
headers.insert(
"x-trace-id",
HeaderValue::from_static("custom-trace-id-123"),
);
let trace_id = extract_trace_from_headers(&headers);
assert_eq!(trace_id, Some("custom-trace-id-123".to_string()));
}
#[test]
fn test_extract_trace_from_headers_none() {
let headers = HeaderMap::new();
let trace_id = extract_trace_from_headers(&headers);
assert_eq!(trace_id, None);
}
}

View File

@@ -0,0 +1,65 @@
use super::{REQUEST_ID_HEADER, SERVER_TIME_HEADER};
use axum::{extract::Request, response::Response};
use std::{
future::Future,
pin::Pin,
task::{Context, Poll},
};
use tokio::time::Instant;
use tower::{Layer, Service};
use tracing::warn;
#[derive(Clone)]
pub struct ServerTimeLayer;
impl<S> Layer<S> for ServerTimeLayer {
type Service = ServerTimeMiddleware<S>;
fn layer(&self, inner: S) -> Self::Service {
ServerTimeMiddleware { inner }
}
}
#[derive(Clone)]
pub struct ServerTimeMiddleware<S> {
inner: S,
}
impl<S> Service<Request> for ServerTimeMiddleware<S>
where
S: Service<Request, Response = Response> + Send + 'static,
S::Future: Send + 'static,
{
type Response = S::Response;
type Error = S::Error;
// `BoxFuture` is a type alias for `Pin<Box<dyn Future + Send + 'a>>`
type Future =
Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>> + Send + 'static>>;
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
self.inner.poll_ready(cx)
}
fn call(&mut self, request: Request) -> Self::Future {
let start = Instant::now();
let future = self.inner.call(request);
Box::pin(async move {
let mut res: Response = future.await?;
let elapsed = format!("{}us", start.elapsed().as_micros());
match elapsed.parse() {
Ok(v) => {
res.headers_mut().insert(SERVER_TIME_HEADER, v);
}
Err(e) => {
warn!(
"Parse elapsed time failed: {} for request {:?}",
e,
res.headers().get(REQUEST_ID_HEADER)
);
}
}
Ok(res)
})
}
}

View File

@@ -0,0 +1,18 @@
pub mod handlers;
mod mcp_dynamic_router_service;
mod middlewares;
pub mod protocol_detector;
mod router_layer;
mod task;
pub mod telemetry;
pub use handlers::{get_health, get_ready};
pub use middlewares::set_layer;
pub use protocol_detector::{detect_mcp_protocol, detect_mcp_protocol_with_headers};
pub use router_layer::get_router;
pub use task::{mcp_start_task, schedule_check_mcp_live, start_schedule_task};
pub use telemetry::{
create_telemetry_layer, init_tracer_provider, log_service_info, shutdown_telemetry,
};

View File

@@ -0,0 +1,87 @@
//! Protocol Detection (Re-export Layer)
//!
//! This module re-exports protocol detection functions and provides
//! a convenient combined detection function.
//!
//! Detection logic: If SSE detected → Sse, else → Stream (default fallback)
// Re-export the detection functions
pub use mcp_sse_proxy::is_sse_with_headers;
use crate::model::McpProtocol;
use anyhow::Result;
use log::info;
use std::collections::HashMap;
/// Automatically detect the MCP service protocol type
///
/// Convenience wrapper around [`detect_mcp_protocol_with_headers`] that passes no
/// custom headers.
pub async fn detect_mcp_protocol(url: &str) -> Result<McpProtocol> {
detect_mcp_protocol_with_headers(url, None).await
}
/// Automatically detect the MCP service protocol type, with optional custom headers
///
/// Detection logic:
/// 1. First try to detect SSE protocol (GET /sse returns text/event-stream)
/// 2. If not SSE, default to Streamable HTTP (modern MCP standard)
///
/// # Arguments
///
/// * `url` - The URL to detect
/// * `headers` - Optional custom headers to include in the detection request
///
/// # Returns
///
/// Returns the detected protocol type (Sse or Stream)
pub async fn detect_mcp_protocol_with_headers(
url: &str,
headers: Option<&HashMap<String, String>>,
) -> Result<McpProtocol> {
info!(
"Start automatically detecting MCP service protocol: {}",
url
);
// Try SSE first
if is_sse_with_headers(url, headers).await {
info!("SSE protocol detected: {}", url);
return Ok(McpProtocol::Sse);
}
// Default to Streamable HTTP (modern MCP standard)
info!("Default uses Streamable HTTP protocol: {}", url);
Ok(McpProtocol::Stream)
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_detect_invalid_url() {
// Invalid URL should default to Stream
let result = detect_mcp_protocol("not-a-url").await;
assert!(result.is_ok());
assert_eq!(result.unwrap(), McpProtocol::Stream);
}
#[tokio::test]
async fn test_detect_nonexistent_server() {
// Non-existent server should default to Stream
let result = detect_mcp_protocol("http://localhost:99999/mcp").await;
assert!(result.is_ok());
assert_eq!(result.unwrap(), McpProtocol::Stream);
}
#[tokio::test]
async fn test_detect_with_headers_nonexistent_server() {
let mut headers = HashMap::new();
headers.insert("Authorization".to_string(), "Bearer test-token".to_string());
let result =
detect_mcp_protocol_with_headers("http://localhost:99999/mcp", Some(&headers)).await;
assert!(result.is_ok());
assert_eq!(result.unwrap(), McpProtocol::Stream);
}
}

View File

@@ -0,0 +1,82 @@
use axum::{
Router,
extract::DefaultBodyLimit,
routing::{delete, get, post},
};
use http::Method;
use tower_http::cors::{self, CorsLayer};
use crate::{
AppError, AppState, DynamicRouterService,
model::{GLOBAL_SSE_MCP_ROUTES_PREFIX, GLOBAL_STREAM_MCP_ROUTES_PREFIX, McpProtocol},
server::handlers::check_mcp_is_status_handler,
};
use super::{
get_health, get_ready,
handlers::{
add_route_handler, check_mcp_status_handler_sse, check_mcp_status_handler_stream,
delete_route_handler, run_code_handler,
},
set_layer,
};
/// 获取路由
pub async fn get_router(state: AppState) -> Result<Router, AppError> {
let health = Router::new()
.route("/health", get(get_health))
.route("/ready", get(get_ready));
let cors = CorsLayer::new()
// allow `GET` and `POST` when accessing the resource
.allow_methods([
Method::GET,
Method::POST,
Method::PATCH,
Method::DELETE,
Method::PUT,
])
.allow_origin(cors::Any)
.allow_headers(cors::Any);
let api = Router::new()
// .layer(from_fn_with_state(state.clone(), verify_token::<AppState>))
//mcp sse 协议路由
.route_service(
&format!("{GLOBAL_SSE_MCP_ROUTES_PREFIX}/proxy/{{*path}}"),
DynamicRouterService(McpProtocol::Sse),
)
.route(
&format!("{GLOBAL_SSE_MCP_ROUTES_PREFIX}/add"),
post(add_route_handler),
)
.route("/mcp/config/delete/{mcp_id}", delete(delete_route_handler))
.route(
"/mcp/check/status/{mcp_id}",
get(check_mcp_is_status_handler),
)
.route(
&format!("{GLOBAL_SSE_MCP_ROUTES_PREFIX}/check_status"),
post(check_mcp_status_handler_sse),
)
//mcp stream 协议路由
.route_service(
&format!("{GLOBAL_STREAM_MCP_ROUTES_PREFIX}/proxy/{{*path}}"),
DynamicRouterService(McpProtocol::Stream),
)
.route(
&format!("{GLOBAL_STREAM_MCP_ROUTES_PREFIX}/check_status"),
post(check_mcp_status_handler_stream),
)
.route("/api/run_code_with_log", post(run_code_handler))
.layer(DefaultBodyLimit::max(20 * 1024 * 1024))
.layer(cors);
// 创建基本路由
let app: Router<AppState> = Router::new().merge(health).merge(api);
// 添加状态
let app = app.with_state(state.clone());
let router = set_layer(app, state);
Ok(router)
}

View File

@@ -0,0 +1,592 @@
//! MCP Service Start Task
//!
//! This module handles starting MCP services using the Builder APIs from
//! mcp-sse-proxy and mcp-streamable-proxy libraries.
//!
//! The refactored implementation removes direct rmcp dependency by delegating
//! protocol-specific logic to the proxy libraries.
use crate::{
AppError, DynamicRouterService, get_proxy_manager,
model::GLOBAL_RESTART_TRACKER,
model::{
CheckMcpStatusResponseStatus, McpConfig, McpProtocol, McpProtocolPath, McpRouterPath,
McpServerCommandConfig, McpServerConfig, McpServiceStatus, McpType,
},
proxy::{
McpHandler, SseBackendConfig, SseServerBuilder, StreamBackendConfig, StreamServerBuilder,
},
};
use anyhow::{Context, Result};
use log::{debug, info};
use std::collections::HashMap;
/// Start an MCP service based on configuration
///
/// This function creates and configures an MCP proxy service based on the
/// provided configuration. It supports both SSE and Streamable HTTP client
/// protocols, with automatic backend protocol detection for URL-based services.
pub async fn mcp_start_task(
mcp_config: McpConfig,
) -> Result<(axum::Router, tokio_util::sync::CancellationToken)> {
let mcp_id = mcp_config.mcp_id.clone();
let client_protocol = mcp_config.client_protocol.clone();
// Create router path based on client protocol (determines exposed API interface)
let mcp_router_path: McpRouterPath = McpRouterPath::new(mcp_id, client_protocol)
.map_err(|e| AppError::mcp_server_error(e.to_string()))?;
let mcp_json_config = mcp_config
.mcp_json_config
.clone()
.expect("mcp_json_config is required");
let mcp_server_config = McpServerConfig::try_from(mcp_json_config)?;
// Use the integrated method to create the server
integrate_server_with_axum(
mcp_server_config.clone(),
mcp_router_path.clone(),
mcp_config.clone(),
)
.await
}
/// Integrate MCP server with axum router
///
/// This function:
/// 1. Determines backend protocol (stdio, SSE, or Streamable HTTP)
/// 2. Creates the appropriate server using Builder APIs
/// 3. Registers the handler with ProxyManager
/// 4. Sets up dynamic routing
pub async fn integrate_server_with_axum(
mcp_config: McpServerConfig,
mcp_router_path: McpRouterPath,
full_mcp_config: McpConfig,
) -> Result<(axum::Router, tokio_util::sync::CancellationToken)> {
let mcp_type = full_mcp_config.mcp_type.clone();
let base_path = mcp_router_path.base_path.clone();
let mcp_id = mcp_router_path.mcp_id.clone();
// Determine backend protocol from configuration
let backend_protocol = match &mcp_config {
// Command-line config: use stdio protocol
McpServerConfig::Command(_) => McpProtocol::Stdio,
// URL config: parse type field or auto-detect
McpServerConfig::Url(url_config) => {
// Merge headers + auth_token for protocol detection
let mut detection_headers = normalize_headers(&url_config.headers).unwrap_or_default();
if let Some(auth_token) = &url_config.auth_token {
let value = if auth_token.starts_with("Bearer ") {
auth_token.clone()
} else {
format!("Bearer {}", auth_token)
};
detection_headers.insert("Authorization".to_string(), value);
}
let detection_headers_ref = if detection_headers.is_empty() {
None
} else {
Some(&detection_headers)
};
// Check type field first
if let Some(type_str) = &url_config.r#type {
match type_str.parse::<McpProtocol>() {
Ok(protocol) => {
debug!(
"Using configured protocol type: {} -> {:?}",
type_str, protocol
);
protocol
}
Err(_) => {
// If parsing fails, auto-detect
debug!("Protocol type '{}' unrecognized, auto-detecting", type_str);
let detected_protocol = crate::server::detect_mcp_protocol_with_headers(
url_config.get_url(),
detection_headers_ref,
)
.await
.map_err(|e| {
anyhow::anyhow!(
"Protocol type '{}' unrecognized and auto-detection failed: {}",
type_str,
e
)
})?;
debug!(
"Auto-detected protocol: {:?} (original config: '{}')",
detected_protocol, type_str
);
detected_protocol
}
}
} else {
// No type field, auto-detect
debug!("No type field specified, auto-detecting protocol");
crate::server::detect_mcp_protocol_with_headers(
url_config.get_url(),
detection_headers_ref,
)
.await
.map_err(|e| anyhow::anyhow!("Auto-detection failed: {}", e))?
}
}
};
debug!(
"MCP ID: {}, client protocol: {:?}, backend protocol: {:?}",
mcp_id, mcp_router_path.mcp_protocol, backend_protocol
);
// Create server based on client protocol using Builder APIs
let (router, ct, handler) = match mcp_router_path.mcp_protocol.clone() {
// ================ Client uses SSE protocol ================
McpProtocol::Sse => {
let sse_path = match &mcp_router_path.mcp_protocol_path {
McpProtocolPath::SsePath(sse_path) => sse_path,
_ => unreachable!(),
};
// Build backend config for SSE
let backend_config = if matches!(backend_protocol, McpProtocol::Stream) {
// Streamable HTTP backend: connect via mcp-streamable-proxy (rmcp 1.4.0),
// then pass as BackendBridge to decouple mcp-sse-proxy from mcp-streamable-proxy
let bridge = connect_stream_backend(&mcp_config, &mcp_id).await?;
SseBackendConfig::BackendBridge(bridge)
} else {
build_sse_backend_config(&mcp_config, backend_protocol)?
};
debug!(
"Creating SSE server, sse_path={}, post_path={}",
sse_path.sse_path, sse_path.message_path
);
// 对于 OneShot 服务,使用更短的 keep_alive 间隔5秒来保持后端活跃
// 防止后端进程因空闲超时而退出
let keep_alive_secs = if matches!(mcp_type, McpType::OneShot) {
5
} else {
15
};
// 对于 OneShot 服务,禁用 stateful 模式以加快响应速度
// stateful=false 会跳过 MCP 初始化步骤,直接处理请求
let stateful = !matches!(mcp_type, McpType::OneShot);
let (router, ct, handler) = SseServerBuilder::new(backend_config)
.mcp_id(mcp_id.clone())
.sse_path(sse_path.sse_path.clone())
.post_path(sse_path.message_path.clone())
.keep_alive(keep_alive_secs)
.stateful(stateful)
.build()
.await
.with_context(|| {
format!(
"SSE server build failed - MCP ID: {}, type: {:?}",
mcp_id, mcp_type
)
})?;
info!(
"SSE server started - MCP ID: {}, type: {:?}",
mcp_router_path.mcp_id, mcp_type
);
(router, ct, McpHandler::Sse(Box::new(handler)))
}
// ================ Client uses Streamable HTTP protocol ================
McpProtocol::Stream => {
// Build backend config for Stream
let backend_config = build_stream_backend_config(&mcp_config, backend_protocol)?;
let (router, ct, handler) = StreamServerBuilder::new(backend_config)
.mcp_id(mcp_id.clone())
.stateful(false)
.build()
.await
.with_context(|| {
format!(
"Stream server build failed - MCP ID: {}, type: {:?}",
mcp_id, mcp_type
)
})?;
info!(
"Streamable HTTP server started - MCP ID: {}, type: {:?}",
mcp_router_path.mcp_id, mcp_type
);
(router, ct, McpHandler::Stream(Box::new(handler)))
}
// Client stdio protocol is not supported in server mode
McpProtocol::Stdio => {
return Err(anyhow::anyhow!(
"Client protocol cannot be Stdio. McpRouterPath::new does not support creating Stdio protocol router paths"
));
}
};
// Clone cancellation token for monitoring
let ct_clone = ct.clone();
let mcp_id_clone = mcp_id.clone();
// Store MCP service status with full mcp_config for auto-restart
let mcp_service_status = McpServiceStatus::new(
mcp_id_clone.clone(),
mcp_type.clone(),
mcp_router_path.clone(),
ct_clone.clone(),
CheckMcpStatusResponseStatus::Ready,
)
.with_mcp_config(full_mcp_config.clone());
// Add MCP service status and proxy handler to global manager
let proxy_manager = get_proxy_manager();
proxy_manager.add_mcp_service_status_and_proxy(mcp_service_status, Some(handler));
// ===== 新增:注册配置到缓存 =====
proxy_manager
.register_mcp_config(&mcp_id, full_mcp_config.clone())
.await;
// Add base path fallback handler for SSE protocol
let router = if matches!(mcp_router_path.mcp_protocol, McpProtocol::Sse) {
let modified_router = router.fallback(base_path_fallback_handler);
info!("SSE base path handler added, base_path: {}", base_path);
modified_router
} else {
router
};
// Register route to global route table
info!(
"Registering route: base_path={}, mcp_id={}",
base_path, mcp_id
);
info!(
"SSE path config: sse_path={}, post_path={}",
match &mcp_router_path.mcp_protocol_path {
McpProtocolPath::SsePath(sse_path) => &sse_path.sse_path,
_ => "N/A",
},
match &mcp_router_path.mcp_protocol_path {
McpProtocolPath::SsePath(sse_path) => &sse_path.message_path,
_ => "N/A",
}
);
DynamicRouterService::register_route(&base_path, router.clone());
info!("Route registration complete: base_path={}", base_path);
// 记录重启时间戳(仅在服务成功启动后)
GLOBAL_RESTART_TRACKER.record_restart(&mcp_id);
Ok((router, ct))
}
/// Connect to a Streamable HTTP backend and return a BackendBridge
///
/// This lives in mcp-proxy (not mcp-sse-proxy) because it uses mcp-streamable-proxy types.
/// The returned `Arc<dyn BackendBridge>` is protocol-agnostic, allowing mcp-sse-proxy
/// to use it without depending on mcp-streamable-proxy.
async fn connect_stream_backend(
mcp_config: &McpServerConfig,
mcp_id: &str,
) -> Result<std::sync::Arc<dyn mcp_common::BackendBridge>> {
use crate::proxy::{StreamClientConnection, StreamProxyHandler};
let url_config = match mcp_config {
McpServerConfig::Url(url_config) => url_config,
_ => {
return Err(anyhow::anyhow!(
"Stream backend requires URL-based config"
))
}
};
let url = url_config.get_url();
info!(
"Connecting to Streamable HTTP backend (SSE frontend) \
- MCP ID: {}, URL: {}",
mcp_id, url
);
let mut config = mcp_common::McpClientConfig::new(url.to_string());
let normalized = normalize_headers(&url_config.headers);
if let Some(ref headers) = normalized {
for (k, v) in headers {
config = config.with_header(k, v);
}
}
// auth_token 合并到 Authorization header与 build_sse_backend_config 逻辑一致)
if let Some(ref auth_token) = url_config.auth_token {
let value = if auth_token.starts_with("Bearer ") {
auth_token.clone()
} else {
format!("Bearer {}", auth_token)
};
config = config.with_header("Authorization", value);
}
let conn = StreamClientConnection::connect(config).await?;
let proxy_handler =
StreamProxyHandler::with_mcp_id(conn.into_running_service(), mcp_id.to_string());
Ok(std::sync::Arc::new(proxy_handler))
}
/// Build SSE backend configuration from MCP server config
fn build_sse_backend_config(
mcp_config: &McpServerConfig,
backend_protocol: McpProtocol,
) -> Result<SseBackendConfig> {
match mcp_config {
McpServerConfig::Command(cmd_config) => {
log_command_details(cmd_config);
Ok(SseBackendConfig::Stdio {
command: cmd_config.command.clone(),
args: cmd_config.args.clone(),
env: cmd_config.env.clone(),
})
}
McpServerConfig::Url(url_config) => match backend_protocol {
McpProtocol::Stdio => Err(anyhow::anyhow!(
"URL-based MCP service cannot use Stdio protocol"
)),
McpProtocol::Sse => {
info!("Connecting to SSE backend: {}", url_config.get_url());
Ok(SseBackendConfig::SseUrl {
url: url_config.get_url().to_string(),
headers: normalize_headers(&url_config.headers),
})
}
McpProtocol::Stream => Err(anyhow::anyhow!(
"Stream backend should be handled via connect_stream_backend(), \
not build_sse_backend_config()"
))
},
}
}
/// Build Stream backend configuration from MCP server config
fn build_stream_backend_config(
mcp_config: &McpServerConfig,
backend_protocol: McpProtocol,
) -> Result<StreamBackendConfig> {
match mcp_config {
McpServerConfig::Command(cmd_config) => {
log_command_details(cmd_config);
Ok(StreamBackendConfig::Stdio {
command: cmd_config.command.clone(),
args: cmd_config.args.clone(),
env: cmd_config.env.clone(),
})
}
McpServerConfig::Url(url_config) => {
match backend_protocol {
McpProtocol::Stdio => Err(anyhow::anyhow!(
"URL-based MCP service cannot use Stdio protocol"
)),
McpProtocol::Sse => {
// Note: StreamServerBuilder currently only supports Streamable HTTP URL backend
// SSE backend with Stream frontend would require protocol conversion
// For now, we return an error for this combination
Err(anyhow::anyhow!(
"SSE backend with Streamable HTTP frontend is not yet supported. \
Please use SSE frontend or configure a Streamable HTTP backend."
))
}
McpProtocol::Stream => {
info!(
"Connecting to Streamable HTTP backend: {}",
url_config.get_url()
);
Ok(StreamBackendConfig::Url {
url: url_config.get_url().to_string(),
headers: normalize_headers(&url_config.headers),
})
}
}
}
}
}
/// 规范化 headers确保 Authorization header 有 "Bearer " 前缀
///
/// 与 client 模式 (`convert.rs:build_mcp_config`) 行为一致,
/// 对没有 "Bearer " 前缀的 Authorization header 自动添加前缀。
fn normalize_headers(headers: &Option<HashMap<String, String>>) -> Option<HashMap<String, String>> {
headers.as_ref().map(|h| {
h.iter()
.map(|(k, v)| {
if k.eq_ignore_ascii_case("Authorization") && !v.starts_with("Bearer ") {
(k.clone(), format!("Bearer {}", v))
} else {
(k.clone(), v.clone())
}
})
.collect()
})
}
/// Log command execution details for debugging
fn log_command_details(mcp_config: &McpServerCommandConfig) {
let args_str = mcp_config
.args
.as_ref()
.map_or(String::new(), |args| args.join(" "));
info!("Executing command: {} {}", mcp_config.command, args_str);
// 只输出 env 变量的 key 列表,避免泄露敏感 value
if let Some(env_vars) = &mcp_config.env {
let keys: Vec<&String> = env_vars.keys().collect();
if !keys.is_empty() {
debug!("Config env keys: {:?}", keys);
}
}
// 输出进程级关键环境变量PATH 摘要 + 镜像变量)
debug!(
"Process PATH: {}",
mcp_common::diagnostic::format_path_summary(3)
);
for (key, val) in mcp_common::diagnostic::collect_mirror_env_vars() {
debug!("Process env: {}={}", key, val);
}
}
/// Base path fallback handler - supports direct access to base path with automatic redirection
#[axum::debug_handler]
async fn base_path_fallback_handler(
method: axum::http::Method,
uri: axum::http::Uri,
headers: axum::http::HeaderMap,
) -> impl axum::response::IntoResponse {
let path = uri.path();
info!("Base path handler: {} {}", method, path);
// Determine if SSE or Stream protocol
if path.contains("/sse/proxy/") {
// SSE protocol handling
match method {
axum::http::Method::GET => {
// Extract MCP ID from path
let mcp_id = path.split("/sse/proxy/").nth(1);
if let Some(mcp_id) = mcp_id {
// Check if MCP service exists
let proxy_manager = get_proxy_manager();
if proxy_manager.get_mcp_service_status(mcp_id).is_none() {
// MCP service not found
(
axum::http::StatusCode::NOT_FOUND,
[("Content-Type", "text/plain".to_string())],
format!("MCP service '{}' not found", mcp_id).to_string(),
)
} else {
// MCP service exists, check Accept header
let accept_header = headers.get("accept");
if let Some(accept) = accept_header {
let accept_str = accept.to_str().unwrap_or("");
if accept_str.contains("text/event-stream") {
// Correct Accept header, redirect to /sse
let redirect_uri = format!("{}/sse", path);
info!("SSE redirect to: {}", redirect_uri);
(
axum::http::StatusCode::FOUND,
[("Location", redirect_uri.to_string())],
"Redirecting to SSE endpoint".to_string(),
)
} else {
// Incorrect Accept header
(
axum::http::StatusCode::BAD_REQUEST,
[("Content-Type", "text/plain".to_string())],
"SSE error: Invalid Accept header, expected 'text/event-stream'".to_string(),
)
}
} else {
// No Accept header
(
axum::http::StatusCode::BAD_REQUEST,
[("Content-Type", "text/plain".to_string())],
"SSE error: Missing Accept header, expected 'text/event-stream'"
.to_string(),
)
}
}
} else {
// Cannot extract MCP ID from path
(
axum::http::StatusCode::BAD_REQUEST,
[("Content-Type", "text/plain".to_string())],
"SSE error: Invalid SSE path".to_string(),
)
}
}
axum::http::Method::POST => {
// POST request redirect to /message
let redirect_uri = format!("{}/message", path);
info!("SSE redirect to: {}", redirect_uri);
(
axum::http::StatusCode::FOUND,
[("Location", redirect_uri.to_string())],
"Redirecting to message endpoint".to_string(),
)
}
_ => {
// Other methods return 405 Method Not Allowed
(
axum::http::StatusCode::METHOD_NOT_ALLOWED,
[("Allow", "GET, POST".to_string())],
"Only GET and POST methods are allowed".to_string(),
)
}
}
} else if path.contains("/stream/proxy/") {
// Stream protocol handling - return success directly without redirect
match method {
axum::http::Method::GET => {
// GET request returns server info
(
axum::http::StatusCode::OK,
[("Content-Type", "application/json".to_string())],
r#"{"jsonrpc":"2.0","result":{"info":"Streamable MCP Server","version":"1.0"}}"#.to_string(),
)
}
axum::http::Method::POST => {
// POST request returns success, let StreamableHttpService handle
(
axum::http::StatusCode::OK,
[("Content-Type", "application/json".to_string())],
r#"{"jsonrpc":"2.0","result":{"message":"Stream request received","protocol":"streamable-http"}}"#.to_string(),
)
}
_ => {
// Other methods return 405 Method Not Allowed
(
axum::http::StatusCode::METHOD_NOT_ALLOWED,
[("Allow", "GET, POST".to_string())],
"Only GET and POST methods are allowed".to_string(),
)
}
}
} else {
// Unknown protocol
(
axum::http::StatusCode::BAD_REQUEST,
[("Content-Type", "text/plain".to_string())],
"Unknown protocol or path".to_string(),
)
}
}

View File

@@ -0,0 +1,7 @@
mod mcp_start_task;
mod schedule_check_mcp_live;
mod schedule_task;
pub use mcp_start_task::{integrate_server_with_axum, mcp_start_task};
pub use schedule_check_mcp_live::schedule_check_mcp_live;
pub use schedule_task::start_schedule_task;

View File

@@ -0,0 +1,195 @@
use crate::get_proxy_manager;
use crate::model::{CheckMcpStatusResponseStatus, GLOBAL_RESTART_TRACKER, McpType};
use crate::server::task::mcp_start_task::mcp_start_task;
use tokio::time::Duration;
use tracing::{error, info, warn};
// OneShot 服务超时时间5分钟无活动则清理
const ONESHOT_TIMEOUT: Duration = Duration::from_secs(5 * 60);
// 连续健康检查失败阈值:连续失败 3 次才触发重启
const MAX_PROBE_FAILURES: u32 = 3;
/// 定期检查全局动态 router 里的 MCP 服务状态
///
/// ## 处理逻辑
///
/// 1. Error 状态 → 清理资源
/// 2. 空闲超时5分钟→ 清理资源(资源回收)
/// 3. 健康检查(只对 Ready 状态)
/// - Pending → 跳过(等待启动完成)
/// - Ready → 执行探测
/// - 成功 → 重置失败计数
/// - 失败 → 失败计数 + 1
/// - 连续失败 >= 3 → 重启后端服务
pub async fn schedule_check_mcp_live() {
// 获取全局动态 router
let proxy_manager = get_proxy_manager();
// 获取所有 mcp 服务状态
let mcp_service_statuses = proxy_manager.get_all_mcp_service_status();
// 打印当前有多少个 mcp 插件服务在运行
info!(
"There are currently {} mcp plug-in services running",
mcp_service_statuses.len()
);
// 遍历所有 mcp 服务状态
for mcp_service_status in mcp_service_statuses {
// 获取服务信息
let mcp_id = mcp_service_status.mcp_id.clone();
let mcp_type = mcp_service_status.mcp_type.clone();
let cancellation_token = mcp_service_status.cancellation_token.clone();
// 1. 如果 mcp 的状态是 ERROR则清理资源
if let CheckMcpStatusResponseStatus::Error(_) =
mcp_service_status.check_mcp_status_response_status
{
if let Err(e) = proxy_manager.cleanup_resources(&mcp_id).await {
error!("Failed to cleanup resources for {}: {}", mcp_id, e);
}
continue;
}
// 根据 MCP 类型进行不同处理
match mcp_type {
McpType::Persistent => {
// 检查持久化服务是否已被取消或子进程已终止
if cancellation_token.is_cancelled() {
info!(
"The persistent MCP service {mcp_id} has been manually canceled and resources are being cleaned up."
);
if let Err(e) = proxy_manager.cleanup_resources(&mcp_id).await {
error!("Failed to cleanup resources for {}: {}", mcp_id, e);
}
continue;
}
// 检查子进程是否还在运行
if let Some(handler) = proxy_manager.get_proxy_handler(&mcp_id)
&& handler.is_terminated_async().await
{
info!(
"The persistent MCP service {mcp_id} child process ended abnormally and cleaned up resources."
);
if let Err(e) = proxy_manager.cleanup_resources(&mcp_id).await {
error!("Failed to cleanup resources for {}: {}", mcp_id, e);
}
}
}
McpType::OneShot => {
// 2. 检查空闲超时(基于所有请求的活动)
let idle_time = mcp_service_status.last_accessed.elapsed();
// 空闲超时 → 清理资源
if idle_time > ONESHOT_TIMEOUT {
info!(
"OneShot service {} idle timeout (idle time: {} seconds), clean up resources",
mcp_id,
idle_time.as_secs()
);
if let Err(e) = proxy_manager.cleanup_resources(&mcp_id).await {
error!("Failed to cleanup resources for {}: {}", mcp_id, e);
}
continue;
}
// 3. 健康检查(只对 Ready 状态)
// Pending 状态跳过探测,等待启动完成
if !matches!(
mcp_service_status.check_mcp_status_response_status,
CheckMcpStatusResponseStatus::Ready
) {
// Pending 状态跳过探测
continue;
}
// 执行健康探测
let handler = proxy_manager.get_proxy_handler(&mcp_id);
if let Some(handler) = handler {
let is_terminated = handler.is_terminated_async().await;
if is_terminated {
let failures = proxy_manager.increment_probe_failures(&mcp_id);
info!(
"OneShot service {} health check failed ({}/{})",
mcp_id, failures, MAX_PROBE_FAILURES
);
if failures >= MAX_PROBE_FAILURES {
info!(
"OneShot service {} failed continuously {} times, triggering a restart",
mcp_id, failures
);
restart_mcp_service(&mcp_id, proxy_manager).await;
}
} else {
// 探测成功,重置失败计数
proxy_manager.reset_probe_failures(&mcp_id);
}
}
}
}
}
}
/// 重启 MCP 服务
///
/// ## 重启流程
///
/// 1. 检查重启冷却期30秒
/// 2. 获取配置(从服务状态或缓存)
/// 3. 清理旧资源(保留配置缓存)
/// 4. 重新启动服务(复用 mcp_start_task
async fn restart_mcp_service(mcp_id: &str, proxy_manager: &crate::model::ProxyHandlerManager) {
// 1. 检查重启冷却期
if !GLOBAL_RESTART_TRACKER.can_restart(mcp_id) {
info!(
"Service {} is skipped during the restart cooling period.",
mcp_id
);
return;
}
// 2. 获取配置(优先从服务状态,其次从缓存)
let mcp_config = proxy_manager.get_mcp_config(mcp_id);
let mcp_config = match mcp_config {
Some(config) => Some(config),
None => proxy_manager.get_mcp_config_from_cache(mcp_id).await,
};
let Some(mcp_config) = mcp_config else {
warn!(
"Service {} has no configuration and cannot be restarted. Clean up resources.",
mcp_id
);
if let Err(e) = proxy_manager.cleanup_resources(mcp_id).await {
error!("Failed to cleanup resources for {}: {}", mcp_id, e);
}
return;
};
// 3. 清理旧资源(保留配置缓存)
if let Err(e) = proxy_manager.cleanup_resources_for_restart(mcp_id).await {
error!("Cleanup service {} resource failed: {}", mcp_id, e);
return;
}
// 4. 重新启动服务(复用 mcp_start_task自动设置 Pending 状态)
match mcp_start_task(mcp_config).await {
Ok((_router, _cancellation_token)) => {
// 重置失败计数(已在新的服务实例中初始化为 0
// 注意:此时 mcp_id 对应的是新的服务实例
proxy_manager.reset_probe_failures(mcp_id);
// 记录重启时间
GLOBAL_RESTART_TRACKER.record_restart(mcp_id);
info!("Service {} restarted successfully", mcp_id);
}
Err(e) => {
error!("Service {} failed to restart: {}", mcp_id, e);
// 重启失败,设置 Error 状态
// 注意:此时服务已被清理,无法设置状态,只能记录日志
// 下次请求到来时会触发重新启动
}
}
}

View File

@@ -0,0 +1,65 @@
use crate::server::task::schedule_check_mcp_live;
use log::{debug, info, warn};
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use tokio::time::{Duration, interval};
/// 启动定时任务定期检查MCP服务状态
///
/// 这个函数会创建一个tokio定时任务每隔指定的时间间隔执行一次`schedule_check_mcp_live`函数
/// 用于检查和清理不再需要的MCP服务资源
pub async fn start_schedule_task() {
info!("Start the MCP service status check scheduled task");
// 创建一个tokio定时器每20秒执行一次
let mut interval = interval(Duration::from_secs(20));
// 使用原子布尔值来跟踪任务是否正在执行
let is_running = Arc::new(AtomicBool::new(false));
// 启动一个新的异步任务
tokio::spawn(async move {
loop {
// 等待下一个时间点
interval.tick().await;
// 检查是否有任务正在执行
if is_running.load(Ordering::SeqCst) {
warn!(
"The last MCP service status check task has not been completed and this execution will be skipped."
);
continue;
}
// 标记任务开始执行
is_running.store(true, Ordering::SeqCst);
// 执行MCP服务状态检查
debug!("Perform periodic checks on MCP service status...");
// 在一个新的任务中执行检查,这样可以捕获任何异常
let is_running_clone = is_running.clone();
tokio::spawn(async move {
// 执行检查任务
match tokio::time::timeout(
Duration::from_secs(10), // 设置超时时间为10秒小于间隔时间
schedule_check_mcp_live(),
)
.await
{
Ok(_) => {
debug!("MCP service status check completed");
}
Err(_) => {
warn!("MCP service status check task timed out");
}
}
// 无论成功还是失败,都标记任务已完成
is_running_clone.store(false, Ordering::SeqCst);
});
}
});
info!("MCP service status check scheduled task has been started");
}

View File

@@ -0,0 +1,60 @@
use anyhow::Result;
use opentelemetry::global;
use opentelemetry_sdk::trace::{RandomIdGenerator, Sampler, SdkTracerProvider};
/// 初始化 OpenTelemetry tracer provider
///
/// 这个函数必须在创建 telemetry layer 之前调用
pub fn init_tracer_provider(_service_name: &str, _service_version: &str) -> Result<()> {
// 创建 tracer provider
let tracer_provider = SdkTracerProvider::builder()
.with_sampler(Sampler::AlwaysOn)
.with_id_generator(RandomIdGenerator::default())
.build();
// 设置全局 tracer provider
global::set_tracer_provider(tracer_provider);
Ok(())
}
/// 创建增强的 OpenTelemetry layer
///
/// 这个函数创建一个配置好的 OpenTelemetry layer可以与现有的 tracing 配置集成
/// 注意:必须先调用 init_tracer_provider()
pub fn create_telemetry_layer() -> impl tracing_subscriber::Layer<tracing_subscriber::Registry> {
tracing_opentelemetry::layer()
}
/// 记录服务启动信息
///
/// 在 telemetry 系统初始化后调用,记录服务的基本信息
pub fn log_service_info(service_name: &str, service_version: &str) -> Result<()> {
tracing::info!(
service_name = %service_name,
service_version = %service_version,
"Service started with OpenTelemetry tracing enabled"
);
Ok(())
}
/// 优雅关闭 OpenTelemetry
pub fn shutdown_telemetry() {
tracing::info!("Shutting down OpenTelemetry");
// 注意:在新版本的 OpenTelemetry 中shutdown 方法可能不同
// 这里我们简单地记录关闭信息
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_log_service_info() {
let result = log_service_info("test-service", "0.1.0");
assert!(result.is_ok());
// 清理
shutdown_telemetry();
}
}

View File

@@ -0,0 +1,157 @@
//! Coze MCP Service Integration Tests
//!
//! This module tests the protocol conversion from Streamable HTTP (backend) to SSE (frontend)
//! when connecting to Coze MCP services.
//!
//! # Configuration
//!
//! These tests require the following environment variables:
//!
//! - `COZE_PLUGIN_ID` - Your Coze plugin ID
//! - `COZE_BEARER_TOKEN` - Your Coze API Bearer token
//!
//! # Running the tests
//!
//! ```bash
//! # Set environment variables and run the test
//! COZE_PLUGIN_ID="your_plugin_id" \
//! COZE_BEARER_TOKEN="your_bearer_token" \
//! cargo test -p mcp-stdio-proxy test_coze_streamable_to_sse_proxy
//!
//! # Or run with logging
//! COZE_PLUGIN_ID="your_plugin_id" \
//! COZE_BEARER_TOKEN="your_bearer_token" \
//! RUST_LOG=debug cargo test -p mcp-stdio-proxy test_coze_streamable_to_sse_proxy
//! ```
use anyhow::Result;
use std::time::Duration;
use tokio::net::TcpListener;
use crate::{
mcp_start_task,
model::{McpConfig, McpProtocol, McpType},
proxy::{McpClientConfig, SseClientConnection},
};
/// Builds Coze MCP configuration from environment variables
fn get_coze_config() -> Result<String> {
let plugin_id = std::env::var("COZE_PLUGIN_ID")
.map_err(|_| anyhow::anyhow!("COZE_PLUGIN_ID environment variable not set"))?;
let bearer_token = std::env::var("COZE_BEARER_TOKEN")
.map_err(|_| anyhow::anyhow!("COZE_BEARER_TOKEN environment variable not set"))?;
let config = r#"{
"mcpServers": {
"coze_plugin_tianyancha": {
"url": "https://mcp.coze.cn/v1/plugins/PLUGIN_ID",
"headers": {
"Authorization": "Bearer BEARER_TOKEN"
}
}
}
}"#;
Ok(config
.replace("PLUGIN_ID", &plugin_id)
.replace("BEARER_TOKEN", &bearer_token))
}
/// Test: Streamable HTTP backend to SSE frontend protocol conversion
///
/// This test verifies that the mcp-proxy correctly:
/// 1. Configures SSE protocol for the client (frontend)
/// 2. Auto-detects Streamable HTTP protocol for the Coze backend
/// 3. Transparently converts between the two protocols
/// 4. Returns valid tools/list responses
#[tokio::test]
#[ignore] // Mark as ignored since it requires network access
async fn test_coze_streamable_to_sse_proxy() -> Result<()> {
// Initialize logging for test
let _ = tracing_subscriber::fmt().with_test_writer().try_init();
println!("🧪 Starting Coze MCP test: Streamable HTTP -> SSE conversion");
// Step 1: Create configuration with SSE client protocol
// The backend protocol (Streamable HTTP) will be auto-detected
let coze_config = get_coze_config()?;
let mcp_config = McpConfig::from_json_with_server(
"coze_plugin_tianyancha".to_string(),
coze_config,
McpType::OneShot,
McpProtocol::Sse, // SSE frontend (client protocol)
)?;
println!("✅ Configuration created with SSE client protocol");
// Step 2: Start MCP service
// The proxy will auto-detect the backend protocol and create appropriate routes
let (router, ct) = mcp_start_task(mcp_config).await?;
println!("✅ MCP service started");
// Step 3: Start HTTP server with the router
let listener = TcpListener::bind("127.0.0.1:0").await?;
let port = listener.local_addr()?.port();
println!("✅ HTTP server listening on 127.0.0.1:{}", port);
// Spawn server in background
let server_handle = tokio::spawn(async move {
axum::serve(listener, router.into_make_service())
.await
.expect("Server error");
});
// Step 4: Wait for backend to be ready
tokio::time::sleep(Duration::from_secs(3)).await;
println!("✅ Backend ready");
// Step 5: Construct SSE endpoint path
// The SSE server exposes endpoints at /mcp/sse/proxy/{mcp_id}/sse
let sse_url = format!(
"http://127.0.0.1:{}/mcp/sse/proxy/coze_plugin_tianyancha/sse",
port
);
// Step 6: Connect SSE client
let client_config = McpClientConfig::new(sse_url.to_string());
let conn = tokio::time::timeout(
Duration::from_secs(30),
SseClientConnection::connect(client_config.clone()),
)
.await
.map_err(|_| anyhow::anyhow!("SSE connection timeout (30s)"))?
.map_err(|e| anyhow::anyhow!("SSE connection failed: {}", e))?;
println!("✅ SSE client connected to {}", sse_url);
// Step 7: Get tools list using the high-level API
let tools = tokio::time::timeout(Duration::from_secs(30), conn.list_tools())
.await
.map_err(|_| anyhow::anyhow!("list_tools timeout (30s)"))?
.map_err(|e| anyhow::anyhow!("list_tools failed: {}", e))?;
println!("📋 Received tools/list response: {} tools", tools.len());
// Step 8: Verify response structure
if tools.is_empty() {
println!("⚠️ Warning: tools/list returned empty array");
} else {
println!("✅ Found {} tools:", tools.len());
for tool in &tools {
let desc = tool.description.as_deref().unwrap_or("no description");
println!(" - {} : {}", tool.name, desc);
}
}
// Step 9: Verify tool structure
for tool in &tools {
assert!(!tool.name.is_empty(), "Tool name should not be empty");
// Description is optional, so we just check the name
println!(" ✓ Tool '{}' has valid structure", tool.name);
}
// Step 10: Cleanup
ct.cancel();
server_handle.abort();
println!("🧹 Cleanup complete");
Ok(())
}

View File

@@ -0,0 +1,12 @@
#[cfg(test)]
pub mod test_utils {
// Tests utility module for common test setup
}
// Coze MCP integration tests - Streamable HTTP to SSE conversion
#[cfg(test)]
pub mod coze_mcp_test;
// Protocol detection tests - SSE vs Streamable HTTP
#[cfg(test)]
pub mod protocol_detection_test;

View File

@@ -0,0 +1,270 @@
//! Protocol Detection Integration Tests
//!
//! Tests protocol detection (SSE vs Streamable HTTP) against real MCP services.
//!
//! # Running the tests
//!
//! ```bash
//! # Run the zimage streamable HTTP test (requires DASHSCOPE_API_KEY)
//! DASHSCOPE_API_KEY=sk-xxx cargo test -p mcp-stdio-proxy test_zimage_protocol_detection -- --ignored --nocapture
//!
//! # Run the howtocook SSE test
//! cargo test -p mcp-stdio-proxy test_howtocook_sse_protocol_detection -- --ignored --nocapture
//!
//! # Run all protocol detection network tests
//! DASHSCOPE_API_KEY=sk-xxx cargo test -p mcp-stdio-proxy protocol_detection_test -- --ignored --nocapture
//!
//! # Run with debug logging
//! DASHSCOPE_API_KEY=sk-xxx RUST_LOG=debug cargo test -p mcp-stdio-proxy protocol_detection_test -- --ignored --nocapture
//! ```
use crate::model::{McpJsonServerParameters, McpProtocol, McpServerConfig};
/// The howtocook SSE MCP JSON config (SSE protocol, 测试环境密钥)
const HOWTOCOOK_MCP_JSON: &str = r#"{
"mcpServers": {
"howtocook-跳跳糖": {
"type": "sse",
"url": "https://testagent.xspaceagi.com/api/mcp/sse?ak=ak-27b83516dfd4417a82f764fe3e859a6e"
}
}
}"#;
/// Build zimage MCP JSON config with Authorization token from environment variable
///
/// 环境变量: `DASHSCOPE_API_KEY`
/// 运行网络测试前需设置: `export DASHSCOPE_API_KEY=sk-xxx`
fn build_zimage_mcp_json(api_key: &str) -> String {
format!(
r#"{{
"mcpServers": {{
"zimage": {{
"type": "streamableHttp",
"description": "Z-Image-Turbo 图像生成模型",
"isActive": true,
"name": "阿里云百炼_Z Image 图像生成",
"baseUrl": "https://dashscope.aliyuncs.com/api/v1/mcps/zimage/mcp",
"headers": {{
"Authorization": "Bearer {}"
}}
}}
}}
}}"#,
api_key
)
}
/// 用于本地解析测试的占位密钥(不需要真实密钥)
const ZIMAGE_TEST_PLACEHOLDER_KEY: &str = "test-placeholder-key";
// ==================== 本地解析测试(无网络) ====================
#[test]
fn test_zimage_config_type_parsed_as_stream() {
let zimage_json = build_zimage_mcp_json(ZIMAGE_TEST_PLACEHOLDER_KEY);
let params = McpJsonServerParameters::from(zimage_json);
let config = params.try_get_first_mcp_server().unwrap();
match config {
McpServerConfig::Url(url_config) => {
// 1. 原始 type 字段值
assert_eq!(url_config.r#type, Some("streamableHttp".to_string()));
// 2. get_protocol_type() 返回 Some且 is_streamable
let protocol_type = url_config.get_protocol_type();
assert!(
protocol_type.is_some(),
"streamableHttp should be recognized"
);
assert!(protocol_type.as_ref().unwrap().is_streamable());
// 3. 转换为 McpProtocol::Stream
assert_eq!(
protocol_type.unwrap().to_mcp_protocol(),
McpProtocol::Stream
);
// 4. FromStr 解析也能识别 "streamableHttp"
assert_eq!(
"streamableHttp".parse::<McpProtocol>(),
Ok(McpProtocol::Stream)
);
// 5. URL 正确解析
assert_eq!(
url_config.get_url(),
"https://dashscope.aliyuncs.com/api/v1/mcps/zimage/mcp"
);
// 6. headers 包含 Authorization
let headers = url_config.headers.as_ref().unwrap();
assert!(headers.contains_key("Authorization"));
assert_eq!(
headers["Authorization"],
format!("Bearer {}", ZIMAGE_TEST_PLACEHOLDER_KEY)
);
}
McpServerConfig::Command(_) => panic!("Expected URL config"),
}
}
// ==================== 网络探测测试(需要网络,默认 ignore ====================
/// Test: SSE detector should return false for a Streamable HTTP service
///
/// 使用真实 zimage 服务验证:
/// - is_sse_with_headers → false不是 SSE
/// - is_streamable_http_with_headers → true是 Streamable HTTP
/// - detect_mcp_protocol_with_headers → Stream
///
/// 需要设置环境变量: `DASHSCOPE_API_KEY`
#[tokio::test]
#[ignore] // 需要网络访问 + DASHSCOPE_API_KEY 环境变量
async fn test_zimage_protocol_detection() {
let api_key = std::env::var("DASHSCOPE_API_KEY")
.expect("需要设置 DASHSCOPE_API_KEY 环境变量才能运行此测试");
let zimage_json = build_zimage_mcp_json(&api_key);
let params = McpJsonServerParameters::from(zimage_json);
let config = params.try_get_first_mcp_server().unwrap();
let (url, headers) = match config {
McpServerConfig::Url(url_config) => {
let url = url_config.get_url().to_string();
let headers = url_config.headers.clone().unwrap_or_default();
(url, headers)
}
_ => panic!("Expected URL config"),
};
println!("=== Protocol detection test: {} ===", url);
// 1. SSE 探测应返回 false
println!("\\n--- SSE Probing ---");
let is_sse = mcp_sse_proxy::is_sse_with_headers(&url, Some(&headers)).await;
println!("is_sse_with_headers = {}", is_sse);
assert!(!is_sse, "Streamable HTTP 服务不应被识别为 SSE");
// 2. Streamable HTTP 探测应返回 true
println!("\\n--- Streamable HTTP probe ---");
let is_stream =
mcp_streamable_proxy::is_streamable_http_with_headers(&url, Some(&headers)).await;
println!("is_streamable_http_with_headers = {}", is_stream);
assert!(
is_stream,
"zimage 服务应被识别为 Streamable HTTP需要传递 Authorization header"
);
// 3. 综合探测应返回 Stream
println!("\\n--- Comprehensive protocol detection ---");
let detected = crate::server::detect_mcp_protocol_with_headers(&url, Some(&headers))
.await
.unwrap();
println!("detect_mcp_protocol_with_headers = {:?}", detected);
assert_eq!(detected, McpProtocol::Stream, "综合探测应返回 Stream 协议");
println!("\\n=== All detection results are correct ===");
}
// ==================== howtocook SSE 本地解析测试 ====================
#[test]
fn test_howtocook_config_type_parsed_as_sse() {
let params = McpJsonServerParameters::from(HOWTOCOOK_MCP_JSON.to_string());
let config = params.try_get_first_mcp_server().unwrap();
match config {
McpServerConfig::Url(url_config) => {
// 1. 原始 type 字段值
assert_eq!(url_config.r#type, Some("sse".to_string()));
// 2. get_protocol_type() 返回 Some且不是 streamable
let protocol_type = url_config.get_protocol_type();
assert!(protocol_type.is_some(), "sse should be recognized");
assert!(!protocol_type.as_ref().unwrap().is_streamable());
// 3. 转换为 McpProtocol::Sse
assert_eq!(protocol_type.unwrap().to_mcp_protocol(), McpProtocol::Sse);
// 4. FromStr 解析也能识别 "sse"
assert_eq!("sse".parse::<McpProtocol>(), Ok(McpProtocol::Sse));
assert_eq!("SSE".parse::<McpProtocol>(), Ok(McpProtocol::Sse));
// 5. URL 正确解析(使用 url 字段而非 baseUrl
assert_eq!(
url_config.get_url(),
"https://testagent.xspaceagi.com/api/mcp/sse?ak=ak-27b83516dfd4417a82f764fe3e859a6e"
);
// 6. 无 headers
assert!(url_config.headers.is_none());
}
McpServerConfig::Command(_) => panic!("Expected URL config"),
}
}
// ==================== howtocook SSE 网络探测测试 ====================
/// Test: SSE detector should return true for a real SSE service
///
/// 使用真实 howtocook SSE 服务验证:
/// - is_sse_with_headers → true是 SSE应探测到 event: endpoint
/// - detect_mcp_protocol_with_headers → Sse
#[tokio::test]
#[ignore] // 需要网络访问,使用 --ignored 运行
async fn test_howtocook_sse_protocol_detection() {
let params = McpJsonServerParameters::from(HOWTOCOOK_MCP_JSON.to_string());
let config = params.try_get_first_mcp_server().unwrap();
let url = match config {
McpServerConfig::Url(url_config) => url_config.get_url().to_string(),
_ => panic!("Expected URL config"),
};
println!("=== SSE protocol detection test: {} ===", url);
// 1. SSE 探测应返回 true该服务是真实 MCP SSE会发送 event: endpoint
println!("\\n--- SSE Probing ---");
let is_sse = mcp_sse_proxy::is_sse(&url).await;
println!("is_sse = {}", is_sse);
assert!(
is_sse,
"howtocook SSE 服务应被识别为 SSE发现 event: endpoint"
);
// 2. Streamable HTTP 探测应返回 falseSSE 服务不应被识别为 Streamable HTTP
println!("\\n--- Streamable HTTP probe ---");
let is_stream = mcp_streamable_proxy::is_streamable_http(&url).await;
println!("is_streamable_http = {}", is_stream);
assert!(!is_stream, "SSE 服务不应被识别为 Streamable HTTP");
// 3. 综合探测应返回 Sse
println!("\\n--- Comprehensive protocol detection ---");
let detected = crate::server::detect_mcp_protocol(&url).await.unwrap();
println!("detect_mcp_protocol = {:?}", detected);
assert_eq!(detected, McpProtocol::Sse, "综合探测应返回 Sse 协议");
println!("\\n=== SSE detection result is correct ===");
}
/// Test: protocol detection without headers should still default to Stream
///
/// 不传 Authorization header探测可能失败但应兜底为 Stream
#[tokio::test]
#[ignore] // 需要网络访问
async fn test_zimage_detection_without_headers() {
let url = "https://dashscope.aliyuncs.com/api/v1/mcps/zimage/mcp";
println!("=== No header detection test: {} ===", url);
// SSE 探测应返回 false
let is_sse = mcp_sse_proxy::is_sse(url).await;
println!("is_sse = {}", is_sse);
assert!(!is_sse);
// 综合探测应兜底为 Stream
let detected = crate::server::detect_mcp_protocol(url).await.unwrap();
println!("detect_mcp_protocol = {:?}", detected);
assert_eq!(detected, McpProtocol::Stream, "无 header 时应兜底为 Stream");
println!("=== No header, the detection result is correct ===");
}