diff --git a/crates/owlen-core/src/config.rs b/crates/owlen-core/src/config.rs index d784992..23ce260 100644 --- a/crates/owlen-core/src/config.rs +++ b/crates/owlen-core/src/config.rs @@ -14,6 +14,9 @@ pub const DEFAULT_CONFIG_PATH: &str = "~/.config/owlen/config.toml"; pub struct Config { /// General application settings pub general: GeneralSettings, + /// MCP (Multi-Client-Provider) settings + #[serde(default)] + pub mcp: McpSettings, /// Provider specific configuration keyed by provider name #[serde(default)] pub providers: HashMap, @@ -48,6 +51,7 @@ impl Default for Config { Self { general: GeneralSettings::default(), + mcp: McpSettings::default(), providers, ui: UiSettings::default(), storage: StorageSettings::default(), @@ -202,6 +206,21 @@ impl Default for GeneralSettings { } } +/// MCP (Multi-Client-Provider) settings +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct McpSettings { + #[serde(default)] + pub mode: McpMode, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)] +#[serde(rename_all = "lowercase")] +pub enum McpMode { + #[default] + Legacy, + Enabled, +} + /// Privacy controls governing network access and storage #[derive(Debug, Clone, Serialize, Deserialize)] pub struct PrivacySettings { diff --git a/crates/owlen-core/src/lib.rs b/crates/owlen-core/src/lib.rs index 378a2ed..168a611 100644 --- a/crates/owlen-core/src/lib.rs +++ b/crates/owlen-core/src/lib.rs @@ -76,4 +76,7 @@ pub enum Error { #[error("Unknown error: {0}")] Unknown(String), + + #[error("Not implemented: {0}")] + NotImplemented(String), } diff --git a/crates/owlen-core/src/mcp/client.rs b/crates/owlen-core/src/mcp/client.rs new file mode 100644 index 0000000..281daf5 --- /dev/null +++ b/crates/owlen-core/src/mcp/client.rs @@ -0,0 +1,33 @@ +use super::{McpToolCall, McpToolDescriptor, McpToolResponse}; +use crate::{Error, Result}; +use async_trait::async_trait; + +/// Trait for a client that can interact with an MCP server +#[async_trait] +pub trait McpClient: Send + Sync { + /// List the tools available on the server + async fn list_tools(&self) -> Result>; + + /// Call a tool on the server + async fn call_tool(&self, call: McpToolCall) -> Result; +} + +/// Placeholder for a client that connects to a remote MCP server. +pub struct RemoteMcpClient; + +#[async_trait] +impl McpClient for RemoteMcpClient { + async fn list_tools(&self) -> Result> { + // TODO: Implement remote call + Err(Error::NotImplemented( + "Remote MCP client is not implemented".to_string(), + )) + } + + async fn call_tool(&self, _call: McpToolCall) -> Result { + // TODO: Implement remote call + Err(Error::NotImplemented( + "Remote MCP client is not implemented".to_string(), + )) + } +} diff --git a/crates/owlen-core/src/mcp/mod.rs b/crates/owlen-core/src/mcp/mod.rs index 718d206..068a6e1 100644 --- a/crates/owlen-core/src/mcp/mod.rs +++ b/crates/owlen-core/src/mcp/mod.rs @@ -1,12 +1,16 @@ use crate::tools::registry::ToolRegistry; use crate::validation::SchemaValidator; use crate::Result; +use async_trait::async_trait; +use client::McpClient; use serde::{Deserialize, Serialize}; use serde_json::Value; use std::collections::HashMap; use std::sync::Arc; use std::time::Duration; +pub mod client; + /// Descriptor for a tool exposed over MCP #[derive(Debug, Clone, Serialize, Deserialize)] pub struct McpToolDescriptor { @@ -80,3 +84,26 @@ impl McpServer { fn duration_to_millis(duration: Duration) -> u128 { duration.as_secs() as u128 * 1_000 + u128::from(duration.subsec_millis()) } + +pub struct LocalMcpClient { + server: McpServer, +} + +impl LocalMcpClient { + pub fn new(registry: Arc, validator: Arc) -> Self { + Self { + server: McpServer::new(registry, validator), + } + } +} + +#[async_trait] +impl McpClient for LocalMcpClient { + async fn list_tools(&self) -> Result> { + Ok(self.server.list_tools()) + } + + async fn call_tool(&self, call: McpToolCall) -> Result { + self.server.call_tool(call).await + } +} diff --git a/crates/owlen-core/src/session.rs b/crates/owlen-core/src/session.rs index 003452c..835f7af 100644 --- a/crates/owlen-core/src/session.rs +++ b/crates/owlen-core/src/session.rs @@ -1,10 +1,12 @@ -use crate::config::Config; +use crate::config::{Config, McpMode}; use crate::consent::ConsentManager; use crate::conversation::ConversationManager; use crate::credentials::CredentialManager; use crate::encryption::{self, VaultHandle}; use crate::formatting::MessageFormatter; use crate::input::InputBuffer; +use crate::mcp::client::{McpClient, RemoteMcpClient}; +use crate::mcp::{LocalMcpClient, McpToolCall}; use crate::model::ModelManager; use crate::provider::{ChatStream, Provider}; use crate::storage::{SessionMeta, StorageManager}; @@ -102,6 +104,7 @@ pub struct SessionController { consent_manager: Arc>, tool_registry: Arc, schema_validator: Arc, + mcp_client: Arc, storage: Arc, vault: Option>>, master_key: Option>>, @@ -109,6 +112,83 @@ pub struct SessionController { enable_code_tools: bool, // Whether to enable code execution tools (code client only) } +fn build_tools( + config: &Config, + enable_code_tools: bool, + consent_manager: Arc>, + credential_manager: Option>, + vault: Option>>, +) -> Result<(Arc, Arc)> { + let mut registry = ToolRegistry::new(); + let mut validator = SchemaValidator::new(); + + for (name, schema) in get_builtin_schemas() { + if let Err(err) = validator.register_schema(&name, schema) { + warn!("Failed to register built-in schema {name}: {err}"); + } + } + + if config + .security + .allowed_tools + .iter() + .any(|tool| tool == "web_search") + && config.tools.web_search.enabled + && config.privacy.enable_remote_search + { + let tool = WebSearchTool::new( + consent_manager.clone(), + credential_manager.clone(), + vault.clone(), + ); + let schema = tool.schema(); + if let Err(err) = validator.register_schema(tool.name(), schema) { + warn!("Failed to register schema for {}: {err}", tool.name()); + } + registry.register(tool); + } + + // Register web_search_detailed tool (provides snippets) + if config + .security + .allowed_tools + .iter() + .any(|tool| tool == "web_search") // Same permission as web_search + && config.tools.web_search.enabled + && config.privacy.enable_remote_search + { + let tool = WebSearchDetailedTool::new( + consent_manager.clone(), + credential_manager.clone(), + vault.clone(), + ); + let schema = tool.schema(); + if let Err(err) = validator.register_schema(tool.name(), schema) { + warn!("Failed to register schema for {}: {err}", tool.name()); + } + registry.register(tool); + } + + // Code execution tool - only available in code client + if enable_code_tools + && config + .security + .allowed_tools + .iter() + .any(|tool| tool == "code_exec") + && config.tools.code_exec.enabled + { + let tool = CodeExecTool::new(config.tools.code_exec.allowed_languages.clone()); + let schema = tool.schema(); + if let Err(err) = validator.register_schema(tool.name(), schema) { + warn!("Failed to register schema for {}: {err}", tool.name()); + } + registry.register(tool); + } + + Ok((Arc::new(registry), Arc::new(validator))) +} + impl SessionController { /// Create a new controller with the given provider and configuration /// @@ -175,7 +255,23 @@ impl SessionController { let model_manager = ModelManager::new(config.general.model_cache_ttl()); - let mut controller = Self { + let (tool_registry, schema_validator) = build_tools( + &config, + enable_code_tools, + consent_manager.clone(), + credential_manager.clone(), + vault_handle.clone(), + )?; + + let mcp_client: Arc = match config.mcp.mode { + McpMode::Legacy => Arc::new(LocalMcpClient::new( + tool_registry.clone(), + schema_validator.clone(), + )), + McpMode::Enabled => Arc::new(RemoteMcpClient {}), + }; + + let controller = Self { provider, conversation, model_manager, @@ -183,8 +279,9 @@ impl SessionController { formatter, config, consent_manager, - tool_registry: Arc::new(ToolRegistry::new()), - schema_validator: Arc::new(SchemaValidator::new()), + tool_registry, + schema_validator, + mcp_client, storage, vault: vault_handle, master_key, @@ -192,8 +289,6 @@ impl SessionController { enable_code_tools, }; - controller.rebuild_tools()?; - Ok(controller) } @@ -416,78 +511,24 @@ impl SessionController { } fn rebuild_tools(&mut self) -> Result<()> { - let mut registry = ToolRegistry::new(); - let mut validator = SchemaValidator::new(); + let (registry, validator) = build_tools( + &self.config, + self.enable_code_tools, + self.consent_manager.clone(), + self.credential_manager.clone(), + self.vault.clone(), + )?; + self.tool_registry = registry; + self.schema_validator = validator; - for (name, schema) in get_builtin_schemas() { - if let Err(err) = validator.register_schema(&name, schema) { - warn!("Failed to register built-in schema {name}: {err}"); - } - } + self.mcp_client = match self.config.mcp.mode { + McpMode::Legacy => Arc::new(LocalMcpClient::new( + self.tool_registry.clone(), + self.schema_validator.clone(), + )), + McpMode::Enabled => Arc::new(RemoteMcpClient {}), + }; - if self - .config - .security - .allowed_tools - .iter() - .any(|tool| tool == "web_search") - && self.config.tools.web_search.enabled - && self.config.privacy.enable_remote_search - { - let tool = WebSearchTool::new( - self.consent_manager.clone(), - self.credential_manager.clone(), - self.vault.clone(), - ); - let schema = tool.schema(); - if let Err(err) = validator.register_schema(tool.name(), schema) { - warn!("Failed to register schema for {}: {err}", tool.name()); - } - registry.register(tool); - } - - // Register web_search_detailed tool (provides snippets) - if self - .config - .security - .allowed_tools - .iter() - .any(|tool| tool == "web_search") // Same permission as web_search - && self.config.tools.web_search.enabled - && self.config.privacy.enable_remote_search - { - let tool = WebSearchDetailedTool::new( - self.consent_manager.clone(), - self.credential_manager.clone(), - self.vault.clone(), - ); - let schema = tool.schema(); - if let Err(err) = validator.register_schema(tool.name(), schema) { - warn!("Failed to register schema for {}: {err}", tool.name()); - } - registry.register(tool); - } - - // Code execution tool - only available in code client - if self.enable_code_tools - && self - .config - .security - .allowed_tools - .iter() - .any(|tool| tool == "code_exec") - && self.config.tools.code_exec.enabled - { - let tool = CodeExecTool::new(self.config.tools.code_exec.allowed_languages.clone()); - let schema = tool.schema(); - if let Err(err) = validator.register_schema(tool.name(), schema) { - warn!("Failed to register schema for {}: {err}", tool.name()); - } - registry.register(tool); - } - - self.tool_registry = Arc::new(registry); - self.schema_validator = Arc::new(validator); Ok(()) } @@ -592,10 +633,13 @@ impl SessionController { // Execute each tool call if let Some(tool_calls) = &response.message.tool_calls { for tool_call in tool_calls { - let tool_result = self - .tool_registry - .execute(&tool_call.name, tool_call.arguments.clone()) - .await; + let mcp_tool_call = McpToolCall { + name: tool_call.name.clone(), + arguments: tool_call.arguments.clone(), + }; + + let tool_result = + self.mcp_client.call_tool(mcp_tool_call).await; let tool_response_content = match tool_result { Ok(result) => serde_json::to_string_pretty(&result.output) @@ -694,10 +738,11 @@ impl SessionController { ) -> Result { // Execute each tool call for tool_call in &tool_calls { - let tool_result = self - .tool_registry - .execute(&tool_call.name, tool_call.arguments.clone()) - .await; + let mcp_tool_call = McpToolCall { + name: tool_call.name.clone(), + arguments: tool_call.arguments.clone(), + }; + let tool_result = self.mcp_client.call_tool(mcp_tool_call).await; let tool_response_content = match tool_result { Ok(result) => serde_json::to_string_pretty(&result.output)