Skip to main content

mcp_utils/client/
manager.rs

1use llm::ToolDefinition;
2
3use super::{
4    McpError, Result,
5    config::McpServer,
6    connection::{
7        ConnectConfig, McpConnectAttempt, McpConnectOutcome, McpServerConnection, Tool, authenticate_http,
8        connect_server,
9    },
10    mcp_client::McpClient,
11    naming::{create_namespaced_tool_name, split_on_server_name},
12    tool_proxy::ToolProxy,
13};
14use aether_auth::{OAuthCredentialStorage, OAuthHandler};
15use futures::future::join_all;
16use rmcp::{
17    RoleClient,
18    model::{
19        CallToolRequestParams, ClientCapabilities, ClientInfo, CreateElicitationRequestParams, CreateElicitationResult,
20        ElicitationAction, FormElicitationCapability, Implementation, Tool as RmcpTool, UrlElicitationCapability,
21    },
22    service::RunningService,
23    transport::streamable_http_client::StreamableHttpClientTransportConfig,
24};
25use serde::{Deserialize, Serialize};
26use serde_json::Value;
27use std::collections::{BTreeMap, HashMap, HashSet};
28use std::future::Future;
29use std::path::PathBuf;
30use std::sync::Arc;
31use tokio::sync::{mpsc, oneshot};
32
33pub use crate::status::{McpServerAuthCapability, McpServerStatus, McpServerStatusEntry};
34
35pub const DEFAULT_PROXY_NAME: &str = "proxy";
36
37pub type OAuthHandlerFactory = Arc<dyn Fn(OAuthHandlerContext) -> Result<Arc<dyn OAuthHandler>> + Send + Sync>;
38
39/// Context passed to an `OAuthHandlerFactory` so the constructed handler can
40/// dispatch user-facing prompts back to the host through the MCP event channel.
41#[derive(Clone)]
42pub struct OAuthHandlerContext {
43    pub server_name: String,
44    pub tx: mpsc::Sender<McpClientEvent>,
45}
46
47#[derive(Debug)]
48pub struct ElicitationRequest {
49    pub server_name: String,
50    pub request: CreateElicitationRequestParams,
51    pub response_sender: oneshot::Sender<CreateElicitationResult>,
52}
53
54#[derive(Debug, Clone)]
55pub struct ElicitationResponse {
56    pub action: ElicitationAction,
57    pub content: Option<Value>,
58}
59
60#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
61pub struct UrlElicitationCompleteParams {
62    pub server_name: String,
63    pub elicitation_id: String,
64}
65
66/// Events emitted by MCP clients that require attention from the host
67/// (e.g. the relay or TUI). Flows through a single channel from `McpManager`
68/// to the consumer.
69#[derive(Debug)]
70pub enum McpClientEvent {
71    Elicitation(ElicitationRequest),
72    UrlElicitationComplete(UrlElicitationCompleteParams),
73    ServerStatusesChanged(Vec<McpServerStatusEntry>),
74    ToolDefinitionsChanged(Vec<ToolDefinition>),
75    ServerInstructionsUpdated { server: String, instructions: Option<String> },
76    AuthenticationFailed { server: String, error: String },
77    ConnectionReady(McpConnectionDetails),
78}
79
80#[derive(Debug, Clone)]
81pub struct McpConnectionDetails {
82    pub instructions: BTreeMap<String, String>,
83    pub tool_definitions: Vec<ToolDefinition>,
84    pub server_statuses: Vec<McpServerStatusEntry>,
85}
86
87/// Manages connections to multiple MCP servers and their tools
88pub struct McpManager {
89    servers: HashMap<String, ServerRecord>,
90    server_order: Vec<String>,
91    proxy: Option<ToolProxy>,
92    aether_home: Option<PathBuf>,
93    client_info: ClientInfo,
94    event_sender: mpsc::Sender<McpClientEvent>,
95    root_dir: PathBuf,
96    oauth_handler_factory: Option<OAuthHandlerFactory>,
97    oauth_credential_store: Option<Arc<dyn OAuthCredentialStorage>>,
98}
99
100impl McpManager {
101    pub fn new(event_sender: mpsc::Sender<McpClientEvent>, oauth_handler_factory: Option<OAuthHandlerFactory>) -> Self {
102        let mut capabilities = ClientCapabilities::builder().enable_elicitation().build();
103        if let Some(elicitation) = capabilities.elicitation.as_mut() {
104            elicitation.form = Some(FormElicitationCapability::default());
105            elicitation.url = Some(UrlElicitationCapability::default());
106        }
107
108        Self {
109            servers: HashMap::new(),
110            server_order: Vec::new(),
111            proxy: None,
112            aether_home: None,
113            client_info: ClientInfo::new(capabilities, Implementation::new("aether", "0.1.0")),
114            event_sender,
115            root_dir: std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")),
116            oauth_handler_factory,
117            oauth_credential_store: None,
118        }
119    }
120
121    pub fn with_aether_home(mut self, aether_home: impl Into<PathBuf>) -> Self {
122        self.aether_home = Some(aether_home.into());
123        self
124    }
125
126    pub fn with_oauth_credential_store(mut self, store: Arc<dyn OAuthCredentialStorage>) -> Self {
127        self.oauth_credential_store = Some(store);
128        self
129    }
130
131    pub fn with_root_dir(mut self, root_dir: impl Into<PathBuf>) -> Self {
132        self.root_dir = root_dir.into();
133        self
134    }
135
136    pub async fn register_pending(&mut self, servers: Vec<McpServer>) -> Result<Vec<McpServer>> {
137        let has_proxy = servers.iter().any(|server| server.proxy);
138        if has_proxy && servers.iter().any(|server| server.name == DEFAULT_PROXY_NAME) {
139            return Err(McpError::Other("server name 'proxy' collides with the tool proxy".into()));
140        }
141
142        for server in &servers {
143            self.register_record(&server.name, ServerState::Connecting, None, server.proxy);
144        }
145
146        self.emit_server_statuses_changed().await;
147        Ok(servers)
148    }
149
150    pub async fn bootstrap_proxy_setup(&mut self, servers: &[McpServer]) -> Result<()> {
151        let proxied_members: HashSet<String> =
152            servers.iter().filter(|server| server.proxy).map(|server| server.name.clone()).collect();
153
154        if proxied_members.is_empty() {
155            return Ok(());
156        }
157
158        let dir = self.proxy_tool_dir()?;
159        ToolProxy::clean_dir(&dir).await?;
160        self.register_proxy(dir, proxied_members);
161        Ok(())
162    }
163
164    pub fn connect_pending_task(&self, server: McpServer) -> impl Future<Output = McpConnectAttempt> + Send + 'static {
165        let ctx = self.connect_config();
166        async move { connect_server(server, &ctx).await }
167    }
168
169    pub async fn add_mcps(&mut self, servers: Vec<McpServer>) -> Result<()> {
170        self.bootstrap_proxy_setup(&servers).await?;
171        let pending = self.register_pending(servers).await?;
172        let ctx = self.connect_config();
173        let attempts = join_all(pending.into_iter().map(|server| connect_server(server, &ctx))).await;
174        for attempt in attempts {
175            self.apply_connection_attempt(attempt).await;
176        }
177        Ok(())
178    }
179
180    pub fn get_client_for_tool(
181        &self,
182        namespaced_tool_name: &str,
183        arguments_json: &str,
184    ) -> Result<(Arc<RunningService<RoleClient, McpClient>>, CallToolRequestParams)> {
185        if !self.is_routable_tool(namespaced_tool_name) {
186            return Err(McpError::ToolNotFound(namespaced_tool_name.to_string()));
187        }
188
189        let (server_name, tool_name) = split_on_server_name(namespaced_tool_name)
190            .ok_or_else(|| McpError::InvalidToolNameFormat(namespaced_tool_name.to_string()))?;
191
192        if let Some(proxy) = self.proxy.as_ref().filter(|proxy| proxy.name() == server_name) {
193            let call = proxy.resolve_call(arguments_json)?;
194            let conn = self.connection_for(&call.server).ok_or_else(|| {
195                McpError::ServerNotFound(format!("Proxied server '{}' is not connected", call.server))
196            })?;
197            let params = CallToolRequestParams::new(call.tool).with_arguments(call.arguments.unwrap_or_default());
198            return Ok((conn.client.clone(), params));
199        }
200
201        let client =
202            self.client_for_server(server_name).ok_or_else(|| McpError::ServerNotFound(server_name.to_string()))?;
203
204        let arguments = serde_json::from_str::<serde_json::Value>(arguments_json)?.as_object().cloned();
205        let mut params = CallToolRequestParams::new(tool_name.to_string());
206        if let Some(args) = arguments {
207            params = params.with_arguments(args);
208        }
209
210        Ok((client, params))
211    }
212
213    pub fn tool_definitions(&self) -> Vec<ToolDefinition> {
214        let mut definitions = Vec::new();
215        if let Some(proxy) = self.proxy.as_ref() {
216            definitions.push(ToolProxy::call_tool_definition(proxy.name()));
217        }
218        for name in &self.server_order {
219            let Some(record) = self.servers.get(name) else { continue };
220            if record.proxied {
221                continue;
222            }
223            definitions.extend(record.tools().iter().map(|tool| {
224                ToolDefinition::new(
225                    create_namespaced_tool_name(name, &tool.name),
226                    tool.description.clone(),
227                    tool.parameters.to_string(),
228                )
229                .with_server(name.clone())
230                .with_annotations(tool.annotations.clone())
231            }));
232        }
233        definitions
234    }
235
236    pub fn server_instructions(&self) -> BTreeMap<String, String> {
237        let mut instructions: BTreeMap<String, String> = self
238            .servers
239            .iter()
240            .filter(|(name, _)| self.proxy.as_ref().is_none_or(|proxy| !proxy.contains_server(name)))
241            .filter_map(|(name, record)| {
242                record
243                    .connection()
244                    .and_then(|conn| conn.instructions.as_ref())
245                    .map(|instr| (name.clone(), instr.clone()))
246            })
247            .collect();
248
249        if let Some((name, body)) = self.proxy_instructions() {
250            instructions.insert(name, body);
251        }
252
253        instructions
254    }
255
256    pub fn server_statuses(&self) -> Vec<McpServerStatusEntry> {
257        self.server_order
258            .iter()
259            .filter_map(|name| self.servers.get(name).map(|record| record.status_entry(name)))
260            .collect()
261    }
262
263    pub async fn authenticate_server_task(
264        &mut self,
265        name: &str,
266    ) -> Result<impl Future<Output = McpConnectAttempt> + Send + 'static> {
267        let record = self
268            .servers
269            .get(name)
270            .ok_or_else(|| McpError::ConnectionFailed(format!("server '{name}' is not OAuth-authenticatable")))?;
271        if !record.can_authenticate() {
272            return Err(McpError::ConnectionFailed(format!("server '{name}' is not OAuth-authenticatable")));
273        }
274        if self.oauth_handler_factory.is_none() {
275            return Err(McpError::ConnectionFailed(format!("No OAuth handler factory available for '{name}'")));
276        }
277
278        let name = name.to_string();
279        let config = record.reauth_config.clone().expect("checked above");
280        let proxied = record.proxied;
281        let ctx = self.connect_config();
282
283        self.set_state(&name, ServerState::Authenticating);
284        self.emit_server_statuses_changed().await;
285
286        Ok(async move { authenticate_http(name, config, ctx, proxied).await })
287    }
288
289    pub async fn apply_connection_attempt(&mut self, attempt: McpConnectAttempt) {
290        let McpConnectAttempt { name, proxied, outcome } = attempt;
291        match outcome {
292            McpConnectOutcome::Connected { conn, reauth_config } => {
293                match self.register_connection(&name, conn, reauth_config, proxied).await {
294                    Ok(tools) => {
295                        self.refresh_proxy_after_auth(&name, &tools, proxied).await;
296                        self.emit_server_statuses_changed().await;
297                        self.emit_tool_definitions_changed().await;
298                        self.emit_instructions_after_connect(&name, proxied).await;
299                    }
300                    Err(error) => self.apply_authentication_failure(name, error.to_string()).await,
301                }
302            }
303            McpConnectOutcome::NeedsOAuth { config, error } => {
304                tracing::warn!("Server '{name}' needs OAuth: {error}");
305                self.register_record(&name, ServerState::NeedsOAuth, Some(config), proxied);
306                self.emit_server_statuses_changed().await;
307            }
308            McpConnectOutcome::Failed { error } => {
309                self.apply_authentication_failure(name, error.to_string()).await;
310            }
311        }
312    }
313
314    /// List all prompts from all connected MCP servers with namespacing
315    pub async fn list_prompts(&self) -> Result<Vec<rmcp::model::Prompt>> {
316        let futures: Vec<_> = self
317            .servers
318            .iter()
319            .filter_map(|(server_name, record)| {
320                let conn = record.connection()?;
321                conn.client.peer_info()?.capabilities.prompts.as_ref()?;
322                let server_name = server_name.clone();
323                let client = conn.client.clone();
324                Some(async move {
325                    let prompts_response = client.list_prompts(None).await.map_err(|e| {
326                        McpError::PromptListFailed(format!("Failed to list prompts for {server_name}: {e}"))
327                    })?;
328
329                    let namespaced_prompts: Vec<rmcp::model::Prompt> = prompts_response
330                        .prompts
331                        .into_iter()
332                        .map(|prompt| {
333                            let namespaced_name = create_namespaced_tool_name(&server_name, &prompt.name);
334                            rmcp::model::Prompt::new(namespaced_name, prompt.description, prompt.arguments)
335                        })
336                        .collect();
337
338                    Ok::<_, McpError>(namespaced_prompts)
339                })
340            })
341            .collect();
342
343        let results = join_all(futures).await;
344        let mut all_prompts = Vec::new();
345        for result in results {
346            all_prompts.extend(result?);
347        }
348
349        Ok(all_prompts)
350    }
351
352    /// Get a specific prompt by namespaced name
353    pub async fn get_prompt(
354        &self,
355        namespaced_prompt_name: &str,
356        arguments: Option<serde_json::Map<String, serde_json::Value>>,
357    ) -> Result<rmcp::model::GetPromptResult> {
358        let (server_name, prompt_name) = split_on_server_name(namespaced_prompt_name)
359            .ok_or_else(|| McpError::InvalidToolNameFormat(namespaced_prompt_name.to_string()))?;
360
361        let server_conn =
362            self.connection_for(server_name).ok_or_else(|| McpError::ServerNotFound(server_name.to_string()))?;
363
364        let mut request = rmcp::model::GetPromptRequestParams::new(prompt_name);
365        if let Some(args) = arguments {
366            request = request.with_arguments(args);
367        }
368
369        server_conn.client.get_prompt(request).await.map_err(|e| {
370            McpError::PromptGetFailed(format!("Failed to get prompt '{prompt_name}' from {server_name}: {e}"))
371        })
372    }
373
374    /// Shutdown all servers and wait for their tasks to complete
375    pub async fn shutdown(&mut self) {
376        let servers: Vec<(String, ServerRecord)> = self.servers.drain().collect();
377
378        for (server_name, record) in servers {
379            if let Some(conn) = record.into_connection()
380                && let Some(handle) = conn.server_task
381            {
382                drop(conn.client);
383
384                match tokio::time::timeout(std::time::Duration::from_secs(5), handle).await {
385                    Ok(Ok(())) => {
386                        tracing::info!("Server '{server_name}' shut down gracefully");
387                    }
388                    Ok(Err(e)) => {
389                        tracing::warn!("Server '{server_name}' task panicked: {e:?}");
390                    }
391                    Err(_) => {
392                        tracing::warn!("Server '{server_name}' shutdown timed out");
393                    }
394                }
395            }
396        }
397
398        self.server_order.clear();
399        self.proxy = None;
400    }
401
402    /// Shutdown a specific server by name
403    pub async fn shutdown_server(&mut self, server_name: &str) -> Result<()> {
404        if let Some(record) = self.servers.remove(server_name)
405            && let Some(conn) = record.into_connection()
406            && let Some(handle) = conn.server_task
407        {
408            drop(conn.client);
409
410            match tokio::time::timeout(std::time::Duration::from_secs(5), handle).await {
411                Ok(Ok(())) => {
412                    tracing::info!("Server '{server_name}' shut down gracefully");
413                }
414                Ok(Err(e)) => {
415                    tracing::warn!("Server '{server_name}' task panicked: {e:?}");
416                }
417                Err(_) => {
418                    tracing::warn!("Server '{server_name}' shutdown timed out");
419                }
420            }
421        }
422
423        Ok(())
424    }
425
426    async fn emit_server_statuses_changed(&self) {
427        self.emit_event(McpClientEvent::ServerStatusesChanged(self.server_statuses())).await;
428    }
429
430    async fn emit_tool_definitions_changed(&self) {
431        self.emit_event(McpClientEvent::ToolDefinitionsChanged(self.tool_definitions())).await;
432    }
433
434    async fn emit_instructions_after_connect(&self, server_name: &str, proxied: bool) {
435        if proxied {
436            if let Some((server, body)) = self.proxy_instructions() {
437                self.emit_event(McpClientEvent::ServerInstructionsUpdated { server, instructions: Some(body) }).await;
438            }
439            return;
440        }
441
442        if let Some(instructions) =
443            self.connection_for(server_name).and_then(|conn| conn.instructions.as_ref()).cloned()
444        {
445            self.emit_event(McpClientEvent::ServerInstructionsUpdated {
446                server: server_name.to_string(),
447                instructions: Some(instructions),
448            })
449            .await;
450        }
451    }
452
453    fn proxy_instructions(&self) -> Option<(String, String)> {
454        let proxy = self.proxy.as_ref()?;
455        let descriptions: Vec<(String, String)> = proxy
456            .members()
457            .iter()
458            .filter_map(|member| {
459                let conn = self.connection_for(member)?;
460                Some((member.clone(), ToolProxy::extract_server_description(&conn.client, member)))
461            })
462            .collect();
463        Some((proxy.name().to_string(), ToolProxy::build_instructions(proxy.tool_dir(), &descriptions)))
464    }
465
466    pub async fn emit_connection_ready(&self) {
467        self.emit_event(McpClientEvent::ConnectionReady(McpConnectionDetails {
468            tool_definitions: self.tool_definitions(),
469            instructions: self.server_instructions(),
470            server_statuses: self.server_statuses(),
471        }))
472        .await;
473    }
474
475    async fn emit_authentication_failed(&self, server: String, error: String) {
476        self.emit_event(McpClientEvent::AuthenticationFailed { server, error }).await;
477    }
478
479    async fn emit_event(&self, event: McpClientEvent) {
480        if let Err(e) = self.event_sender.send(event).await {
481            tracing::warn!("Failed to emit MCP client event: {e}");
482        }
483    }
484
485    fn connect_config(&self) -> Arc<ConnectConfig> {
486        Arc::new(ConnectConfig {
487            client_info: self.client_info.clone(),
488            event_sender: self.event_sender.clone(),
489            root_dir: self.root_dir.clone(),
490            oauth_handler_factory: self.oauth_handler_factory.clone(),
491            oauth_credential_store: self.oauth_credential_store.clone(),
492        })
493    }
494
495    fn proxy_tool_dir(&self) -> Result<PathBuf> {
496        self.aether_home
497            .as_ref()
498            .map(|home| ToolProxy::dir_in_home(home, DEFAULT_PROXY_NAME))
499            .map_or_else(|| ToolProxy::dir(DEFAULT_PROXY_NAME), Ok)
500    }
501
502    async fn register_connection(
503        &mut self,
504        name: &str,
505        conn: McpServerConnection,
506        reauth_config: Option<StreamableHttpClientTransportConfig>,
507        proxied: bool,
508    ) -> Result<Vec<RmcpTool>> {
509        let tools = conn
510            .list_tools()
511            .await
512            .map_err(|e| McpError::ToolDiscoveryFailed(format!("Failed to list tools for {name}: {e}")))?;
513        self.apply_connected(name, conn, &tools, reauth_config, proxied);
514        Ok(tools)
515    }
516
517    fn apply_connected(
518        &mut self,
519        name: &str,
520        conn: McpServerConnection,
521        tools: &[RmcpTool],
522        reauth_config: Option<StreamableHttpClientTransportConfig>,
523        proxied: bool,
524    ) {
525        let existing_reauth = self.servers.get(name).and_then(|r| r.reauth_config.clone());
526        let final_reauth = reauth_config.or(existing_reauth);
527        let tools = tools.iter().map(Tool::from).collect();
528
529        self.remember_server_order(name);
530        self.servers.insert(name.to_string(), ServerRecord::connected(conn, tools, final_reauth, proxied));
531    }
532
533    fn register_proxy(&mut self, tool_dir: std::path::PathBuf, members: HashSet<String>) {
534        self.proxy = Some(ToolProxy::new(DEFAULT_PROXY_NAME.to_string(), members, tool_dir));
535    }
536
537    async fn refresh_proxy_after_auth(&mut self, name: &str, tools: &[RmcpTool], proxied: bool) {
538        if !proxied {
539            return;
540        }
541
542        if let Some(proxy) = self.proxy.as_mut() {
543            proxy.add_member(name.to_string());
544        }
545
546        if let Some(tool_dir) = self.proxy.as_ref().map(|proxy| proxy.tool_dir().to_path_buf())
547            && let Err(e) = ToolProxy::write_tool_entries_to_dir(name, tools, &tool_dir).await
548        {
549            tracing::warn!("Failed to write tool files for '{name}' after OAuth: {e}");
550        }
551    }
552
553    fn remember_server_order(&mut self, name: &str) {
554        if !self.server_order.iter().any(|n| n == name) {
555            self.server_order.push(name.to_string());
556        }
557    }
558
559    async fn apply_authentication_failure(&mut self, name: String, error: String) {
560        self.set_state(&name, ServerState::Failed { error: error.clone() });
561        self.emit_server_statuses_changed().await;
562        self.emit_authentication_failed(name, error).await;
563    }
564
565    fn set_state(&mut self, name: &str, state: ServerState) {
566        self.remember_server_order(name);
567        match self.servers.get_mut(name) {
568            Some(record) => record.state = state,
569            None => {
570                self.servers.insert(name.to_string(), ServerRecord::new(state, None, false));
571            }
572        }
573    }
574
575    fn register_record(
576        &mut self,
577        name: &str,
578        state: ServerState,
579        reauth_config: Option<StreamableHttpClientTransportConfig>,
580        proxied: bool,
581    ) {
582        self.remember_server_order(name);
583        self.servers.insert(name.to_string(), ServerRecord::new(state, reauth_config, proxied));
584    }
585
586    fn connection_for(&self, server_name: &str) -> Option<&McpServerConnection> {
587        self.servers.get(server_name).and_then(ServerRecord::connection)
588    }
589
590    fn client_for_server(&self, server_name: &str) -> Option<Arc<RunningService<RoleClient, McpClient>>> {
591        self.connection_for(server_name).map(|conn| conn.client.clone())
592    }
593
594    fn is_routable_tool(&self, namespaced_tool_name: &str) -> bool {
595        if self.proxy.as_ref().is_some_and(|proxy| proxy.call_tool_name() == namespaced_tool_name) {
596            return true;
597        }
598        match split_on_server_name(namespaced_tool_name) {
599            Some((server_name, tool_name)) => {
600                self.servers.get(server_name).is_some_and(|record| !record.proxied && record.has_tool(tool_name))
601            }
602            None => false,
603        }
604    }
605}
606
607impl Drop for McpManager {
608    fn drop(&mut self) {
609        let servers: Vec<(String, ServerRecord)> = self.servers.drain().collect();
610        for (server_name, record) in servers {
611            if let Some(conn) = record.into_connection()
612                && let Some(handle) = conn.server_task
613            {
614                handle.abort();
615                tracing::warn!("Server '{server_name}' task aborted during cleanup");
616            }
617        }
618    }
619}
620
621/// Internal record holding all mutable state for a single MCP server.
622struct ServerRecord {
623    state: ServerState,
624    reauth_config: Option<StreamableHttpClientTransportConfig>,
625    proxied: bool,
626}
627
628enum ServerState {
629    Connecting,
630    Connected { connection: McpServerConnection, tools: Vec<Tool> },
631    Authenticating,
632    Failed { error: String },
633    NeedsOAuth,
634}
635
636impl From<&ServerState> for McpServerStatus {
637    fn from(state: &ServerState) -> Self {
638        match state {
639            ServerState::Connecting => Self::Connecting,
640            ServerState::Connected { tools, .. } => Self::Connected { tool_count: tools.len() },
641            ServerState::Authenticating => Self::Authenticating,
642            ServerState::Failed { error } => Self::Failed { error: error.clone() },
643            ServerState::NeedsOAuth => Self::NeedsOAuth,
644        }
645    }
646}
647
648impl ServerRecord {
649    fn new(state: ServerState, reauth_config: Option<StreamableHttpClientTransportConfig>, proxied: bool) -> Self {
650        Self { state, reauth_config, proxied }
651    }
652
653    fn connected(
654        connection: McpServerConnection,
655        tools: Vec<Tool>,
656        reauth_config: Option<StreamableHttpClientTransportConfig>,
657        proxied: bool,
658    ) -> Self {
659        Self::new(ServerState::Connected { connection, tools }, reauth_config, proxied)
660    }
661
662    fn tools(&self) -> &[Tool] {
663        match &self.state {
664            ServerState::Connected { tools, .. } => tools,
665            ServerState::Connecting
666            | ServerState::Authenticating
667            | ServerState::Failed { .. }
668            | ServerState::NeedsOAuth => &[],
669        }
670    }
671
672    fn has_tool(&self, tool_name: &str) -> bool {
673        self.tools().iter().any(|tool| tool.name == tool_name)
674    }
675
676    fn connection(&self) -> Option<&McpServerConnection> {
677        match &self.state {
678            ServerState::Connected { connection, .. } => Some(connection),
679            ServerState::Connecting
680            | ServerState::Authenticating
681            | ServerState::Failed { .. }
682            | ServerState::NeedsOAuth => None,
683        }
684    }
685
686    fn into_connection(self) -> Option<McpServerConnection> {
687        match self.state {
688            ServerState::Connected { connection, .. } => Some(connection),
689            ServerState::Connecting
690            | ServerState::Authenticating
691            | ServerState::Failed { .. }
692            | ServerState::NeedsOAuth => None,
693        }
694    }
695
696    fn auth_capability(&self) -> McpServerAuthCapability {
697        if self.reauth_config.is_some() { McpServerAuthCapability::OAuth } else { McpServerAuthCapability::Unavailable }
698    }
699
700    fn can_authenticate(&self) -> bool {
701        self.reauth_config.is_some()
702    }
703
704    fn status(&self) -> McpServerStatus {
705        (&self.state).into()
706    }
707
708    fn status_entry(&self, name: &str) -> McpServerStatusEntry {
709        McpServerStatusEntry::new(name, self.status())
710            .with_auth_capability(self.auth_capability())
711            .with_proxied(self.proxied)
712    }
713}
714
715#[cfg(test)]
716mod tests {
717    use super::{DEFAULT_PROXY_NAME, McpClientEvent, McpManager, McpServerStatus, ServerState};
718    use crate::client::OAuthHandlerFactory;
719    use crate::client::config::{McpServer, McpTransport};
720    use crate::client::connection::{McpConnectAttempt, McpConnectOutcome};
721    use crate::status::McpServerAuthCapability;
722    use aether_auth::{OAuthCallback, OAuthError, OAuthHandler};
723    use futures::future::BoxFuture;
724    use rmcp::{
725        Json, RoleServer, ServerHandler,
726        handler::server::{router::tool::ToolRouter, wrapper::Parameters},
727        model::{Implementation, ServerCapabilities, ServerInfo},
728        service::DynService,
729        tool, tool_handler, tool_router,
730        transport::streamable_http_client::StreamableHttpClientTransportConfig,
731    };
732    use schemars::JsonSchema;
733    use serde::{Deserialize, Serialize};
734    use std::{
735        io,
736        sync::{Arc, Mutex},
737    };
738    use tokio::sync::mpsc;
739
740    #[derive(Clone)]
741    struct TestServer {
742        tool_router: ToolRouter<Self>,
743    }
744
745    #[tool_handler(router = self.tool_router)]
746    impl ServerHandler for TestServer {
747        fn get_info(&self) -> ServerInfo {
748            ServerInfo::new(ServerCapabilities::builder().enable_tools().build())
749                .with_server_info(Implementation::new("test-server", "0.1.0").with_description("Test MCP server"))
750                .with_instructions("Test server instructions")
751        }
752    }
753
754    impl Default for TestServer {
755        fn default() -> Self {
756            Self { tool_router: Self::tool_router() }
757        }
758    }
759
760    #[derive(Debug, Deserialize, Serialize, JsonSchema)]
761    struct EchoRequest {
762        value: String,
763    }
764
765    #[derive(Debug, Deserialize, Serialize, JsonSchema)]
766    struct EchoResult {
767        value: String,
768    }
769
770    #[tool_router]
771    impl TestServer {
772        fn into_dyn(self) -> Box<dyn DynService<RoleServer>> {
773            Box::new(self)
774        }
775
776        #[tool(description = "Returns the provided value", annotations(read_only_hint = true, open_world_hint = false))]
777        async fn echo(&self, request: Parameters<EchoRequest>) -> Json<EchoResult> {
778            let Parameters(EchoRequest { value }) = request;
779            Json(EchoResult { value })
780        }
781    }
782
783    #[derive(Clone)]
784    struct SharedWriter(Arc<Mutex<Vec<u8>>>);
785
786    impl io::Write for SharedWriter {
787        fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
788            self.0.lock().unwrap().extend_from_slice(buf);
789            Ok(buf.len())
790        }
791
792        fn flush(&mut self) -> io::Result<()> {
793            Ok(())
794        }
795    }
796
797    struct TestOAuthHandler;
798
799    impl OAuthHandler for TestOAuthHandler {
800        fn redirect_uri(&self) -> &'static str {
801            "http://127.0.0.1:0/oauth2callback"
802        }
803
804        fn authorize(&self, _auth_url: &str) -> BoxFuture<'_, Result<OAuthCallback, OAuthError>> {
805            Box::pin(async { Err(OAuthError::UserCancelled) })
806        }
807    }
808
809    fn test_oauth_handler_factory() -> OAuthHandlerFactory {
810        Arc::new(|_ctx| Ok(Arc::new(TestOAuthHandler)))
811    }
812
813    #[tokio::test]
814    async fn authenticate_server_task_rejects_record_without_reauth_config() {
815        let (event_sender, _event_receiver) = mpsc::channel(1);
816        let mut manager = McpManager::new(event_sender, Some(test_oauth_handler_factory()));
817        manager.register_record("public", ServerState::Connecting, None, false);
818
819        let error = match manager.authenticate_server_task("public").await {
820            Ok(_) => panic!("non-OAuth server should be rejected"),
821            Err(error) => error.to_string(),
822        };
823        assert!(error.contains("not OAuth-authenticatable"));
824    }
825
826    #[tokio::test]
827    async fn authenticate_server_task_marks_server_authenticating_and_emits_status() {
828        let (event_sender, mut event_receiver) = mpsc::channel(2);
829        let mut manager = McpManager::new(event_sender, Some(test_oauth_handler_factory()));
830        manager.register_record(
831            "remote",
832            ServerState::NeedsOAuth,
833            Some(StreamableHttpClientTransportConfig::with_uri("http://localhost:19999/mcp")),
834            false,
835        );
836
837        let _task = manager.authenticate_server_task("remote").await.expect("auth should start");
838
839        assert!(matches!(manager.server_statuses()[0].status, McpServerStatus::Authenticating));
840        let event = event_receiver.recv().await.expect("status change event");
841        let McpClientEvent::ServerStatusesChanged(servers) = event else {
842            panic!("expected ServerStatusesChanged");
843        };
844        let status = servers.iter().find(|entry| entry.name == "remote").expect("remote status");
845        assert!(matches!(status.status, McpServerStatus::Authenticating));
846        assert_eq!(status.auth_capability, McpServerAuthCapability::OAuth);
847    }
848
849    #[tokio::test]
850    async fn apply_connection_attempt_failure_allows_retry() {
851        let (event_sender, mut event_receiver) = mpsc::channel(2);
852        let mut manager = McpManager::new(event_sender, Some(test_oauth_handler_factory()));
853        manager.register_record(
854            "remote",
855            ServerState::NeedsOAuth,
856            Some(StreamableHttpClientTransportConfig::with_uri("http://localhost:19999/mcp")),
857            false,
858        );
859        let _task = manager.authenticate_server_task("remote").await.expect("auth should start");
860        let _authenticating_event = event_receiver.recv().await.expect("authenticating status change event");
861
862        manager
863            .apply_connection_attempt(McpConnectAttempt {
864                name: "remote".to_string(),
865                proxied: false,
866                outcome: McpConnectOutcome::Failed {
867                    error: crate::client::McpError::ConnectionFailed("boom".to_string()),
868                },
869            })
870            .await;
871
872        let event = event_receiver.recv().await.expect("status change event");
873        let McpClientEvent::ServerStatusesChanged(servers) = event else {
874            panic!("expected ServerStatusesChanged");
875        };
876        let auth_event = event_receiver.recv().await.expect("authentication failure event");
877        let McpClientEvent::AuthenticationFailed { server, error } = auth_event else {
878            panic!("expected AuthenticationFailed");
879        };
880        assert_eq!(server, "remote");
881        assert!(error.contains("boom"));
882
883        let status = servers.iter().find(|entry| entry.name == "remote").expect("remote status");
884        assert_eq!(status.auth_capability, McpServerAuthCapability::OAuth);
885        assert!(matches!(status.status, McpServerStatus::Failed { ref error } if error.contains("boom")));
886        assert!(manager.authenticate_server_task("remote").await.is_ok());
887    }
888
889    #[test]
890    fn status_entries_are_derived_from_reauth_config() {
891        let (event_sender, _event_receiver) = mpsc::channel(1);
892        let mut manager = McpManager::new(event_sender, Some(test_oauth_handler_factory()));
893
894        manager.register_record(
895            "with-oauth",
896            ServerState::Connecting,
897            Some(StreamableHttpClientTransportConfig::with_uri("http://localhost/mcp")),
898            false,
899        );
900        manager.register_record("without-oauth", ServerState::Connecting, None, false);
901        manager.register_record(
902            "needs-oauth",
903            ServerState::NeedsOAuth,
904            Some(StreamableHttpClientTransportConfig::with_uri("http://localhost/mcp2")),
905            false,
906        );
907
908        let statuses = manager.server_statuses();
909        let with_oauth = statuses.iter().find(|s| s.name == "with-oauth").unwrap();
910        let without_oauth = statuses.iter().find(|s| s.name == "without-oauth").unwrap();
911        let needs_oauth = statuses.iter().find(|s| s.name == "needs-oauth").unwrap();
912
913        assert_eq!(with_oauth.auth_capability, McpServerAuthCapability::OAuth);
914        assert_eq!(without_oauth.auth_capability, McpServerAuthCapability::Unavailable);
915        assert_eq!(needs_oauth.auth_capability, McpServerAuthCapability::OAuth);
916    }
917
918    #[tokio::test]
919    async fn register_pending_marks_every_server_connecting_and_emits_status() {
920        let (event_sender, mut event_receiver) = mpsc::channel(32);
921        let mut manager = McpManager::new(event_sender, None);
922
923        let servers = vec![
924            McpServer::new("alpha", McpTransport::InMemory { server: TestServer::default().into_dyn() }, false),
925            McpServer::new("beta", McpTransport::InMemory { server: TestServer::default().into_dyn() }, true),
926        ];
927
928        let returned = manager.register_pending(servers).await.unwrap();
929        assert_eq!(returned.iter().map(|s| s.name.as_str()).collect::<Vec<_>>(), vec!["alpha", "beta"]);
930
931        let statuses = manager.server_statuses();
932        assert_eq!(statuses.len(), 2);
933        assert!(matches!(statuses.iter().find(|s| s.name == "alpha").unwrap().status, McpServerStatus::Connecting));
934        assert!(matches!(statuses.iter().find(|s| s.name == "beta").unwrap().status, McpServerStatus::Connecting));
935        assert!(statuses.iter().find(|s| s.name == "beta").unwrap().proxied);
936
937        let event = event_receiver.try_recv().expect("expected initial ServerStatusesChanged emission");
938        let McpClientEvent::ServerStatusesChanged(emitted) = event else {
939            panic!("expected ServerStatusesChanged, got {event:?}");
940        };
941        assert_eq!(emitted.iter().map(|s| s.name.as_str()).collect::<Vec<_>>(), vec!["alpha", "beta"]);
942    }
943
944    #[tokio::test]
945    async fn apply_connection_attempt_emits_instructions_updated_after_connect() {
946        let (event_sender, mut event_receiver) = mpsc::channel(32);
947        let mut manager = McpManager::new(event_sender, None);
948
949        let servers =
950            vec![McpServer::new("test", McpTransport::InMemory { server: TestServer::default().into_dyn() }, false)];
951        manager.add_mcps(servers).await.unwrap();
952
953        let mut update_for_test = None;
954        while let Ok(event) = event_receiver.try_recv() {
955            if let McpClientEvent::ServerInstructionsUpdated { server, instructions } = event
956                && server == "test"
957            {
958                update_for_test = Some(instructions);
959            }
960        }
961        let instructions = update_for_test.expect("expected ServerInstructionsUpdated for 'test'");
962        assert!(instructions.is_some(), "TestServer publishes instructions, so update should carry Some(_)");
963    }
964
965    #[tokio::test]
966    async fn server_statuses_mark_direct_and_proxied_servers_without_proxy_row() {
967        let (event_sender, _event_receiver) = mpsc::channel(32);
968        let mut manager = McpManager::new(event_sender, None);
969        manager
970            .add_mcps(vec![
971                McpServer::new("direct", McpTransport::InMemory { server: TestServer::default().into_dyn() }, false),
972                McpServer::new("math", McpTransport::InMemory { server: TestServer::default().into_dyn() }, true),
973            ])
974            .await
975            .unwrap();
976
977        let statuses = manager.server_statuses();
978        assert_eq!(statuses.iter().map(|status| status.name.as_str()).collect::<Vec<_>>(), vec!["direct", "math"]);
979        assert!(!statuses.iter().find(|status| status.name == "direct").unwrap().proxied);
980        assert!(statuses.iter().find(|status| status.name == "math").unwrap().proxied);
981        assert!(!statuses.iter().any(|status| status.name == DEFAULT_PROXY_NAME));
982    }
983
984    #[tokio::test]
985    async fn tool_definitions_drop_when_a_server_shuts_down() {
986        let (event_sender, _event_receiver) = mpsc::channel(32);
987        let mut manager = McpManager::new(event_sender, None);
988        manager
989            .add_mcps(vec![
990                McpServer::new("git", McpTransport::InMemory { server: TestServer::default().into_dyn() }, false),
991                McpServer::new("github", McpTransport::InMemory { server: TestServer::default().into_dyn() }, false),
992            ])
993            .await
994            .unwrap();
995
996        let names =
997            |manager: &McpManager| manager.tool_definitions().into_iter().map(|tool| tool.name).collect::<Vec<_>>();
998        assert!(names(&manager).contains(&"git__echo".to_string()));
999        assert!(names(&manager).contains(&"github__echo".to_string()));
1000
1001        manager.shutdown_server("git").await.unwrap();
1002
1003        assert!(!names(&manager).iter().any(|name| name.starts_with("git__")));
1004        assert!(names(&manager).contains(&"github__echo".to_string()));
1005    }
1006
1007    #[tokio::test]
1008    async fn tool_definitions_preserve_annotations() {
1009        let (event_sender, _event_receiver) = mpsc::channel(32);
1010        let mut manager = McpManager::new(event_sender, None);
1011        manager
1012            .add_mcps(vec![McpServer::new(
1013                "test",
1014                McpTransport::InMemory { server: TestServer::default().into_dyn() },
1015                false,
1016            )])
1017            .await
1018            .unwrap();
1019
1020        let tools = manager.tool_definitions();
1021        let echo = tools.iter().find(|tool| tool.name == "test__echo").expect("echo tool");
1022        let annotations = echo.annotations.as_ref().expect("annotations should be preserved");
1023        assert_eq!(annotations.read_only_hint, Some(true));
1024        assert_eq!(annotations.open_world_hint, Some(false));
1025    }
1026
1027    #[tokio::test]
1028    async fn drop_logs_cleanup_abort_with_tracing() {
1029        let (event_sender, _event_receiver) = mpsc::channel(32);
1030        let mut manager = McpManager::new(event_sender, None);
1031        manager
1032            .add_mcps(vec![McpServer::new(
1033                "test",
1034                McpTransport::InMemory { server: TestServer::default().into_dyn() },
1035                false,
1036            )])
1037            .await
1038            .unwrap();
1039
1040        let output = Arc::new(Mutex::new(Vec::new()));
1041        let subscriber = tracing_subscriber::fmt()
1042            .with_ansi(false)
1043            .without_time()
1044            .with_writer({
1045                let output = Arc::clone(&output);
1046                move || SharedWriter(Arc::clone(&output))
1047            })
1048            .finish();
1049
1050        tracing::subscriber::with_default(subscriber, || {
1051            drop(manager);
1052        });
1053
1054        let logs = String::from_utf8(output.lock().unwrap().clone()).unwrap();
1055        assert!(logs.contains("Server 'test' task aborted during cleanup"));
1056    }
1057}