1use super::{
2 McpClientEvent, McpError, OAuthHandlerFactory, Result,
3 config::McpHttpConfig,
4 manager::{RuntimeMcpServer, RuntimeMcpTransport, ToolListChangedRequest},
5 mcp_client::McpClient,
6};
7use crate::{client::OAuthHandlerContext, protocol::client_lifecycle_mode, transport::create_in_memory_transport};
8use aether_auth::{OAuthCredentialStorage, create_auth_manager_from_store, perform_oauth_flow};
9use llm::ToolAnnotations;
10use rmcp::{
11 RoleClient, RoleServer, ServiceExt,
12 model::{ClientInfo, Tool as RmcpTool},
13 serve_client_with_lifecycle,
14 service::{DynService, RunningService},
15 transport::{
16 StreamableHttpClientTransport, TokioChildProcess, auth::AuthClient,
17 streamable_http_client::StreamableHttpClientTransportConfig,
18 },
19};
20use serde_json::Value;
21use std::collections::HashMap;
22use std::path::PathBuf;
23use std::process::Stdio;
24use std::sync::{
25 Arc,
26 atomic::{AtomicU64, Ordering},
27};
28use tokio::{
29 io::{AsyncBufReadExt, BufReader},
30 process::{ChildStderr, Command},
31 sync::mpsc,
32 task::JoinHandle,
33};
34
35#[derive(Debug, Clone)]
36pub struct Tool {
37 pub name: String,
38 pub description: String,
39 pub parameters: Value,
40 pub annotations: Option<ToolAnnotations>,
41}
42
43pub(crate) fn convert_tool_annotations(annotations: &rmcp::model::ToolAnnotations) -> ToolAnnotations {
44 ToolAnnotations {
45 title: annotations.title.clone(),
46 read_only_hint: annotations.read_only_hint,
47 destructive_hint: annotations.destructive_hint,
48 idempotent_hint: annotations.idempotent_hint,
49 open_world_hint: annotations.open_world_hint,
50 }
51}
52
53impl From<RmcpTool> for Tool {
54 fn from(tool: RmcpTool) -> Self {
55 Self::from(&tool)
56 }
57}
58
59impl From<&RmcpTool> for Tool {
60 fn from(tool: &RmcpTool) -> Self {
61 Self {
62 name: tool.name.to_string(),
63 description: tool.description.clone().unwrap_or_default().to_string(),
64 parameters: serde_json::Value::Object((*tool.input_schema).clone()),
65 annotations: tool.annotations.as_ref().map(convert_tool_annotations),
66 }
67 }
68}
69
70pub(super) struct ConnectConfig {
71 pub client_info: ClientInfo,
72 pub event_sender: mpsc::Sender<McpClientEvent>,
73 pub tool_refresh_sender: mpsc::Sender<ToolListChangedRequest>,
74 pub next_connection_generation: Arc<AtomicU64>,
75 pub root_dir: PathBuf,
76 pub oauth_handler_factory: Option<OAuthHandlerFactory>,
77 pub oauth_credential_store: Option<Arc<dyn OAuthCredentialStorage>>,
78}
79
80pub struct McpConnectAttempt {
82 pub name: String,
83 pub outcome: McpConnectOutcome,
84}
85
86pub enum McpConnectOutcome {
87 Connected { conn: McpServerConnection, reauth_config: Option<McpHttpConfig> },
88 NeedsOAuth { config: McpHttpConfig, challenge: Option<String>, error: McpError },
89 Failed { error: McpError },
90}
91
92impl McpConnectAttempt {
93 pub fn failed(name: impl Into<String>, error: McpError) -> Self {
94 Self { name: name.into(), outcome: McpConnectOutcome::Failed { error } }
95 }
96}
97
98pub struct McpServerConnection {
99 pub(super) client: Arc<RunningService<RoleClient, McpClient>>,
100 pub(super) server_task: Option<JoinHandle<()>>,
101 pub(super) instructions: Option<String>,
102 generation: u64,
103}
104
105impl McpServerConnection {
106 pub(super) async fn reconnect_with_auth(
107 name: &str,
108 config: StreamableHttpClientTransportConfig,
109 auth_client: AuthClient<reqwest::Client>,
110 mcp_client: McpClient,
111 generation: u64,
112 ) -> Result<Self> {
113 let transport = StreamableHttpClientTransport::with_client(auth_client, config);
114 let client = serve_client_with_lifecycle(mcp_client, transport, client_lifecycle_mode())
115 .await
116 .map_err(|e| McpError::ConnectionFailed(format!("reconnect failed for '{name}': {e}")))?;
117 Ok(Self::from_parts(client, None, generation))
118 }
119
120 pub(super) async fn list_tools(&self) -> Result<Vec<RmcpTool>> {
121 self.client
122 .list_all_tools()
123 .await
124 .map_err(|e| McpError::ToolDiscoveryFailed(format!("Failed to list tools: {e}")))
125 }
126
127 fn from_parts(
128 client: RunningService<RoleClient, McpClient>,
129 server_task: Option<JoinHandle<()>>,
130 generation: u64,
131 ) -> Self {
132 let instructions = client.peer_info().and_then(|info| info.instructions.clone()).filter(|s| !s.is_empty());
133 Self { client: Arc::new(client), server_task, instructions, generation }
134 }
135
136 pub(super) fn generation(&self) -> u64 {
137 self.generation
138 }
139}
140
141pub(super) async fn connect_server(server: RuntimeMcpServer, ctx: &ConnectConfig) -> McpConnectAttempt {
142 let RuntimeMcpServer { name, transport, tool_exposure: _ } = server;
143 let reauth_config = reauth_config_for(&transport, ctx.oauth_handler_factory.as_ref());
144 let generation = ctx.next_connection_generation.fetch_add(1, Ordering::Relaxed);
145 let mcp_client = McpClient::new(ctx.client_info.clone(), name.clone(), ctx.event_sender.clone())
146 .with_tool_refresh(ctx.tool_refresh_sender.clone(), generation);
147
148 let outcome = match transport {
149 RuntimeMcpTransport::Stdio { command, args, env } => {
150 connect_stdio(&name, command, args, env, mcp_client, ctx.root_dir.clone(), generation).await
151 }
152 RuntimeMcpTransport::InMemory { server } => connect_in_memory(&name, server, mcp_client, generation).await,
153 RuntimeMcpTransport::Http(config) => {
154 connect_http(
155 &name,
156 config,
157 mcp_client,
158 ctx.oauth_handler_factory.as_ref(),
159 ctx.oauth_credential_store.as_ref(),
160 generation,
161 )
162 .await
163 }
164 };
165
166 McpConnectAttempt { name, outcome: outcome.with_reauth(reauth_config) }
167}
168
169pub async fn authenticate_http(
170 name: String,
171 config: McpHttpConfig,
172 challenge: Option<String>,
173 ctx: Arc<ConnectConfig>,
174) -> McpConnectAttempt {
175 let outcome = match async {
176 let factory = ctx
177 .oauth_handler_factory
178 .as_ref()
179 .ok_or_else(|| McpError::ConnectionFailed(format!("No OAuth handler factory available for '{name}'")))?;
180 let oauth = config
181 .resolved_oauth()
182 .ok_or_else(|| McpError::ConnectionFailed(format!("OAuth is not available for '{name}'")))?;
183 let handler = factory(OAuthHandlerContext {
184 server_name: name.clone(),
185 callback_port: Some(oauth.callback_port),
186 tx: ctx.event_sender.clone(),
187 })?;
188
189 let auth_client = perform_oauth_flow(
190 &name,
191 &config.transport.uri,
192 handler.as_ref(),
193 aether_auth::OAuthFlowOptions { client_registration: oauth.client_registration, challenge },
194 ctx.oauth_credential_store.clone(),
195 )
196 .await
197 .map_err(|e| McpError::ConnectionFailed(format!("OAuth failed for '{name}': {e}")))?;
198
199 let generation = ctx.next_connection_generation.fetch_add(1, Ordering::Relaxed);
200 let mcp_client = McpClient::new(ctx.client_info.clone(), name.clone(), ctx.event_sender.clone())
201 .with_tool_refresh(ctx.tool_refresh_sender.clone(), generation);
202 McpServerConnection::reconnect_with_auth(&name, config.transport.clone(), auth_client, mcp_client, generation)
203 .await
204 }
205 .await
206 {
207 Ok(conn) => McpConnectOutcome::Connected { conn, reauth_config: Some(config) },
208 Err(error) => McpConnectOutcome::Failed { error },
209 };
210
211 McpConnectAttempt { name, outcome }
212}
213
214impl McpConnectOutcome {
215 fn with_reauth(self, reauth_config: Option<McpHttpConfig>) -> Self {
216 match self {
217 Self::Connected { conn, .. } => Self::Connected { conn, reauth_config },
218 other => other,
219 }
220 }
221}
222
223async fn connect_stdio(
224 server_name: &str,
225 command: String,
226 args: Vec<String>,
227 env: HashMap<String, String>,
228 mcp_client: McpClient,
229 cwd: PathBuf,
230 generation: u64,
231) -> McpConnectOutcome {
232 let mut cmd = Command::new(&command);
233 cmd.args(&args).envs(&env).current_dir(&cwd);
234
235 let (proc, stderr) = match TokioChildProcess::builder(cmd).stderr(Stdio::piped()).spawn() {
236 Ok(parts) => parts,
237 Err(e) => return McpConnectOutcome::Failed { error: McpError::SpawnFailed { command, reason: e.to_string() } },
238 };
239 let stderr_task = stderr.map(|stderr| spawn_stderr_logger(server_name.to_string(), stderr));
240
241 match serve_client_with_lifecycle(mcp_client, proc, client_lifecycle_mode()).await {
242 Ok(client) => McpConnectOutcome::Connected {
243 conn: McpServerConnection::from_parts(client, stderr_task, generation),
244 reauth_config: None,
245 },
246 Err(e) => {
247 if let Some(task) = stderr_task {
248 task.abort();
249 }
250 McpConnectOutcome::Failed { error: McpError::from(e) }
251 }
252 }
253}
254
255fn spawn_stderr_logger(server_name: String, stderr: ChildStderr) -> JoinHandle<()> {
256 tokio::spawn(async move {
257 let mut lines = BufReader::new(stderr).lines();
258 loop {
259 match lines.next_line().await {
260 Ok(Some(line)) => tracing::info!(server = %server_name, stderr = %line, "MCP server stderr"),
261 Ok(None) => break,
262 Err(error) => {
263 tracing::warn!(server = %server_name, %error, "failed to read MCP server stderr");
264 break;
265 }
266 }
267 }
268 })
269}
270
271async fn connect_in_memory(
272 name: &str,
273 server: Box<dyn DynService<RoleServer>>,
274 mcp_client: McpClient,
275 generation: u64,
276) -> McpConnectOutcome {
277 match serve_in_memory(server, mcp_client, name).await {
278 Ok((client, handle)) => McpConnectOutcome::Connected {
279 conn: McpServerConnection::from_parts(client, Some(handle), generation),
280 reauth_config: None,
281 },
282 Err(error) => McpConnectOutcome::Failed { error },
283 }
284}
285
286async fn connect_http(
287 name: &str,
288 config: McpHttpConfig,
289 mcp_client: McpClient,
290 oauth_handler_factory: Option<&OAuthHandlerFactory>,
291 oauth_credential_store: Option<&Arc<dyn OAuthCredentialStorage>>,
292 generation: u64,
293) -> McpConnectOutcome {
294 let conn_err = |e| McpError::ConnectionFailed(format!("HTTP MCP server {name}: {e}"));
295 let oauth = config.resolved_oauth();
296 let restored = if let (Some(store), Some(oauth)) = (oauth_credential_store, oauth.as_ref()) {
297 match create_auth_manager_from_store(
298 name,
299 &config.transport.uri,
300 oauth.client_registration.pre_registered_client_id(),
301 &oauth.redirect_uri(),
302 Arc::clone(store),
303 )
304 .await
305 {
306 Ok(manager) => manager,
307 Err(e) => {
308 tracing::warn!(
309 server = %name,
310 error = %e,
311 "Failed to initialize auth manager from stored credentials, proceeding without auth"
312 );
313 None
314 }
315 }
316 } else {
317 None
318 };
319 let result = if let Some(manager) = restored {
320 tracing::debug!("Using OAuth for server '{name}'");
321 let auth_client = AuthClient::new(reqwest::Client::default(), manager);
322 let transport = StreamableHttpClientTransport::with_client(auth_client, config.transport.clone());
323 serve_client_with_lifecycle(mcp_client, transport, client_lifecycle_mode()).await
324 } else {
325 let transport = StreamableHttpClientTransport::from_config(config.transport.clone());
326 serve_client_with_lifecycle(mcp_client, transport, client_lifecycle_mode()).await
327 };
328
329 match result {
330 Ok(client) => McpConnectOutcome::Connected {
331 conn: McpServerConnection::from_parts(client, None, generation),
332 reauth_config: None,
333 },
334 Err(error) => {
335 let challenge = error.auth_challenge().map(str::to_string);
336 let authorization_required = error.is_authorization_required();
337 let error = conn_err(error);
338 tracing::warn!("Failed to connect to MCP server '{name}': {error}");
339 if oauth_handler_factory.is_some() && oauth.is_some() && (authorization_required || challenge.is_some()) {
340 McpConnectOutcome::NeedsOAuth { config, challenge, error }
341 } else {
342 McpConnectOutcome::Failed { error }
343 }
344 }
345 }
346}
347
348fn reauth_config_for(
349 transport: &RuntimeMcpTransport,
350 oauth_handler_factory: Option<&OAuthHandlerFactory>,
351) -> Option<McpHttpConfig> {
352 match transport {
353 RuntimeMcpTransport::Http(config)
354 if oauth_handler_factory.is_some() && config.transport.auth_header.is_none() =>
355 {
356 Some(config.clone())
357 }
358 _ => None,
359 }
360}
361
362async fn serve_in_memory(
363 server: Box<dyn DynService<RoleServer>>,
364 mcp_client: McpClient,
365 label: &str,
366) -> Result<(RunningService<RoleClient, McpClient>, JoinHandle<()>)> {
367 let (client_transport, server_transport) = create_in_memory_transport();
368
369 let server_handle = tokio::spawn(async move {
370 match server.serve(server_transport).await {
371 Ok(_service) => {
372 std::future::pending::<()>().await;
373 }
374 Err(e) => {
375 eprintln!("MCP server error: {e}");
376 }
377 }
378 });
379
380 let client = serve_client_with_lifecycle(mcp_client, client_transport, client_lifecycle_mode())
381 .await
382 .map_err(|e| McpError::ConnectionFailed(format!("Failed to connect to in-memory server '{label}': {e}")))?;
383
384 Ok((client, server_handle))
385}