1use crate::events::TraceContext;
2use mcp_utils::client::{
3 McpClient, McpConnectAttempt, McpConnectionAttemptManager, McpError, McpManager, McpServer, McpServerStatusEntry,
4};
5use mcp_utils::display_meta::ToolResultMeta;
6
7use futures::future::Either;
8use futures::stream::{self, StreamExt};
9use llm::{ToolCallError, ToolCallRequest, ToolCallResult};
10use rmcp::RoleClient;
11use rmcp::model::{
12 CallToolRequestParams, ElicitRequestParams, ErrorCode, GetPromptResult, ProgressNotificationParam, Prompt,
13 RequestMetaObject,
14};
15use rmcp::service::RunningService;
16use std::collections::HashSet;
17use std::sync::Arc;
18use std::time::Duration;
19use tokio::select;
20use tokio::sync::mpsc;
21use tokio::sync::oneshot;
22
23#[derive(Debug)]
25pub enum ToolExecutionEvent {
26 Progress { tool_id: String, progress: ProgressNotificationParam },
27 Complete { tool_id: String, result: Result<ToolCallResult, ToolCallError>, result_meta: Option<ToolResultMeta> },
28}
29
30const MCP_AUTH_TIMEOUT: Duration = Duration::from_mins(3);
31const URL_ELICITATION_REQUIRED: ErrorCode = ErrorCode(-32042);
32
33#[derive(Debug)]
35pub enum McpCommand {
36 ExecuteTool {
37 request: ToolCallRequest,
38 trace_context: Option<TraceContext>,
39 timeout: Duration,
40 tx: mpsc::Sender<ToolExecutionEvent>,
41 },
42 ListPrompts {
43 tx: oneshot::Sender<Result<Vec<Prompt>, String>>,
44 },
45 GetPrompt {
46 name: String,
47 arguments: Option<serde_json::Map<String, serde_json::Value>>,
48 tx: oneshot::Sender<Result<GetPromptResult, String>>,
49 },
50 GetServerStatuses {
51 tx: oneshot::Sender<Vec<McpServerStatusEntry>>,
52 },
53 AuthenticateServer {
54 name: String,
55 },
56}
57
58pub async fn run_mcp_task(
59 mut mcp: McpManager,
60 mut command_rx: mpsc::Receiver<McpCommand>,
61 pending_servers: Vec<McpServer>,
62) {
63 let mut mcp_connection_attempts = McpConnectionAttemptManager::default();
64 let mut pending_connections: HashSet<String> = pending_servers.iter().map(|server| server.name.clone()).collect();
65 for server in pending_servers {
66 let name = server.name.clone();
67 let task = mcp.connect_pending_task(server);
68 mcp_connection_attempts.spawn(name, task);
69 }
70 if pending_connections.is_empty() {
71 mcp.emit_connection_ready().await;
72 }
73
74 loop {
75 select! {
76 command = command_rx.recv() => {
77 let Some(command) = command else { break; };
78 on_command(command, &mut mcp, &mut mcp_connection_attempts).await;
79 }
80
81 Some(joined) = mcp_connection_attempts.join_next(), if !mcp_connection_attempts.is_empty() => {
82 match joined {
83 Ok(attempt) => {
84 let was_bootstrap = pending_connections.remove(&attempt.name);
85 mcp.apply_connection_attempt(attempt).await;
86 if was_bootstrap && pending_connections.is_empty() {
87 mcp.emit_connection_ready().await;
88 }
89 }
90 Err(e) => tracing::error!("MCP auth task did not complete normally: {e:?}"),
91 }
92 }
93 }
94 }
95
96 mcp_connection_attempts.shutdown().await;
97 mcp.shutdown().await;
98 tracing::debug!("MCP manager task ended");
99}
100
101async fn on_command(command: McpCommand, mcp: &mut McpManager, auth_tasks: &mut McpConnectionAttemptManager) {
102 match command {
103 McpCommand::ExecuteTool { request, trace_context, timeout, tx } => {
104 let tool_id = request.id.clone();
105
106 match mcp.get_client_for_tool(&request.name, &request.arguments) {
107 Ok((client, params)) => {
108 let trace_meta = trace_context.as_ref().map(TraceContext::to_meta);
109 tokio::spawn(async move {
110 let outcome = execute_mcp_call(
111 client,
112 &request,
113 params,
114 trace_meta,
115 timeout,
116 tool_id.clone(),
117 tx.clone(),
118 )
119 .await;
120 let (result, result_meta) = match outcome {
121 Ok((r, m)) => (Ok(r), m),
122 Err(e) => (Err(e), None),
123 };
124 let _ = tx.send(ToolExecutionEvent::Complete { tool_id, result, result_meta }).await;
125 });
126 }
127 Err(e) => {
128 tracing::error!("Failed to get client for tool {}: {e}", request.name);
129 let error = ToolCallError::from_request(&request, format!("Failed to get client: {e}"));
130 let _ =
131 tx.send(ToolExecutionEvent::Complete { tool_id, result: Err(error), result_meta: None }).await;
132 }
133 }
134 }
135
136 McpCommand::ListPrompts { tx } => {
137 let result = mcp.list_prompts().await.map_err(|e| format!("Failed to list prompts: {e}"));
138 let _ = tx.send(result);
139 }
140
141 McpCommand::GetPrompt { name: namespaced_name, arguments, tx } => {
142 let result =
143 mcp.get_prompt(&namespaced_name, arguments).await.map_err(|e| format!("Failed to get prompt: {e}"));
144 let _ = tx.send(result);
145 }
146
147 McpCommand::GetServerStatuses { tx } => {
148 let _ = tx.send(mcp.server_statuses());
149 }
150
151 McpCommand::AuthenticateServer { name } => match mcp.authenticate_server_task(&name).await {
152 Ok(task) => {
153 let server_name = name.clone();
154 auth_tasks.spawn(name, async move {
155 match tokio::time::timeout(MCP_AUTH_TIMEOUT, task).await {
156 Ok(attempt) => attempt,
157 Err(_) => McpConnectAttempt::failed(
158 server_name,
159 McpError::ConnectionFailed("authentication timed out after 3 minutes".to_string()),
160 false,
161 ),
162 }
163 });
164 }
165 Err(e) => tracing::warn!("Authentication failed for '{name}': {e}"),
166 },
167 }
168}
169
170async fn execute_mcp_call(
173 client: Arc<RunningService<RoleClient, McpClient>>,
174 request: &ToolCallRequest,
175 params: CallToolRequestParams,
176 trace_meta: Option<RequestMetaObject>,
177 timeout: Duration,
178 tool_call_id: String,
179 event_tx: mpsc::Sender<ToolExecutionEvent>,
180) -> Result<(ToolCallResult, Option<ToolResultMeta>), ToolCallError> {
181 use super::tool_bridge::mcp_result_to_tool_call_result;
182 use rmcp::model::{ClientRequest::CallToolRequest, Request, ServerResult};
183 use rmcp::service::PeerRequestOptions;
184
185 let handle = client
186 .send_cancellable_request(CallToolRequest(Request::new(params)), {
187 let mut opts = PeerRequestOptions::default();
188 opts.timeout = Some(timeout);
189 opts.meta = trace_meta;
190 opts
191 })
192 .await
193 .map_err(|e| ToolCallError::from_request(request, format!("Failed to send tool request: {e}")))?;
194
195 let progress_subscriber = client.service().progress_dispatcher.subscribe(handle.progress_token.clone()).await;
196
197 let progress_stream = progress_subscriber
198 .map(move |progress| Either::Left(ToolExecutionEvent::Progress { tool_id: tool_call_id.clone(), progress }));
199
200 let result_stream = stream::once(handle.await_response()).map(Either::Right);
201 let combined_stream = stream::select(progress_stream, result_stream);
202 tokio::pin!(combined_stream);
203
204 let server_result = loop {
205 match combined_stream.next().await {
206 Some(Either::Left(progress_event)) => {
207 let _ = event_tx.send(progress_event).await;
208 }
209 Some(Either::Right(result)) => {
210 break match result {
211 Ok(server_result) => server_result,
212 Err(e) => {
213 if let rmcp::service::ServiceError::McpError(ref error_data) = e
214 && error_data.code == URL_ELICITATION_REQUIRED
215 {
216 return Err(handle_url_elicitation_required(&client, request, error_data).await);
217 }
218 return Err(ToolCallError::from_request(request, format!("Tool execution failed: {e}")));
219 }
220 };
221 }
222 None => {
223 return Err(ToolCallError::from_request(request, "Stream ended without result"));
224 }
225 }
226 };
227
228 let ServerResult::CallToolResult(mcp_result) = server_result else {
229 return Err(ToolCallError::from_request(request, "Unexpected response type from MCP server"));
230 };
231
232 mcp_result_to_tool_call_result(request, mcp_result)
233}
234
235#[derive(serde::Deserialize)]
236struct UrlElicitationRequiredData {
237 elicitations: Vec<ElicitRequestParams>,
238}
239
240#[derive(Debug)]
241enum UrlElicitationRequiredParseError {
242 MissingData,
243 InvalidData(serde_json::Error),
244 NoUrlRequests,
245}
246
247impl std::fmt::Display for UrlElicitationRequiredParseError {
248 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
249 match self {
250 Self::MissingData => write!(f, "missing error data"),
251 Self::InvalidData(error) => write!(f, "malformed error data: {error}"),
252 Self::NoUrlRequests => write!(f, "provided no URL elicitation requests"),
253 }
254 }
255}
256
257fn parse_required_url_elicitations(
258 error_data: &rmcp::model::ErrorData,
259) -> Result<Vec<ElicitRequestParams>, UrlElicitationRequiredParseError> {
260 let data = error_data.data.as_ref().ok_or(UrlElicitationRequiredParseError::MissingData)?;
261 let parsed: UrlElicitationRequiredData =
262 serde_json::from_value(data.clone()).map_err(UrlElicitationRequiredParseError::InvalidData)?;
263
264 let url_elicitations = parsed
265 .elicitations
266 .into_iter()
267 .filter(|elicitation| matches!(elicitation, ElicitRequestParams::UrlElicitationParams { .. }))
268 .collect::<Vec<_>>();
269
270 if url_elicitations.is_empty() {
271 return Err(UrlElicitationRequiredParseError::NoUrlRequests);
272 }
273
274 Ok(url_elicitations)
275}
276
277async fn handle_url_elicitation_required(
281 client: &Arc<RunningService<RoleClient, McpClient>>,
282 request: &ToolCallRequest,
283 error_data: &rmcp::model::ErrorData,
284) -> ToolCallError {
285 let server_name = client.service().server_name().to_string();
286 let url_elicitations = match parse_required_url_elicitations(error_data) {
287 Ok(url_elicitations) => url_elicitations,
288 Err(UrlElicitationRequiredParseError::NoUrlRequests) => {
289 return ToolCallError::from_request(
290 request,
291 format!("Server '{server_name}' requires URL elicitation but provided no URL elicitation requests"),
292 );
293 }
294 Err(parse_error) => {
295 return ToolCallError::from_request(
296 request,
297 format!("Server '{server_name}' sent an invalid URL elicitation response: {parse_error}"),
298 );
299 }
300 };
301
302 tracing::info!("Server '{server_name}' requires {} URL elicitation(s)", url_elicitations.len());
303
304 for elicitation in url_elicitations {
305 let result = client.service().dispatch_elicitation(elicitation).await;
306 match result.action {
307 rmcp::model::ElicitationAction::Decline => {
308 return ToolCallError::from_request(
309 request,
310 format!("Required browser interaction for server '{server_name}' was declined"),
311 );
312 }
313 rmcp::model::ElicitationAction::Cancel => {
314 return ToolCallError::from_request(
315 request,
316 format!("Required browser interaction for server '{server_name}' was cancelled"),
317 );
318 }
319 rmcp::model::ElicitationAction::Accept => {
320 tracing::info!("User accepted URL elicitation for server '{server_name}'");
321 }
322 _ => {
323 return ToolCallError::from_request(
324 request,
325 format!("Required browser interaction for server '{server_name}' returned an unsupported response"),
326 );
327 }
328 }
329 }
330
331 ToolCallError::from_request(
332 request,
333 format!(
334 "Server '{server_name}' requires a browser flow. The URL has been opened for your approval. Retry the previous request after completing the browser flow."
335 ),
336 )
337}
338
339#[cfg(test)]
340mod tests {
341 use super::*;
342
343 #[test]
344 fn url_elicitation_required_data_parses_url_entries() {
345 let data = serde_json::json!({
346 "elicitations": [
347 {
348 "mode": "url",
349 "message": "Auth",
350 "url": "https://example.com/auth?elicitationId=el-1",
351 "elicitationId": "el-1"
352 }
353 ]
354 });
355
356 let parsed: UrlElicitationRequiredData = serde_json::from_value(data).unwrap();
357 assert_eq!(parsed.elicitations.len(), 1);
358 assert!(matches!(
359 &parsed.elicitations[0],
360 ElicitRequestParams::UrlElicitationParams { elicitation_id, .. } if elicitation_id == "el-1"
361 ));
362 }
363
364 #[test]
365 fn parse_required_url_elicitations_filters_to_url_only() {
366 let error_data = rmcp::model::ErrorData {
367 code: URL_ELICITATION_REQUIRED,
368 message: "URL elicitation required".into(),
369 data: Some(serde_json::json!({
370 "elicitations": [
371 {
372 "mode": "url",
373 "message": "Auth",
374 "url": "https://example.com/auth",
375 "elicitationId": "el-1"
376 },
377 {
378 "mode": "form",
379 "message": "Pick a color",
380 "requestedSchema": { "type": "object", "properties": {} }
381 }
382 ]
383 })),
384 };
385
386 let result = parse_required_url_elicitations(&error_data).unwrap();
387 assert_eq!(result.len(), 1);
388 assert!(matches!(
389 &result[0],
390 ElicitRequestParams::UrlElicitationParams { elicitation_id, .. } if elicitation_id == "el-1"
391 ));
392 }
393}