提交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,60 @@
//! Backend Bridge trait for cross-protocol MCP proxy
//!
//! This trait provides a protocol-agnostic interface for backend MCP connections,
//! enabling `mcp-sse-proxy` to communicate with any backend implementation
//! (e.g., `mcp-streamable-proxy`) without direct dependencies.
//!
//! All method parameters and return values use `serde_json::Value` to avoid
//! coupling to specific rmcp version types.
use std::future::Future;
use std::pin::Pin;
use serde_json::Value;
/// Protocol-agnostic backend bridge for MCP proxy
///
/// Implementations of this trait wrap a concrete MCP client (e.g., rmcp 1.4.0's
/// `ProxyHandler`) and expose its functionality through a version-independent
/// JSON-based interface.
///
/// # Design
///
/// This trait uses `Pin<Box<dyn Future>>` for async methods instead of `async_trait`
/// to maintain object safety (`dyn BackendBridge`) without additional macro dependencies.
pub trait BackendBridge: Send + Sync {
/// Get the MCP service identifier (used for logging and tracking)
fn mcp_id(&self) -> &str;
/// Get the backend's ServerInfo as JSON
///
/// Used for cross-rmcp-version bridging: the implementation serializes its
/// native `ServerInfo` to JSON, and the consumer deserializes it into its
/// own rmcp version's `ServerInfo` type.
fn get_server_info_json(&self) -> Value;
/// Check if the backend connection is available (fast, synchronous check)
fn is_backend_available(&self) -> bool;
/// Check if the MCP server is ready (async, sends a validation request)
fn is_mcp_server_ready(&self) -> Pin<Box<dyn Future<Output = bool> + Send + '_>>;
/// Check if the backend connection is terminated (async)
fn is_terminated_async(&self) -> Pin<Box<dyn Future<Output = bool> + Send + '_>>;
/// Call an MCP method on the backend via JSON bridge
///
/// # Arguments
/// * `method` - MCP method name (e.g., "tools/list", "tools/call",
/// "resources/list", "prompts/get", "completion/complete")
/// * `params` - JSON-serialized method parameters
///
/// # Returns
/// * `Ok(Value)` - JSON-serialized result from the backend
/// * `Err(String)` - Error description
fn call_peer_method(
&self,
method: &str,
params: Value,
) -> Pin<Box<dyn Future<Output = Result<Value, String>> + Send + '_>>;
}

View File

@@ -0,0 +1,131 @@
//! Client connection configuration for MCP services
//!
//! This module provides a unified configuration structure for connecting
//! to MCP servers via SSE or Streamable HTTP protocols.
use std::collections::HashMap;
use std::time::Duration;
/// Configuration for MCP client connections
///
/// This struct provides a protocol-agnostic way to configure connections
/// to MCP servers. It can be used with both SSE and Streamable HTTP transports.
///
/// # Example
///
/// ```rust
/// use mcp_common::McpClientConfig;
/// use std::time::Duration;
///
/// let config = McpClientConfig::new("http://localhost:8080/mcp")
/// .with_header("Authorization", "Bearer token123")
/// .with_connect_timeout(Duration::from_secs(30));
/// ```
#[derive(Clone, Debug, Default)]
pub struct McpClientConfig {
/// Target URL for the MCP server
pub url: String,
/// HTTP headers to include in requests
pub headers: HashMap<String, String>,
/// Connection timeout duration
pub connect_timeout: Option<Duration>,
/// Read timeout duration
pub read_timeout: Option<Duration>,
}
impl McpClientConfig {
/// Create a new configuration with the given URL
///
/// # Arguments
/// * `url` - The MCP server URL (e.g., "http://localhost:8080/mcp")
pub fn new(url: impl Into<String>) -> Self {
Self {
url: url.into(),
headers: HashMap::new(),
connect_timeout: None,
read_timeout: None,
}
}
/// Add a header to the configuration
///
/// # Arguments
/// * `key` - Header name
/// * `value` - Header value
pub fn with_header(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.headers.insert(key.into(), value.into());
self
}
/// Add multiple headers from a HashMap
pub fn with_headers(mut self, headers: HashMap<String, String>) -> Self {
self.headers.extend(headers);
self
}
/// Set the Authorization header with a Bearer token
///
/// # Arguments
/// * `token` - The bearer token (without "Bearer " prefix)
pub fn with_bearer_auth(self, token: impl Into<String>) -> Self {
self.with_header("Authorization", format!("Bearer {}", token.into()))
}
/// Set the connection timeout
///
/// # Arguments
/// * `timeout` - Maximum time to wait for connection establishment
pub fn with_connect_timeout(mut self, timeout: Duration) -> Self {
self.connect_timeout = Some(timeout);
self
}
/// Set the read timeout
///
/// # Arguments
/// * `timeout` - Maximum time to wait for response data
pub fn with_read_timeout(mut self, timeout: Duration) -> Self {
self.read_timeout = Some(timeout);
self
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_new_config() {
let config = McpClientConfig::new("http://localhost:8080");
assert_eq!(config.url, "http://localhost:8080");
assert!(config.headers.is_empty());
}
#[test]
fn test_with_header() {
let config = McpClientConfig::new("http://localhost:8080").with_header("X-Custom", "value");
assert_eq!(config.headers.get("X-Custom"), Some(&"value".to_string()));
}
#[test]
fn test_with_bearer_auth() {
let config = McpClientConfig::new("http://localhost:8080").with_bearer_auth("mytoken");
assert_eq!(
config.headers.get("Authorization"),
Some(&"Bearer mytoken".to_string())
);
}
#[test]
fn test_builder_chain() {
let config = McpClientConfig::new("http://localhost:8080")
.with_header("X-Api-Key", "key123")
.with_connect_timeout(Duration::from_secs(30))
.with_read_timeout(Duration::from_secs(60));
assert_eq!(config.url, "http://localhost:8080");
assert_eq!(config.headers.get("X-Api-Key"), Some(&"key123".to_string()));
assert_eq!(config.connect_timeout, Some(Duration::from_secs(30)));
assert_eq!(config.read_timeout, Some(Duration::from_secs(60)));
}
}

View File

@@ -0,0 +1,51 @@
//! MCP 服务配置
use crate::ToolFilter;
use std::collections::HashMap;
/// MCP 服务配置
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
pub struct McpServiceConfig {
/// 服务名称
pub name: String,
/// 启动命令
pub command: String,
/// 命令参数
pub args: Option<Vec<String>>,
/// 环境变量(来自 MCP JSON 配置)
pub env: Option<HashMap<String, String>>,
/// 工具过滤配置
#[serde(skip_serializing_if = "Option::is_none")]
pub tool_filter: Option<ToolFilter>,
}
impl McpServiceConfig {
/// 创建新配置
pub fn new(name: String, command: String) -> Self {
Self {
name,
command,
args: None,
env: None,
tool_filter: None,
}
}
/// 设置参数
pub fn with_args(mut self, args: Vec<String>) -> Self {
self.args = Some(args);
self
}
/// 设置环境变量
pub fn with_env(mut self, env: HashMap<String, String>) -> Self {
self.env = Some(env);
self
}
/// 设置工具过滤器
pub fn with_tool_filter(mut self, filter: ToolFilter) -> Self {
self.tool_filter = Some(filter);
self
}
}

View File

@@ -0,0 +1,143 @@
//! 子进程启动诊断工具
//!
//! 提供 stdio 子进程启动时的环境诊断日志,供 mcp-proxy / mcp-sse-proxy /
//! mcp-streamable-proxy 共用,避免诊断代码散落在业务逻辑中。
use std::collections::HashMap;
/// PATH 分隔符
const PATH_SEP: char = if cfg!(windows) { ';' } else { ':' };
/// 诊断时检查的镜像相关环境变量
const MIRROR_ENV_KEYS: &[&str] = &[
"npm_config_registry",
"UV_INDEX_URL",
"UV_EXTRA_INDEX_URL",
"UV_INSECURE_HOST",
"PIP_INDEX_URL",
];
// ─── 纯数据格式化(不依赖日志框架) ───
/// 返回 PATH 摘要字符串,只展示前 `max_segments` 段
///
/// 示例: `/usr/bin:/usr/local/bin ... (12 entries total)`
pub fn format_path_summary(max_segments: usize) -> String {
match std::env::var("PATH") {
Ok(path) => {
let segments: Vec<&str> = path.split(PATH_SEP).collect();
let preview: String = segments
.iter()
.take(max_segments)
.copied()
.collect::<Vec<_>>()
.join(&PATH_SEP.to_string());
if segments.len() > max_segments {
format!("{} ... ({} entries total)", preview, segments.len())
} else {
preview
}
}
Err(_) => "(unset)".to_string(),
}
}
/// 收集当前进程中已设置的镜像相关环境变量
///
/// 返回 `Vec<(key, value)>`,仅包含已设置的条目。
pub fn collect_mirror_env_vars() -> Vec<(&'static str, String)> {
MIRROR_ENV_KEYS
.iter()
.filter_map(|&key| std::env::var(key).ok().map(|val| (key, val)))
.collect()
}
/// 构造 spawn 失败时的完整错误信息
pub fn format_spawn_error(
mcp_id: &str,
command: &str,
args: &Option<Vec<String>>,
inner: impl std::fmt::Display,
) -> String {
let path_val = std::env::var("PATH").unwrap_or_else(|_| "(unset)".to_string());
format!(
"Failed to spawn child process - MCP ID: {}, command: {}, \
args: {:?}, PATH: {}, error: {}",
mcp_id,
command,
args.as_ref().unwrap_or(&Vec::new()),
path_val,
inner
)
}
// ─── 带 tracing 的便捷函数 ───
/// 输出 stdio 子进程启动前的诊断日志debug 级别)
///
/// 包含PATH 摘要、镜像变量、config env keys 列表。
/// 业务代码只需在 spawn 前调用此函数。
pub fn log_stdio_spawn_context(tag: &str, mcp_id: &str, env: &Option<HashMap<String, String>>) {
tracing::debug!(
"[{}] MCP ID: {}, PATH: {}",
tag,
mcp_id,
format_path_summary(3),
);
for (key, val) in collect_mirror_env_vars() {
tracing::debug!("[{}] MCP ID: {}, {}={}", tag, mcp_id, key, val);
}
if let Some(env_vars) = env {
let keys: Vec<&String> = env_vars.keys().collect();
tracing::debug!("[{}] MCP ID: {}, config env keys: {:?}", tag, mcp_id, keys);
}
}
/// 启动阶段环境变量汇总eprintln 输出,日志框架尚未初始化时使用)
pub fn eprint_env_summary() {
eprintln!(" - PATH: {}", format_path_summary(3));
for (key, val) in collect_mirror_env_vars() {
eprintln!(" - {}={}", key, val);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_format_path_summary_not_empty() {
let summary = format_path_summary(3);
assert!(!summary.is_empty());
}
#[test]
fn test_collect_mirror_env_vars() {
// 不应 panic
let _ = collect_mirror_env_vars();
}
#[test]
fn test_format_spawn_error() {
let msg = format_spawn_error(
"test-id",
"npx",
&Some(vec!["-y".into(), "server".into()]),
"file not found",
);
assert!(msg.contains("test-id"));
assert!(msg.contains("npx"));
assert!(msg.contains("file not found"));
}
#[test]
fn test_log_stdio_spawn_context_no_panic() {
let mut env = HashMap::new();
env.insert("FOO".to_string(), "bar".to_string());
log_stdio_spawn_context("Test", "test-id", &Some(env));
log_stdio_spawn_context("Test", "test-id", &None);
}
}

View File

@@ -0,0 +1,391 @@
//! 国际化模块
//!
//! 使用 rust-i18n 提供多语言支持,支持中文简体、中文繁体、英文三种语言。
//!
//! # 使用方法
//!
//! ```rust,ignore
//! use mcp_common::{t, set_locale, init_locale_from_env};
//!
//! // 初始化语言设置(通常在程序启动时调用)
//! init_locale_from_env();
//!
//! // 获取翻译
//! let msg = t!("errors.mcp_proxy.service_not_found", service = "my-service");
//!
//! // 手动设置语言
//! set_locale("zh-CN");
//! ```
//!
//! # 支持的语言
//!
//! - `en` - English
//! - `zh-CN` - 中文简体
//! - `zh-TW` - 中文繁体
//!
//! # 配置优先级
//!
//! 1. `DEFAULT_LOCALE` 环境变量(最高优先级)
//! 2. `LANG` 系统环境变量
//! 3. 默认使用英文
//!
//! # 线程安全
//!
//! `set_locale()` 和 `init_locale_from_env()` 应在程序启动时调用。
//! 语言设置是全局状态,不建议在运行时多线程环境中修改。
// 注意: rust-i18n 的 i18n! 宏需要在 lib.rs 中调用
/// 导出 t! 宏,用于获取翻译
pub use rust_i18n::t;
/// 设置当前语言
///
/// # 线程安全
///
/// 此函数应在程序启动时调用,不建议在运行时多线程环境中调用。
/// 语言设置是全局状态,并发调用可能导致不一致的翻译结果。
///
/// # 示例
///
/// ```rust
/// use mcp_common::set_locale;
///
/// set_locale("zh-CN");
/// set_locale("en");
/// set_locale("zh-TW");
/// ```
pub fn set_locale(locale: &str) {
rust_i18n::set_locale(locale);
}
/// 获取当前语言设置
///
/// # 示例
///
/// ```rust
/// use mcp_common::current_locale;
///
/// let locale = current_locale();
/// println!("Current locale: {}", locale);
/// ```
pub fn current_locale() -> String {
rust_i18n::locale().to_string()
}
/// 支持的语言列表
pub const AVAILABLE_LOCALES: &[&str] = &["en", "zh-CN", "zh-TW"];
/// 默认语言
pub const DEFAULT_LOCALE: &str = "en";
/// 从环境变量初始化语言设置
///
/// 按照以下优先级设置语言:
/// 1. `DEFAULT_LOCALE` 环境变量(最高优先级)
/// 2. `LANG` 系统环境变量(自动解析语言代码)
/// 3. 默认使用英文
///
/// # 示例
///
/// ```rust
/// use mcp_common::init_locale_from_env;
///
/// // 在程序启动时调用
/// init_locale_from_env();
/// ```
pub fn init_locale_from_env() {
// 优先使用 DEFAULT_LOCALE 环境变量(最高优先级)
if let Ok(lang) = std::env::var("DEFAULT_LOCALE") {
let locale = normalize_locale(&lang);
if AVAILABLE_LOCALES.contains(&locale.as_str()) {
set_locale(&locale);
return;
} else {
tracing::warn!(
"Invalid locale '{}' from DEFAULT_LOCALE, falling back. Supported: {:?}",
locale,
AVAILABLE_LOCALES
);
}
}
// 其次尝试从 LANG 环境变量解析
if let Ok(lang) = std::env::var("LANG") {
let locale = parse_lang_env(&lang);
if AVAILABLE_LOCALES.contains(&locale.as_str()) {
set_locale(&locale);
return;
}
}
// 使用默认语言
set_locale(DEFAULT_LOCALE);
}
/// 标准化语言代码
///
/// 支持的输入格式:
/// - `en`, `EN`, `En`, `en_US`, `en_US.UTF-8` -> `en`
/// - `zh-CN`, `zh-cn`, `ZH-CN` -> `zh-CN`
/// - `zh_TW`, `zh-TW` -> `zh-TW`
/// - `zh`, `ZH` -> `zh-CN` (默认简体中文)
fn normalize_locale(input: &str) -> String {
let input = input.trim();
// 支持解析带编码/修饰符的值(例如 en_US.UTF-8、zh_CN@cjk
let input = input.split('.').next().unwrap_or(input);
let input = input.split('@').next().unwrap_or(input);
// 直接匹配
match input.to_lowercase().as_str() {
"en" | "en_us" | "en-us" | "en_gb" | "en-gb" => return "en".to_string(),
"zh-cn" | "zh_cn" | "zh-hans" => return "zh-CN".to_string(),
"zh-tw" | "zh_tw" | "zh-hant" => return "zh-TW".to_string(),
"zh" => return "zh-CN".to_string(), // 默认简体中文
_ => {}
}
// 尝试解析语言-地区格式
let parts: Vec<&str> = input.split(|c| c == '-' || c == '_').collect();
if parts.len() >= 2 {
let lang = parts[0].to_lowercase();
let region = parts[1].to_uppercase();
// 英文变体统一映射到 en
if lang == "en" {
return "en".to_string();
}
return format!("{}-{}", lang, region);
}
input.to_string()
}
/// 解析 LANG 环境变量
///
/// 支持的格式:
/// - `en_US.UTF-8` -> `en`
/// - `zh_CN.UTF-8` -> `zh-CN`
/// - `zh_TW.UTF-8` -> `zh-TW`
/// - `zh_CN` -> `zh-CN`
fn parse_lang_env(lang: &str) -> String {
// 移除编码部分 (如 .UTF-8)
let lang = lang.split('.').next().unwrap_or(lang);
// 移除修饰部分 (如 @cjk)
let lang = lang.split('@').next().unwrap_or(lang);
// 标准化格式
normalize_locale(lang)
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::{Mutex, OnceLock};
fn test_lock() -> &'static Mutex<()> {
static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
LOCK.get_or_init(|| Mutex::new(()))
}
struct EnvRestore {
saved: Vec<(&'static str, Option<String>)>,
}
impl Drop for EnvRestore {
fn drop(&mut self) {
for (key, value) in &self.saved {
match value {
Some(v) => unsafe { std::env::set_var(key, v) },
None => unsafe { std::env::remove_var(key) },
}
}
}
}
fn prepare_env(overrides: &[(&'static str, Option<&str>)]) -> EnvRestore {
let tracked_keys = ["DEFAULT_LOCALE", "LANG", "MCP_PROXY_LANG", "APP_LANG"];
let mut saved = Vec::with_capacity(tracked_keys.len());
for key in tracked_keys {
saved.push((key, std::env::var(key).ok()));
unsafe { std::env::remove_var(key) };
}
for (key, value) in overrides {
match value {
Some(v) => unsafe { std::env::set_var(key, v) },
None => unsafe { std::env::remove_var(key) },
}
}
EnvRestore { saved }
}
#[test]
fn test_normalize_locale() {
let _guard = test_lock().lock().expect("locale test lock poisoned");
assert_eq!(normalize_locale("en"), "en");
assert_eq!(normalize_locale("EN"), "en");
assert_eq!(normalize_locale("zh-CN"), "zh-CN");
assert_eq!(normalize_locale("zh-cn"), "zh-CN");
assert_eq!(normalize_locale("zh_CN"), "zh-CN");
assert_eq!(normalize_locale("zh-TW"), "zh-TW");
assert_eq!(normalize_locale("zh_tw"), "zh-TW");
assert_eq!(normalize_locale("zh"), "zh-CN");
assert_eq!(normalize_locale("en_US.UTF-8"), "en");
assert_eq!(normalize_locale("zh_CN@cjk"), "zh-CN");
}
#[test]
fn test_parse_lang_env() {
let _guard = test_lock().lock().expect("locale test lock poisoned");
assert_eq!(parse_lang_env("en_US.UTF-8"), "en");
assert_eq!(parse_lang_env("zh_CN.UTF-8"), "zh-CN");
assert_eq!(parse_lang_env("zh_TW.UTF-8"), "zh-TW");
assert_eq!(parse_lang_env("zh_CN"), "zh-CN");
assert_eq!(parse_lang_env("en_US@cjk"), "en");
}
#[test]
fn test_set_and_get_locale() {
let _guard = test_lock().lock().expect("locale test lock poisoned");
set_locale("zh-CN");
assert_eq!(current_locale(), "zh-CN");
set_locale("en");
assert_eq!(current_locale(), "en");
set_locale("zh-TW");
assert_eq!(current_locale(), "zh-TW");
}
/// 关键翻译键在所有语言中都存在的测试
#[test]
fn test_translation_completeness() {
let _guard = test_lock().lock().expect("locale test lock poisoned");
set_locale("en");
let test_msg = t!("common.success").to_string();
assert_ne!(
test_msg, "common.success",
"Translations are not loaded; expected crate-local locales to be available"
);
// 测试关键错误消息键
let critical_keys = [
"errors.mcp_proxy.service_not_found",
"errors.mcp_proxy.service_startup_failed",
"errors.document_parser.config",
"errors.document_parser.parse",
"errors.oss.config",
"errors.oss.network",
"errors.voice.config",
"errors.voice.transcription",
"cli.startup.service_starting",
"cli.startup.success",
"common.error",
"common.success",
];
for locale in AVAILABLE_LOCALES {
set_locale(locale);
for key in &critical_keys {
let msg = match *key {
"errors.mcp_proxy.service_not_found" => {
t!("errors.mcp_proxy.service_not_found", service = "test").to_string()
}
"errors.mcp_proxy.service_startup_failed" => t!(
"errors.mcp_proxy.service_startup_failed",
mcp_id = "test",
reason = "test"
)
.to_string(),
_ => t!(*key).to_string(),
};
// 翻译不应该返回 key 本身(表示翻译缺失)
assert_ne!(
msg, *key,
"Missing translation for '{}' in locale '{}'",
key, locale
);
}
}
}
/// 测试所有支持的语言都能正确切换
///
/// 注意:此测试依赖于 rust-i18n 的全局状态,在测试环境中可能不稳定。
/// 但在实际运行时,语言切换功能是正常的。
#[test]
fn test_all_locales_available() {
let _guard = test_lock().lock().expect("locale test lock poisoned");
// 测试每个语言代码都是有效的
for locale in AVAILABLE_LOCALES {
// 验证语言代码格式正确
assert!(
locale.contains('-') || *locale == "en",
"Locale '{}' should follow language-region format",
locale
);
// 尝试设置(在翻译文件不可用时可能不生效,但不应该崩溃)
set_locale(locale);
}
// 重置为默认语言
set_locale(DEFAULT_LOCALE);
}
#[test]
fn test_init_locale_from_env_prefers_default_locale() {
let _guard = test_lock().lock().expect("locale test lock poisoned");
let _env = prepare_env(&[
("DEFAULT_LOCALE", Some("zh-TW")),
("LANG", Some("en_US.UTF-8")),
("MCP_PROXY_LANG", Some("zh-CN")),
("APP_LANG", Some("zh-CN")),
]);
init_locale_from_env();
assert_eq!(current_locale(), "zh-TW");
set_locale(DEFAULT_LOCALE);
}
#[test]
fn test_init_locale_from_env_falls_back_to_lang() {
let _guard = test_lock().lock().expect("locale test lock poisoned");
let _env = prepare_env(&[
("DEFAULT_LOCALE", Some("unsupported")),
("LANG", Some("zh_CN.UTF-8")),
]);
init_locale_from_env();
assert_eq!(current_locale(), "zh-CN");
set_locale(DEFAULT_LOCALE);
}
#[test]
fn test_init_locale_from_env_falls_back_to_english() {
let _guard = test_lock().lock().expect("locale test lock poisoned");
let _env = prepare_env(&[
("DEFAULT_LOCALE", Some("unsupported")),
("LANG", Some("ja_JP.UTF-8")),
]);
init_locale_from_env();
assert_eq!(current_locale(), "en");
set_locale(DEFAULT_LOCALE);
}
#[test]
fn test_init_locale_from_env_ignores_removed_env_vars() {
let _guard = test_lock().lock().expect("locale test lock poisoned");
let _env = prepare_env(&[
("MCP_PROXY_LANG", Some("zh-TW")),
("APP_LANG", Some("zh-CN")),
]);
init_locale_from_env();
assert_eq!(current_locale(), "en");
set_locale(DEFAULT_LOCALE);
}
}

View File

@@ -0,0 +1,63 @@
//! MCP Common - Shared types and utilities for MCP proxy modules
//!
//! This crate provides common functionality shared across mcp-sse-proxy
//! and mcp-streamable-proxy to avoid code duplication.
//!
//! # Feature Flags
//!
//! - `telemetry`: 基础 OpenTelemetry 支持
//! - `otlp`: OTLP exporter 支持(用于 Jaeger 等)
//!
//! # 国际化 (i18n)
//!
//! 本 crate 提供多语言支持,使用 rust-i18n 实现。
//!
//! ## 使用方法
//!
//! ```rust
//! use mcp_common::{t, set_locale, init_locale_from_env};
//!
//! // 初始化语言设置(程序启动时调用)
//! init_locale_from_env();
//!
//! // 获取翻译
//! let msg = t!("errors.mcp_proxy.service_not_found", service = "my-service");
//! ```
// 初始化 i18n必须在 crate root 调用
#[macro_use]
extern crate rust_i18n;
// 初始化翻译文件,使用 crate 内置 locales支持独立发布
i18n!("locales", fallback = "en");
pub mod backend_bridge;
pub mod client_config;
pub mod config;
pub mod diagnostic;
pub mod i18n;
pub mod mirror;
pub mod process_compat;
pub mod tool_filter;
#[cfg(feature = "telemetry")]
pub mod telemetry;
// Re-export main types
pub use backend_bridge::BackendBridge;
pub use client_config::McpClientConfig;
pub use config::McpServiceConfig;
pub use process_compat::check_windows_command;
pub use process_compat::ensure_runtime_path;
pub use process_compat::resolve_windows_command;
pub use process_compat::spawn_stderr_reader;
pub use tool_filter::ToolFilter;
// Re-export i18n types
pub use i18n::{
AVAILABLE_LOCALES, DEFAULT_LOCALE, current_locale, init_locale_from_env, set_locale, t,
};
// Re-export telemetry types when feature is enabled
#[cfg(feature = "telemetry")]
pub use telemetry::{TracingConfig, TracingGuard, create_otel_layer, init_tracing};

View File

@@ -0,0 +1,93 @@
//! 镜像源配置:通过进程级环境变量为 npx/uvx 子进程设置国内镜像源
/// 镜像源配置
#[derive(Debug, Clone, Default)]
pub struct MirrorConfig {
pub npm_registry: Option<String>,
pub pypi_index_url: Option<String>,
}
impl MirrorConfig {
/// 从环境变量 `MCP_PROXY_NPM_REGISTRY` / `MCP_PROXY_PYPI_INDEX_URL` 加载
pub fn from_env() -> Self {
Self {
npm_registry: std::env::var("MCP_PROXY_NPM_REGISTRY").ok(),
pypi_index_url: std::env::var("MCP_PROXY_PYPI_INDEX_URL").ok(),
}
}
pub fn is_empty(&self) -> bool {
self.npm_registry.is_none() && self.pypi_index_url.is_none()
}
/// 设为进程级环境变量,所有子进程自动继承。
///
/// # Safety
/// 应在 main() 启动早期、单线程阶段调用。
pub fn apply_to_process_env(&self) {
unsafe {
if let Some(ref registry) = self.npm_registry
&& std::env::var("npm_config_registry").is_err()
{
std::env::set_var("npm_config_registry", registry);
}
if let Some(ref index_url) = self.pypi_index_url {
if std::env::var("UV_INDEX_URL").is_err() {
std::env::set_var("UV_INDEX_URL", index_url);
}
if std::env::var("PIP_INDEX_URL").is_err() {
std::env::set_var("PIP_INDEX_URL", index_url);
}
if std::env::var("UV_INSECURE_HOST").is_err()
&& index_url.starts_with("http://")
&& let Some(host) = extract_host(index_url)
{
std::env::set_var("UV_INSECURE_HOST", &host);
}
}
}
}
}
/// 从 URL 中提取 host
fn extract_host(url: &str) -> Option<String> {
let without_scheme = url
.strip_prefix("https://")
.or_else(|| url.strip_prefix("http://"))?;
let host = without_scheme.split('/').next()?.split(':').next()?;
if host.is_empty() {
None
} else {
Some(host.to_string())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_mirror_config_is_empty() {
assert!(MirrorConfig::default().is_empty());
assert!(
!MirrorConfig {
npm_registry: Some("test".to_string()),
pypi_index_url: None,
}
.is_empty()
);
}
#[test]
fn test_extract_host() {
assert_eq!(
extract_host("https://mirrors.aliyun.com/pypi/simple/"),
Some("mirrors.aliyun.com".to_string())
);
assert_eq!(
extract_host("https://example.com:8080/path"),
Some("example.com".to_string())
);
assert_eq!(extract_host("not-a-url"), None);
}
}

View File

@@ -0,0 +1,433 @@
//! 跨平台进程管理兼容层
//!
//! 提供统一的进程管理抽象,减少平台特定代码的侵入性。
//!
//! # 使用方法
//!
//! ## 命令检测
//!
//! ```ignore
//! use mcp_common::process_compat::check_windows_command;
//!
//! check_windows_command(&config.command);
//! ```
//!
//! ## 进程包装宏
//!
//! process-wrap 8.x (TokioCommandWrap):
//! ```ignore
//! use mcp_common::process_compat::wrap_process_v8;
//!
//! let mut wrapped_cmd = TokioCommandWrap::with_new(...);
//! wrap_process_v8!(wrapped_cmd);
//! wrapped_cmd.wrap(KillOnDrop);
//! ```
//!
//! process-wrap 9.x (CommandWrap):
//! ```ignore
//! use mcp_common::process_compat::wrap_process_v9;
//!
//! let mut wrapped_cmd = CommandWrap::with_new(...);
//! wrap_process_v9!(wrapped_cmd);
//! wrapped_cmd.wrap(KillOnDrop);
//! ```
#[cfg(windows)]
use tracing::{info, warn};
/// 检测 Windows 平台上可能导致弹窗的命令格式
///
/// 在 Windows 上,运行 `.cmd`、`.bat` 文件或 `npx` 命令可能会弹出 CMD 窗口。
/// 此函数会检测这些情况并输出警告,建议用户使用替代方案。
///
/// # Arguments
///
/// * `command` - 要执行的命令字符串
///
/// # Example
///
/// ```ignore
/// use mcp_common::process_compat::check_windows_command;
///
/// check_windows_command("npx some-server");
/// check_windows_command("mcp-server.cmd");
/// ```
#[cfg(windows)]
pub fn check_windows_command(command: &str) {
use std::path::Path;
let cmd_ext = Path::new(command)
.extension()
.and_then(|e| e.to_str())
.map(|s| s.to_ascii_lowercase());
match cmd_ext.as_deref() {
Some("cmd" | "bat") => {
warn!(
"[MCP] Windows detected .cmd/.bat command: {} - CMD window may pop up!",
command
);
warn!(
"[MCP] It is recommended to use node.exe to run the JS file directly, or use the full path in the configuration"
);
}
None => {
// 无扩展名,检查是否是 npx 命令
if command.contains("npx") {
warn!(
"[MCP] Windows detects npx command: {} - CMD window may pop up!",
command
);
warn!("[MCP] It is recommended to use node.exe to run JS files directly");
}
}
_ => {
info!("[MCP] Windows detected command format: {}", command);
}
}
}
/// Unix/macOS 平台的空实现
#[cfg(not(windows))]
pub fn check_windows_command(_command: &str) {
// 非 Windows 平台无需检测
}
/// Windows 上解析命令路径,自动添加扩展名
///
/// 在 Windows 上,命令如 `npx` 实际上是 `npx.cmd` 批处理文件。
/// `std::process::Command` 不会自动查找 `.cmd` 扩展名,需要手动指定。
/// 此函数尝试在 PATH 中查找命令,并返回带扩展名的完整路径或原始命令。
///
/// # Arguments
///
/// * `command` - 要解析的命令字符串
///
/// # Returns
///
/// 如果找到,返回带扩展名的命令;否则返回原始命令
///
/// # Example
///
/// ```ignore
/// use mcp_common::process_compat::resolve_windows_command;
///
/// let resolved = resolve_windows_command("npx");
/// // 返回 "npx.cmd" 或 "C:\Program Files\nodejs\npx.cmd"
/// ```
#[cfg(target_os = "windows")]
pub fn resolve_windows_command(command: &str) -> String {
use std::path::Path;
// 如果已经有扩展名,直接返回
if Path::new(command).extension().is_some() {
return command.to_string();
}
// 如果是绝对路径,直接返回
if Path::new(command).is_absolute() {
return command.to_string();
}
// 获取 PATH 环境变量
let path_env = match std::env::var("PATH") {
Ok(p) => p,
Err(_) => return command.to_string(),
};
// Windows 可执行文件扩展名(按优先级)
let extensions = [".cmd", ".exe", ".bat", ".ps1"];
// 遍历 PATH 中的每个目录
for dir in path_env.split(';') {
let dir = dir.trim();
if dir.is_empty() {
continue;
}
// 尝试每个扩展名
for ext in &extensions {
let full_path = Path::new(dir).join(format!("{}{}", command, ext));
if full_path.exists() {
tracing::debug!(
"[MCP] Windows command analysis: {} -> {}",
command,
full_path.display()
);
// 返回带扩展名的命令(不是完整路径,保持简洁)
return format!("{}{}", command, ext);
}
}
}
// 未找到,返回原始命令
command.to_string()
}
/// 非 Windows 平台的空实现
#[cfg(not(target_os = "windows"))]
pub fn resolve_windows_command(command: &str) -> String {
command.to_string()
}
/// 确保应用内置运行时路径NUWAX_APP_RUNTIME_PATH在 PATH 最前面。
///
/// 当应用捆绑了 node/uv 等运行时时,通过 `NUWAX_APP_RUNTIME_PATH` 传递其路径。
/// 此函数将这些路径插入到给定 PATH 的最前面,确保优先使用应用内置版本,
/// 即使用户在 MCP 配置的 `env` 中指定了自定义 PATH。
///
/// **按段去重**:将 runtime_path 和现有 PATH 拆分为独立条目,
/// 先放 runtime 段,再追加 PATH 中不在 runtime 里的段,彻底避免重复。
///
/// 如果 `NUWAX_APP_RUNTIME_PATH` 未设置或为空,直接返回原始 PATH。
pub fn ensure_runtime_path(path: &str) -> String {
if let Ok(runtime_path) = std::env::var("NUWAX_APP_RUNTIME_PATH") {
let runtime_path = runtime_path.trim();
if !runtime_path.is_empty() {
let sep = if cfg!(windows) { ";" } else { ":" };
// 将 runtime_path 拆成各段
let runtime_segments: Vec<&str> =
runtime_path.split(sep).filter(|s| !s.is_empty()).collect();
// 将现有 PATH 拆成各段,去掉已在 runtime 中的
let existing_segments: Vec<&str> = path
.split(sep)
.filter(|s| !s.is_empty() && !runtime_segments.contains(s))
.collect();
let merged: Vec<&str> = runtime_segments
.iter()
.copied()
.chain(existing_segments)
.collect();
let result = merged.join(sep);
if result != path {
tracing::info!(
"[ProcessCompat] Front-end application built-in runtime to PATH: {}",
runtime_path
);
}
return result;
}
}
path.to_string()
}
/// 为 process-wrap 8.x 的 TokioCommandWrap 应用平台特定的包装
///
/// 此宏会根据目标平台自动应用正确的进程包装:
/// - Windows: `CreationFlags(CREATE_NO_WINDOW | CREATE_NEW_PROCESS_GROUP)` + `JobObject`
/// - Unix: `ProcessGroup::leader()`
///
/// # Arguments
///
/// * `$cmd` - 可变的 TokioCommandWrap 实例
///
/// # Example
///
/// ```ignore
/// use process_wrap::tokio::{TokioCommandWrap, KillOnDrop};
/// use mcp_common::process_compat::wrap_process_v8;
///
/// let mut wrapped_cmd = TokioCommandWrap::with_new("node", |cmd| {
/// cmd.arg("server.js");
/// });
/// wrap_process_v8!(wrapped_cmd);
/// wrapped_cmd.wrap(KillOnDrop);
/// ```
#[cfg(unix)]
#[macro_export]
macro_rules! wrap_process_v8 {
($cmd:expr) => {{
use process_wrap::tokio::ProcessGroup;
$cmd.wrap(ProcessGroup::leader());
}};
}
#[cfg(windows)]
#[macro_export]
macro_rules! wrap_process_v8 {
($cmd:expr) => {{
use process_wrap::tokio::{CreationFlags, JobObject};
use windows::Win32::System::Threading::{CREATE_NEW_PROCESS_GROUP, CREATE_NO_WINDOW};
$cmd.wrap(CreationFlags(CREATE_NO_WINDOW | CREATE_NEW_PROCESS_GROUP));
$cmd.wrap(JobObject);
}};
}
/// 为 process-wrap 9.x 的 CommandWrap 应用平台特定的包装
///
/// 此宏会根据目标平台自动应用正确的进程包装:
/// - Windows: `CreationFlags(CREATE_NO_WINDOW | CREATE_NEW_PROCESS_GROUP)` + `JobObject`
/// - Unix: `ProcessGroup::leader()`
///
/// # Arguments
///
/// * `$cmd` - 可变的 CommandWrap 实例
///
/// # Example
///
/// ```ignore
/// use process_wrap::tokio::{CommandWrap, KillOnDrop};
/// use mcp_common::process_compat::wrap_process_v9;
///
/// let mut wrapped_cmd = CommandWrap::with_new("node", |cmd| {
/// cmd.arg("server.js");
/// });
/// wrap_process_v9!(wrapped_cmd);
/// wrapped_cmd.wrap(KillOnDrop);
/// ```
#[cfg(unix)]
#[macro_export]
macro_rules! wrap_process_v9 {
($cmd:expr) => {{
use process_wrap::tokio::ProcessGroup;
$cmd.wrap(ProcessGroup::leader());
}};
}
#[cfg(windows)]
#[macro_export]
macro_rules! wrap_process_v9 {
($cmd:expr) => {{
use process_wrap::tokio::{CreationFlags, JobObject};
use windows::Win32::System::Threading::{CREATE_NEW_PROCESS_GROUP, CREATE_NO_WINDOW};
$cmd.wrap(CreationFlags(CREATE_NO_WINDOW | CREATE_NEW_PROCESS_GROUP));
$cmd.wrap(JobObject);
}};
}
/// 启动 stderr 日志读取任务
///
/// 创建一个异步任务来读取子进程的 stderr 输出并记录到日志。
/// 这个函数封装了通用的 stderr 读取逻辑。
///
/// # Arguments
///
/// * `stderr` - stderr 管道(实现 AsyncRead + Unpin + Send
/// * `service_name` - MCP 服务名称(用于日志标识)
///
/// # Returns
///
/// 返回 `JoinHandle<()>`,任务会在 stderr 关闭时自动结束
///
/// # Example
///
/// ```ignore
/// use mcp_common::process_compat::spawn_stderr_reader;
///
/// let (tokio_process, child_stderr) = TokioChildProcess::builder(wrapped_cmd)
/// .stderr(Stdio::piped())
/// .spawn()?;
///
/// if let Some(stderr) = child_stderr {
/// spawn_stderr_reader(stderr, "my-mcp-service".to_string());
/// }
/// ```
pub fn spawn_stderr_reader<T>(stderr: T, service_name: String) -> tokio::task::JoinHandle<()>
where
T: tokio::io::AsyncRead + Unpin + Send + 'static,
{
tokio::spawn(async move {
use tokio::io::{AsyncBufReadExt, BufReader};
let mut reader = BufReader::new(stderr);
let mut line = String::new();
loop {
line.clear();
match reader.read_line(&mut line).await {
Ok(0) => {
// EOF - stderr 已关闭
tracing::debug!("[Subprocess stderr][{}] End of read (EOF)", service_name);
break;
}
Ok(_) => {
let trimmed = line.trim();
if !trimmed.is_empty() {
tracing::warn!("[child process stderr][{}] {}", service_name, trimmed);
}
}
Err(e) => {
tracing::debug!("[Subprocess stderr][{}] Read error: {}", service_name, e);
break;
}
}
}
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_check_windows_command_non_windows() {
// 在非 Windows 平台上,此函数应该不执行任何操作
check_windows_command("npx some-server");
check_windows_command("test.cmd");
}
#[test]
fn test_ensure_runtime_path_no_env() {
// NUWAX_APP_RUNTIME_PATH 未设置时,返回原始 PATH
unsafe { std::env::remove_var("NUWAX_APP_RUNTIME_PATH") };
let result = ensure_runtime_path("/usr/bin:/usr/local/bin");
assert_eq!(result, "/usr/bin:/usr/local/bin");
}
#[test]
fn test_ensure_runtime_path_prepend() {
unsafe {
std::env::set_var("NUWAX_APP_RUNTIME_PATH", "/app/node/bin:/app/uv/bin");
}
let result = ensure_runtime_path("/usr/bin:/usr/local/bin");
assert_eq!(result, "/app/node/bin:/app/uv/bin:/usr/bin:/usr/local/bin");
unsafe { std::env::remove_var("NUWAX_APP_RUNTIME_PATH") };
}
#[test]
fn test_ensure_runtime_path_dedup() {
// 模拟PATH 中已有 runtime 的部分段 → 不应重复
unsafe {
std::env::set_var("NUWAX_APP_RUNTIME_PATH", "/app/node/bin:/app/uv/bin");
}
let result = ensure_runtime_path("/app/node/bin:/opt/homebrew/bin:/usr/bin");
assert_eq!(
result,
"/app/node/bin:/app/uv/bin:/opt/homebrew/bin:/usr/bin"
);
unsafe { std::env::remove_var("NUWAX_APP_RUNTIME_PATH") };
}
#[test]
fn test_ensure_runtime_path_all_present() {
// PATH 已含全部 runtime 段 → 仅调整顺序确保 runtime 在前
unsafe {
std::env::set_var("NUWAX_APP_RUNTIME_PATH", "/app/node/bin:/app/uv/bin");
}
let result = ensure_runtime_path("/app/uv/bin:/usr/bin:/app/node/bin");
assert_eq!(result, "/app/node/bin:/app/uv/bin:/usr/bin");
unsafe { std::env::remove_var("NUWAX_APP_RUNTIME_PATH") };
}
#[test]
fn test_ensure_runtime_path_double_node() {
// 模拟日志中的问题node/bin 出现两次
unsafe {
std::env::set_var(
"NUWAX_APP_RUNTIME_PATH",
"/app/node/bin:/app/uv/bin:/app/debug",
);
}
let result = ensure_runtime_path(
"/app/node/bin:/app/node/bin:/app/uv/bin:/app/debug:/opt/homebrew/bin",
);
assert_eq!(
result,
"/app/node/bin:/app/uv/bin:/app/debug:/opt/homebrew/bin"
);
unsafe { std::env::remove_var("NUWAX_APP_RUNTIME_PATH") };
}
}

View File

@@ -0,0 +1,189 @@
//! OpenTelemetry 追踪模块
//!
//! 提供统一的分布式追踪初始化接口,支持 OTLP (Jaeger) exporter。
//!
//! # Feature Flags
//! - `telemetry`: 基础 OpenTelemetry 支持
//! - `otlp`: OTLP exporter 支持(用于 Jaeger
//!
//! # 使用示例
//!
//! ```ignore
//! use mcp_common::{TracingConfig, init_tracing};
//!
//! let config = TracingConfig::new("my-service")
//! .with_otlp("http://localhost:4317")
//! .with_version("1.0.0");
//!
//! let _guard = init_tracing(&config)?;
//! // guard 保持存活期间,追踪数据会被发送到 OTLP endpoint
//! ```
use anyhow::Result;
/// 追踪配置
#[derive(Debug, Clone, Default)]
pub struct TracingConfig {
/// 服务名称
pub service_name: String,
/// 服务版本
pub service_version: Option<String>,
/// OTLP 端点 (如 http://localhost:4317)
pub otlp_endpoint: Option<String>,
/// 采样率 (0.0 - 1.0),默认为 1.0(全部采样)
pub sample_ratio: Option<f64>,
}
impl TracingConfig {
/// 创建新的追踪配置
pub fn new(service_name: impl Into<String>) -> Self {
Self {
service_name: service_name.into(),
..Default::default()
}
}
/// 设置 OTLP 端点
pub fn with_otlp(mut self, endpoint: impl Into<String>) -> Self {
self.otlp_endpoint = Some(endpoint.into());
self
}
/// 设置服务版本
pub fn with_version(mut self, version: impl Into<String>) -> Self {
self.service_version = Some(version.into());
self
}
/// 设置采样率
pub fn with_sample_ratio(mut self, ratio: f64) -> Self {
self.sample_ratio = Some(ratio.clamp(0.0, 1.0));
self
}
}
/// 初始化追踪系统(启用 OTLP feature 时)
#[cfg(feature = "otlp")]
pub fn init_tracing(config: &TracingConfig) -> Result<TracingGuard> {
use opentelemetry::KeyValue;
use opentelemetry::global;
use opentelemetry_otlp::WithExportConfig;
use opentelemetry_sdk::Resource;
use opentelemetry_sdk::trace::{Sampler, SdkTracerProvider};
let mut provider_builder = SdkTracerProvider::builder();
// 配置采样率
if let Some(ratio) = config.sample_ratio {
provider_builder = provider_builder.with_sampler(Sampler::TraceIdRatioBased(ratio));
}
// 配置 OTLP exporter
if let Some(endpoint) = &config.otlp_endpoint {
let exporter = opentelemetry_otlp::SpanExporter::builder()
.with_tonic()
.with_endpoint(endpoint)
.build()?;
provider_builder = provider_builder.with_batch_exporter(exporter);
tracing::info!(endpoint = %endpoint, "OTLP exporter configured");
}
// 配置资源属性
let mut attributes = vec![KeyValue::new("service.name", config.service_name.clone())];
if let Some(version) = &config.service_version {
attributes.push(KeyValue::new("service.version", version.clone()));
}
let resource = Resource::builder().with_attributes(attributes).build();
provider_builder = provider_builder.with_resource(resource);
let provider = provider_builder.build();
global::set_tracer_provider(provider.clone());
tracing::info!(
service_name = %config.service_name,
"OpenTelemetry tracer provider initialized"
);
Ok(TracingGuard {
provider: Some(provider),
})
}
/// 无 OTLP 时的空实现
#[cfg(not(feature = "otlp"))]
pub fn init_tracing(_config: &TracingConfig) -> Result<TracingGuard> {
tracing::debug!("OTLP feature not enabled, skipping tracer initialization");
Ok(TracingGuard { provider: None })
}
/// 创建 tracing-opentelemetry layer
///
/// 此 layer 可以添加到 tracing_subscriber 中,将 tracing 的 span 和事件
/// 转发到 OpenTelemetry。
#[cfg(feature = "telemetry")]
pub fn create_otel_layer() -> impl tracing_subscriber::Layer<tracing_subscriber::Registry> {
tracing_opentelemetry::layer()
}
/// 追踪守卫 - Drop 时自动关闭 tracer provider
///
/// 必须保持此守卫存活,否则追踪数据可能不会被正确发送。
pub struct TracingGuard {
#[cfg(feature = "otlp")]
provider: Option<opentelemetry_sdk::trace::SdkTracerProvider>,
#[cfg(not(feature = "otlp"))]
#[allow(dead_code)]
provider: Option<()>,
}
impl TracingGuard {
/// 检查追踪是否已初始化
pub fn is_initialized(&self) -> bool {
self.provider.is_some()
}
}
impl Drop for TracingGuard {
fn drop(&mut self) {
#[cfg(feature = "otlp")]
if let Some(provider) = self.provider.take() {
tracing::info!("Shutting down OpenTelemetry tracer provider");
if let Err(e) = provider.shutdown() {
tracing::warn!("Failed to shutdown tracer provider: {}", e);
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_tracing_config_builder() {
let config = TracingConfig::new("test-service")
.with_otlp("http://localhost:4317")
.with_version("1.0.0")
.with_sample_ratio(0.5);
assert_eq!(config.service_name, "test-service");
assert_eq!(
config.otlp_endpoint,
Some("http://localhost:4317".to_string())
);
assert_eq!(config.service_version, Some("1.0.0".to_string()));
assert_eq!(config.sample_ratio, Some(0.5));
}
#[test]
fn test_sample_ratio_clamping() {
let config = TracingConfig::new("test").with_sample_ratio(1.5);
assert_eq!(config.sample_ratio, Some(1.0));
let config = TracingConfig::new("test").with_sample_ratio(-0.5);
assert_eq!(config.sample_ratio, Some(0.0));
}
}

View File

@@ -0,0 +1,79 @@
//! 工具过滤器
//!
//! 提供白名单和黑名单两种过滤模式
use std::collections::HashSet;
/// 工具过滤配置
#[derive(Clone, Debug, Default, serde::Serialize, serde::Deserialize)]
pub struct ToolFilter {
/// 白名单(只允许这些工具)
pub allow_tools: Option<HashSet<String>>,
/// 黑名单(排除这些工具)
pub deny_tools: Option<HashSet<String>>,
}
impl ToolFilter {
/// 创建白名单过滤器
pub fn allow(tools: Vec<String>) -> Self {
Self {
allow_tools: Some(tools.into_iter().collect()),
deny_tools: None,
}
}
/// 创建黑名单过滤器
pub fn deny(tools: Vec<String>) -> Self {
Self {
allow_tools: None,
deny_tools: Some(tools.into_iter().collect()),
}
}
/// 检查工具是否被允许
pub fn is_allowed(&self, tool_name: &str) -> bool {
// 白名单模式:只有在白名单中的工具才被允许
if let Some(ref allow_list) = self.allow_tools {
return allow_list.contains(tool_name);
}
// 黑名单模式:不在黑名单中的工具都被允许
if let Some(ref deny_list) = self.deny_tools {
return !deny_list.contains(tool_name);
}
// 无过滤:全部允许
true
}
/// 检查是否启用了过滤
pub fn is_enabled(&self) -> bool {
self.allow_tools.is_some() || self.deny_tools.is_some()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_allow_filter() {
let filter = ToolFilter::allow(vec!["tool1".to_string(), "tool2".to_string()]);
assert!(filter.is_allowed("tool1"));
assert!(filter.is_allowed("tool2"));
assert!(!filter.is_allowed("tool3"));
}
#[test]
fn test_deny_filter() {
let filter = ToolFilter::deny(vec!["tool1".to_string()]);
assert!(!filter.is_allowed("tool1"));
assert!(filter.is_allowed("tool2"));
assert!(filter.is_allowed("tool3"));
}
#[test]
fn test_no_filter() {
let filter = ToolFilter::default();
assert!(filter.is_allowed("any_tool"));
assert!(!filter.is_enabled());
}
}