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#[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#[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
87pub 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 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 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 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 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
621struct 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}