fix: restore mcp flexibility and improve cli tooling

This commit is contained in:
2025-10-11 06:11:22 +02:00
parent 40c44470e8
commit 5ac0d152cb
19 changed files with 998 additions and 162 deletions

View File

@@ -4,10 +4,11 @@
/// Supports switching between local (in-process) and remote (STDIO) execution modes.
use super::client::McpClient;
use super::{remote_client::RemoteMcpClient, LocalMcpClient};
use crate::config::Config;
use crate::config::{Config, McpMode};
use crate::tools::registry::ToolRegistry;
use crate::validation::SchemaValidator;
use crate::Result;
use crate::{Error, Result};
use log::{info, warn};
use std::sync::Arc;
/// Factory for creating MCP clients based on configuration
@@ -30,30 +31,72 @@ impl McpClientFactory {
}
}
/// Create an MCP client based on the current configuration
///
/// In v1.0+, MCP architecture is always enabled. If MCP servers are configured,
/// uses the first server; otherwise falls back to local in-process client.
/// Create an MCP client based on the current configuration.
pub fn create(&self) -> Result<Box<dyn McpClient>> {
// Use the first configured MCP server, if any.
if let Some(server_cfg) = self.config.mcp_servers.first() {
match RemoteMcpClient::new_with_config(server_cfg) {
Ok(client) => Ok(Box::new(client)),
Err(e) => {
eprintln!("Warning: Failed to start remote MCP client '{}': {}. Falling back to local mode.", server_cfg.name, e);
match self.config.mcp.mode {
McpMode::Disabled => Err(Error::Config(
"MCP mode is set to 'disabled'; tooling cannot function in this configuration."
.to_string(),
)),
McpMode::LocalOnly | McpMode::Legacy => {
if matches!(self.config.mcp.mode, McpMode::Legacy) {
warn!("Using deprecated MCP legacy mode; consider switching to 'local_only'.");
}
Ok(Box::new(LocalMcpClient::new(
self.registry.clone(),
self.validator.clone(),
)))
}
McpMode::RemoteOnly => {
let server_cfg = self.config.mcp_servers.first().ok_or_else(|| {
Error::Config(
"MCP mode 'remote_only' requires at least one entry in [[mcp_servers]]"
.to_string(),
)
})?;
RemoteMcpClient::new_with_config(server_cfg)
.map(|client| Box::new(client) as Box<dyn McpClient>)
.map_err(|e| {
Error::Config(format!(
"Failed to start remote MCP client '{}': {e}",
server_cfg.name
))
})
}
McpMode::RemotePreferred => {
if let Some(server_cfg) = self.config.mcp_servers.first() {
match RemoteMcpClient::new_with_config(server_cfg) {
Ok(client) => {
info!(
"Connected to remote MCP server '{}' via {} transport.",
server_cfg.name, server_cfg.transport
);
Ok(Box::new(client) as Box<dyn McpClient>)
}
Err(e) if self.config.mcp.allow_fallback => {
warn!(
"Failed to start remote MCP client '{}': {}. Falling back to local tooling.",
server_cfg.name, e
);
Ok(Box::new(LocalMcpClient::new(
self.registry.clone(),
self.validator.clone(),
)))
}
Err(e) => Err(Error::Config(format!(
"Failed to start remote MCP client '{}': {e}. To allow fallback, set [mcp].allow_fallback = true.",
server_cfg.name
))),
}
} else {
warn!("No MCP servers configured; using local MCP tooling.");
Ok(Box::new(LocalMcpClient::new(
self.registry.clone(),
self.validator.clone(),
)))
}
}
} else {
// No servers configured fall back to local client.
eprintln!("Warning: No MCP servers defined in config. Using local client.");
Ok(Box::new(LocalMcpClient::new(
self.registry.clone(),
self.validator.clone(),
)))
}
}
@@ -66,11 +109,10 @@ impl McpClientFactory {
#[cfg(test)]
mod tests {
use super::*;
use crate::config::McpServerConfig;
use crate::Error;
#[test]
fn test_factory_creates_local_client_when_no_servers_configured() {
let config = Config::default();
fn build_factory(config: Config) -> McpClientFactory {
let ui = Arc::new(crate::ui::NoOpUiController);
let registry = Arc::new(ToolRegistry::new(
Arc::new(tokio::sync::Mutex::new(config.clone())),
@@ -78,10 +120,58 @@ mod tests {
));
let validator = Arc::new(SchemaValidator::new());
let factory = McpClientFactory::new(Arc::new(config), registry, validator);
McpClientFactory::new(Arc::new(config), registry, validator)
}
#[test]
fn test_factory_creates_local_client_when_no_servers_configured() {
let config = Config::default();
let factory = build_factory(config);
// Should create without error and fall back to local client
let result = factory.create();
assert!(result.is_ok());
}
#[test]
fn test_remote_only_without_servers_errors() {
let mut config = Config::default();
config.mcp.mode = McpMode::RemoteOnly;
config.mcp_servers.clear();
let factory = build_factory(config);
let result = factory.create();
assert!(matches!(result, Err(Error::Config(_))));
}
#[test]
fn test_remote_preferred_without_fallback_propagates_remote_error() {
let mut config = Config::default();
config.mcp.mode = McpMode::RemotePreferred;
config.mcp.allow_fallback = false;
config.mcp_servers = vec![McpServerConfig {
name: "invalid".to_string(),
command: "nonexistent-mcp-server-binary".to_string(),
args: Vec::new(),
transport: "stdio".to_string(),
env: std::collections::HashMap::new(),
}];
let factory = build_factory(config);
let result = factory.create();
assert!(
matches!(result, Err(Error::Config(message)) if message.contains("Failed to start remote MCP client"))
);
}
#[test]
fn test_legacy_mode_uses_local_client() {
let mut config = Config::default();
config.mcp.mode = McpMode::Legacy;
let factory = build_factory(config);
let result = factory.create();
assert!(result.is_ok());
}
}