sovran_mcp/
client.rs

1//! Client implementation for the Model Context Protocol (MCP).
2//!
3//! The MCP client provides a synchronous interface for interacting with MCP servers,
4//! supporting operations like:
5//! - Tool execution
6//! - Prompt management
7//! - Resource handling
8//! - Server capability detection
9//!
10//! # Usage
11//!
12//! Basic usage with stdio transport:
13//!
14//! ```
15//! use sovran_mcp::{McpClient, transport::StdioTransport};
16//!
17//! # fn main() -> Result<(), sovran_mcp::McpError> {
18//! // Create and start client
19//! let transport = StdioTransport::new("npx", &["-y", "@modelcontextprotocol/server-everything"])?;
20//! let mut client = McpClient::new(transport, None, None);
21//! client.start()?;
22//!
23//! // Use MCP features
24//! if client.supports_tools() {
25//!     let tools = client.list_tools()?;
26//!     println!("Available tools: {}", tools.tools.len());
27//! }
28//!
29//! // Clean up
30//! client.stop()?;
31//! # Ok(())
32//! # }
33//! ```
34//!
35//! # Error Handling
36//!
37//! The client uses `McpError` for error handling, which covers:
38//! - Transport errors (I/O, connection issues)
39//! - Protocol errors (JSON-RPC, serialization)
40//! - Capability errors (unsupported features)
41//! - Request timeouts
42//! - Command failures
43//!
44//! # Thread Safety
45//!
46//! The client spawns a message handling thread for processing responses and notifications.
47//! This thread is properly managed through the `start()` and `stop()` methods.
48use crate::commands::{
49    CallTool, GetPrompt, Initialize, ListPrompts, ListResources, ListTools, McpCommand,
50    ReadResource, Subscribe, Unsubscribe,
51};
52use crate::messaging::{
53    JsonRpcMessage, JsonRpcNotification, JsonRpcRequest, JsonRpcResponse, MessageHandler,
54};
55use crate::transport::Transport;
56use crate::types::*;
57use crate::McpError;
58use std::collections::HashMap;
59use std::sync::atomic::AtomicBool;
60use std::sync::mpsc::{channel, Sender};
61use std::sync::{
62    atomic::{AtomicU64, Ordering},
63    Arc, Mutex,
64};
65use std::thread::{self, JoinHandle};
66use tracing::{debug, warn};
67use url::Url;
68
69/// A client implementation of the Model Context Protocol (MCP).
70///
71/// The MCP client provides a synchronous interface for interacting with MCP servers,
72/// allowing operations like tool execution, prompt management, and resource handling.
73///
74/// # Examples
75///
76/// ```
77/// use sovran_mcp::{McpClient, transport::StdioTransport};
78///
79/// // Create a client using stdio transport
80/// let transport = StdioTransport::new("npx", &["-y", "@modelcontextprotocol/server-everything"])?;
81/// let mut client = McpClient::new(transport, None, None);
82///
83/// // Start the client (initializes connection and protocol)
84/// client.start()?;
85///
86/// // Use the client...
87/// let tools = client.list_tools()?;
88///
89/// // Clean up when done
90/// client.stop()?;
91/// # Ok::<(), sovran_mcp::McpError>(())
92/// ```
93pub struct McpClient<T: Transport + 'static> {
94    transport: Arc<T>,
95    request_id: Arc<AtomicU64>,
96    pending_requests: Arc<Mutex<HashMap<u64, Sender<JsonRpcResponse>>>>,
97    listener_handle: Option<Arc<Mutex<Option<JoinHandle<()>>>>>,
98    sampling_handler: Option<Arc<Box<dyn SamplingHandler + Send>>>,
99    notification_handler: Option<Arc<Box<dyn NotificationHandler + Send>>>,
100    stop_flag: Arc<AtomicBool>,
101    server_info: Arc<Mutex<Option<InitializeResponse>>>,
102}
103
104impl<T: Transport + 'static> Drop for McpClient<T> {
105    fn drop(&mut self) {
106        if !self.stop_flag.load(Ordering::SeqCst) {
107            // We don't want to panic in drop, so we ignore any errors
108            let _ = self.stop();
109        }
110    }
111}
112
113impl<T: Transport + 'static> McpClient<T> {
114    /// Creates a new MCP client with the specified transport and optional handlers.
115    ///
116    /// # Arguments
117    ///
118    /// * `transport` - The transport implementation to use for communication
119    /// * `sampling_handler` - Optional handler for LLM completion requests from the server. This enables
120    ///   the server to request AI completions through the client while maintaining security boundaries.
121    /// * `notification_handler` - Optional handler for receiving one-way messages from the server,
122    ///   such as resource updates.
123    ///
124    /// # Examples
125    ///
126    /// Basic client without handlers:
127    /// ```
128    /// use sovran_mcp::{McpClient, transport::StdioTransport};
129    ///
130    /// let transport = StdioTransport::new("npx", &["-y", "server-name"])?;
131    /// let client = McpClient::new(transport, None, None);
132    /// # Ok::<(), sovran_mcp::McpError>(())
133    /// ```
134    ///
135    /// Client with sampling and notification handlers:
136    /// ```no_run
137    /// use sovran_mcp::{McpClient, transport::StdioTransport, types::*, McpError};
138    /// use std::sync::Arc;
139    /// use serde_json::Value;
140    /// use url::Url;    ///
141    ///
142    /// use sovran_mcp::messaging::{LogLevel, NotificationMethod};
143    ///
144    /// // Handler for LLM completion requests
145    /// struct MySamplingHandler;
146    /// impl SamplingHandler for MySamplingHandler {
147    ///     fn handle_message(&self, request: CreateMessageRequest) -> Result<CreateMessageResponse, McpError> {
148    ///         // Process the completion request and return response
149    ///         Ok(CreateMessageResponse {
150    ///             content: MessageContent::Text(TextContent {
151    ///                 text: "AI response".to_string()
152    ///             }),
153    ///             model: "test-model".to_string(),
154    ///             role: Role::Assistant,
155    ///             stop_reason: Some("complete".to_string()),
156    ///             meta: None,
157    ///         })
158    ///     }
159    /// }
160    ///
161    /// // Handler for server notifications
162    /// struct MyNotificationHandler;
163    /// impl NotificationHandler for MyNotificationHandler {
164    ///     fn handle_resource_update(&self, uri: &Url) -> Result<(), McpError> {
165    ///         println!("Resource updated: {}", uri);
166    ///         Ok(())
167    ///     }
168    ///     fn handle_initialized(&self) {
169    ///         todo!()
170    ///     }
171    ///     fn handle_log_message(&self, level: &LogLevel, data: &Value, logger: &Option<String>) {
172    ///         todo!()
173    ///     }
174    ///     fn handle_progress_update(&self, token: &String, progress: &f64, total: &Option<f64>) {
175    ///         todo!()
176    ///     }
177    ///     fn handle_list_changed(&self, method: &NotificationMethod) {
178    ///         todo!()
179    ///     }
180    /// }
181    ///
182    /// // Create client with handlers
183    /// let transport = StdioTransport::new("npx", &["-y", "server-name"])?;
184    /// let client = McpClient::new(
185    ///     transport,
186    ///     Some(Box::new(MySamplingHandler)),
187    ///     Some(Box::new(MyNotificationHandler))
188    /// );
189    /// # Ok::<(), McpError>(())
190    /// ```
191    pub fn new(
192        transport: T,
193        sampling_handler: Option<Box<dyn SamplingHandler + Send>>,
194        notification_handler: Option<Box<dyn NotificationHandler + Send>>,
195    ) -> Self {
196        Self {
197            transport: Arc::new(transport),
198            request_id: Arc::new(AtomicU64::new(0)),
199            pending_requests: Arc::new(Mutex::new(HashMap::new())),
200            listener_handle: None,
201            sampling_handler: sampling_handler.map(Arc::new),
202            notification_handler: notification_handler.map(Arc::new),
203            stop_flag: Arc::new(AtomicBool::new(false)),
204            server_info: Arc::new(Mutex::new(None)),
205        }
206    }
207
208    /// Starts the client, establishing the transport connection and initializing the MCP protocol.
209    ///
210    /// This method must be called before using any other client operations. It:
211    /// - Opens the transport connection
212    /// - Starts the message handling thread
213    /// - Performs protocol initialization
214    ///
215    /// # Errors
216    ///
217    /// Returns `McpError` if:
218    /// - Transport connection fails
219    /// - Protocol initialization fails
220    /// - Message handling thread cannot be started
221    ///
222    /// # Examples
223    ///
224    /// ```no_run
225    /// use sovran_mcp::{McpClient, transport::StdioTransport};
226    ///
227    /// let transport = StdioTransport::new("npx", &["-y", "server-name"])?;
228    /// let mut client = McpClient::new(transport, None, None);
229    ///
230    /// // Start the client
231    /// client.start()?;
232    /// # Ok::<(), sovran_mcp::McpError>(())
233    /// ```
234    pub fn start(&mut self) -> Result<(), McpError> {
235        self.transport.open()?;
236
237        let handler = MessageHandler::new(
238            self.transport.clone(),
239            self.pending_requests.clone(),
240            self.sampling_handler.clone(),
241            self.notification_handler.clone(),
242        );
243
244        let transport = self.transport.clone();
245        let stop_flag = self.stop_flag.clone();
246
247        let handle_wrapper = Arc::new(Mutex::new(None));
248        let handle = thread::spawn(move || {
249            while !stop_flag.load(Ordering::SeqCst) {
250                match transport.receive() {
251                    Ok(message) => {
252                        debug!("Received message: {:?}", message);
253                        if let Err(e) = handler.handle_message(message) {
254                            warn!("Error handling message: {}", e);
255                        }
256                    }
257                    Err(e) => {
258                        warn!("Transport error: {}", e);
259                        break;
260                    }
261                }
262            }
263            debug!("Message handling thread exited");
264        });
265
266        *handle_wrapper.lock().unwrap() = Some(handle);
267        self.listener_handle = Some(handle_wrapper);
268
269        // Phase 1: Initialize request/response
270        self.initialize()?;
271        debug!("Phase 1 complete: Received initialize response");
272
273        // Phase 2: Send initialized notification
274        // Phase 2: Send initialized notification
275        debug!("Phase 2: Sending initialized notification");
276        self.transport.send(&JsonRpcMessage::Notification(
277            JsonRpcNotification::initialized(),
278        ))?;
279        debug!("Two-phase initialization complete");
280
281        Ok(())
282    }
283
284    /// Stops the client, cleaning up resources and closing the transport connection.
285    ///
286    /// This method:
287    /// - Signals the message handling thread to stop
288    /// - Closes the transport connection
289    /// - Cleans up any pending requests
290    ///
291    /// # Errors
292    ///
293    /// Returns `McpError` if:
294    /// - The transport close operation fails
295    /// - The message handling thread cannot be properly stopped
296    ///
297    /// # Examples
298    ///
299    /// ```
300    /// # use sovran_mcp::{McpClient, transport::StdioTransport};
301    /// # let transport = StdioTransport::new("npx", &["-y", "@modelcontextprotocol/server-everything"])?;
302    /// # let mut client = McpClient::new(transport, None, None);
303    /// # client.start()?;
304    /// // Use the client...
305    ///
306    /// // Clean up when done
307    /// client.stop()?;
308    /// # Ok::<(), sovran_mcp::McpError>(())
309    /// ```
310    pub fn stop(&mut self) -> Result<(), McpError> {
311        // Signal the thread to stop
312        self.stop_flag.store(true, Ordering::SeqCst);
313
314        // send a dummy command to generate an error which will
315        // unblock any pending receive.
316        _ = self.request("__internal/server_stop", None)?;
317
318        // Kill the transport
319        self.transport.close()?;
320
321        // Join the thread if it hasn't detached
322        if let Some(wrapper) = self.listener_handle.take() {
323            if let Some(handle) = wrapper.lock().unwrap().take() {
324                debug!("client::stop() attempting to join message handling thread");
325                handle.join().map_err(|_| McpError::ThreadJoinFailed)?;
326                debug!("client::stop() joined message handling thread");
327            }
328        }
329
330        Ok(())
331    }
332
333    fn request(
334        &self,
335        method: &str,
336        params: Option<serde_json::Value>,
337    ) -> Result<JsonRpcResponse, McpError> {
338        let id = self.request_id.fetch_add(1, Ordering::SeqCst);
339        let (tx, rx) = channel();
340
341        // Store the sender
342        {
343            let mut pending = self.pending_requests.lock().unwrap();
344            pending.insert(id, tx);
345        }
346
347        // Send the request
348        let request = JsonRpcRequest {
349            id,
350            method: method.to_string(),
351            params,
352            jsonrpc: Default::default(),
353        };
354        self.transport.send(&JsonRpcMessage::Request(request))?;
355
356        // Wait for response with timeout but DON'T remove the pending request
357        match rx.recv_timeout(std::time::Duration::from_secs(2)) {
358            Ok(response) => {
359                // Only remove on success
360                let mut pending = self.pending_requests.lock().unwrap();
361                pending.remove(&id);
362                Ok(response)
363            }
364            Err(e) => Err(McpError::RequestTimeout {
365                method: method.into(),
366                source: e,
367            }),
368        }
369    }
370
371    /// Executes a generic MCP command on the server.
372    ///
373    /// This is a lower-level method used internally by the specific command methods (list_tools, get_prompt, etc).
374    /// It can be used to implement custom commands when extending the protocol.
375    ///
376    /// # Type Parameters
377    ///
378    /// * `C` - A type implementing the `McpCommand` trait, which defines:
379    ///   - The command name (`COMMAND`)
380    ///   - The request type (`Request`)
381    ///   - The response type (`Response`)
382    ///
383    /// # Arguments
384    ///
385    /// * `request` - The command-specific request data
386    ///
387    /// # Errors
388    ///
389    /// Returns `McpError` if:
390    /// - The command execution fails on the server
391    /// - The request times out
392    /// - The response cannot be deserialized
393    ///
394    /// # Examples
395    ///
396    /// ```no_run
397    /// # use sovran_mcp::{McpClient, transport::StdioTransport, McpCommand};
398    /// use serde::{Serialize, Deserialize};
399    ///
400    /// // Define a custom command
401    /// #[derive(Debug, Clone)]
402    /// pub struct MyCommand;
403    ///
404    /// impl McpCommand for MyCommand {
405    ///     const COMMAND: &'static str = "custom/operation";
406    ///     type Request = MyRequest;
407    ///     type Response = MyResponse;
408    /// }
409    ///
410    /// #[derive(Debug, Serialize)]
411    /// pub struct MyRequest {
412    ///     data: String,
413    /// }
414    ///
415    /// #[derive(Debug, Deserialize)]
416    /// pub struct MyResponse {
417    ///     result: String,
418    /// }
419    ///
420    /// # let mut client = McpClient::new(
421    /// #     StdioTransport::new("npx", &["-y", "@myserver/dummy_server"])?,
422    /// #     None,
423    /// #     None
424    /// # );
425    /// # client.start()?;
426    /// // Execute the custom command
427    /// let request = MyRequest {
428    ///     data: "test".to_string(),
429    /// };
430    ///
431    /// let response = client.execute::<MyCommand>(request)?;
432    /// println!("Got result: {}", response.result);
433    /// # client.stop()?;
434    /// # Ok::<(), sovran_mcp::McpError>(())
435    /// ```
436    pub fn execute<C: McpCommand>(&self, request: C::Request) -> Result<C::Response, McpError> {
437        debug!("Executing command: {}", C::COMMAND); // Add this
438        let response = self.request(C::COMMAND, Some(serde_json::to_value(request)?))?;
439        debug!("Got response for: {}", C::COMMAND); // Add this
440
441        if let Some(error) = response.error {
442            return Err(McpError::CommandFailed {
443                command: C::COMMAND.to_string(),
444                error,
445            });
446        }
447
448        let result = response.result.ok_or_else(|| McpError::MissingResult)?;
449
450        Ok(serde_json::from_value(result)?)
451    }
452
453    fn initialize(&self) -> Result<&Self, McpError> {
454        let request = InitializeRequest {
455            protocol_version: LATEST_PROTOCOL_VERSION.to_string(),
456            capabilities: ClientCapabilities::default(),
457            client_info: Implementation {
458                name: "mcp-simple".to_string(),
459                version: env!("CARGO_PKG_VERSION").to_string(),
460            },
461        };
462
463        let response = self.execute::<Initialize>(request)?;
464        // Store the response
465        *self.server_info.lock().unwrap() = Some(response);
466
467        Ok(self)
468    }
469
470    /// Returns the server's capabilities as reported during initialization.
471    ///
472    /// # Errors
473    ///
474    /// Returns `McpError::ClientNotInitialized` if called before the client is started.
475    ///
476    /// # Examples
477    ///
478    /// ```
479    /// # use sovran_mcp::{McpClient, transport::StdioTransport};
480    /// # let mut client = McpClient::new(
481    /// #     StdioTransport::new("npx", &["-y", "@modelcontextprotocol/server-everything"])?,
482    /// #     None,
483    /// #     None
484    /// # );
485    /// # client.start()?;
486    /// let capabilities = client.server_capabilities()?;
487    ///
488    /// // Check specific capabilities
489    /// if let Some(prompts) = capabilities.prompts {
490    ///     println!("Server supports prompts with list_changed: {:?}",
491    ///         prompts.list_changed);
492    /// }
493    ///
494    /// if let Some(resources) = capabilities.resources {
495    ///     println!("Server supports resources with subscribe: {:?}",
496    ///         resources.subscribe);
497    /// }
498    /// # Ok::<(), sovran_mcp::McpError>(())
499    /// ```
500    pub fn server_capabilities(&self) -> Result<ServerCapabilities, McpError> {
501        self.server_info
502            .lock()
503            .unwrap()
504            .as_ref()
505            .map(|info| info.capabilities.clone())
506            .ok_or_else(|| McpError::ClientNotInitialized)
507    }
508
509    /// Returns the protocol version supported by the server.
510    ///
511    /// # Errors
512    ///
513    /// Returns `McpError::ClientNotInitialized` if called before the client is started.
514    ///
515    /// # Examples
516    ///
517    /// ```
518    /// # use sovran_mcp::{McpClient, transport::StdioTransport};
519    /// # let mut client = McpClient::new(
520    /// #     StdioTransport::new("npx", &["-y", "@modelcontextprotocol/server-everything"])?,
521    /// #     None,
522    /// #     None
523    /// # );
524    /// # client.start()?;
525    /// let version = client.server_version()?;
526    /// println!("Server supports MCP version: {}", version);
527    /// # Ok::<(), sovran_mcp::McpError>(())
528    /// ```
529    pub fn server_version(&self) -> Result<String, McpError> {
530        self.server_info
531            .lock()
532            .unwrap()
533            .as_ref()
534            .map(|info| info.protocol_version.clone())
535            .ok_or_else(|| McpError::ClientNotInitialized)
536    }
537
538    /// Checks if the server has a specific capability using a custom predicate.
539    ///
540    /// This is a lower-level method used by the specific capability checks. It allows
541    /// for custom capability testing logic.
542    ///
543    /// # Arguments
544    ///
545    /// * `check` - A closure that takes a reference to ServerCapabilities and returns a boolean
546    ///
547    /// # Examples
548    ///
549    /// ```
550    /// # use sovran_mcp::{McpClient, transport::StdioTransport};
551    /// # let mut client = McpClient::new(
552    /// #     StdioTransport::new("npx", &["-y", "@modelcontextprotocol/server-everything"])?,
553    /// #     None,
554    /// #     None
555    /// # );
556    /// # client.start()?;
557    /// // Check if server supports prompt list change notifications
558    /// let has_prompt_notifications = client.has_capability(|caps| {
559    ///     caps.prompts
560    ///         .as_ref()
561    ///         .and_then(|p| p.list_changed)
562    ///         .unwrap_or(false)
563    /// });
564    ///
565    /// println!("Prompt notifications supported: {}", has_prompt_notifications);
566    /// # Ok::<(), sovran_mcp::McpError>(())
567    /// ```
568    pub fn has_capability<F>(&self, check: F) -> bool
569    where
570        F: FnOnce(&ServerCapabilities) -> bool,
571    {
572        self.server_info
573            .lock()
574            .unwrap()
575            .as_ref()
576            .map(|info| check(&info.capabilities))
577            .unwrap_or(false)
578    }
579
580    /// Checks if the server supports resource operations.
581    ///
582    /// Resources are server-managed content that can be read and monitored for changes.
583    ///
584    /// # Returns
585    ///
586    /// Returns `true` if the server supports resources, `false` otherwise.
587    ///
588    /// # Examples
589    ///
590    /// ```
591    /// # use sovran_mcp::{McpClient, transport::StdioTransport};
592    /// # let mut client = McpClient::new(
593    /// #     StdioTransport::new("npx", &["-y", "@modelcontextprotocol/server-everything"])?,
594    /// #     None,
595    /// #     None
596    /// # );
597    /// # client.start()?;
598    /// if client.supports_resources() {
599    ///     let resources = client.list_resources()?;
600    ///     println!("Available resources: {}", resources.resources.len());
601    /// }
602    /// # Ok::<(), sovran_mcp::McpError>(())
603    /// ```
604    pub fn supports_resources(&self) -> bool {
605        self.has_capability(|caps| caps.resources.is_some())
606    }
607
608    /// Checks if the server supports prompt operations.
609    ///
610    /// Prompts are server-managed message templates that can be retrieved and processed.
611    ///
612    /// # Returns
613    ///
614    /// Returns `true` if the server supports prompts, `false` otherwise.
615    ///
616    /// # Examples
617    ///
618    /// ```
619    /// # use sovran_mcp::{McpClient, transport::StdioTransport};
620    /// # let mut client = McpClient::new(
621    /// #     StdioTransport::new("npx", &["-y", "@modelcontextprotocol/server-everything"])?,
622    /// #     None,
623    /// #     None
624    /// # );
625    /// # client.start()?;
626    /// if client.supports_prompts() {
627    ///     let prompts = client.list_prompts()?;
628    ///     println!("Available prompts: {}", prompts.prompts.len());
629    /// }
630    /// # Ok::<(), sovran_mcp::McpError>(())
631    /// ```
632    pub fn supports_prompts(&self) -> bool {
633        self.has_capability(|caps| caps.prompts.is_some())
634    }
635
636    /// Checks if the server supports tool operations.
637    ///
638    /// Tools are server-provided functions that can be called by the client.
639    ///
640    /// # Returns
641    ///
642    /// Returns `true` if the server supports tools, `false` otherwise.
643    ///
644    /// # Examples
645    ///
646    /// ```
647    /// # use sovran_mcp::{McpClient, transport::StdioTransport};
648    /// # let mut client = McpClient::new(
649    /// #     StdioTransport::new("npx", &["-y", "@modelcontextprotocol/server-everything"])?,
650    /// #     None,
651    /// #     None
652    /// # );
653    /// # client.start()?;
654    /// if client.supports_tools() {
655    ///     let tools = client.list_tools()?;
656    ///     println!("Available tools: {}", tools.tools.len());
657    /// }
658    /// # Ok::<(), sovran_mcp::McpError>(())
659    /// ```
660    pub fn supports_tools(&self) -> bool {
661        self.has_capability(|caps| caps.tools.is_some())
662    }
663
664    /// Checks if the server supports logging capabilities.
665    ///
666    /// # Returns
667    ///
668    /// Returns `true` if the server supports logging, `false` otherwise.
669    ///
670    /// # Examples
671    ///
672    /// ```
673    /// # use sovran_mcp::{McpClient, transport::StdioTransport};
674    /// # let mut client = McpClient::new(
675    /// #     StdioTransport::new("npx", &["-y", "@modelcontextprotocol/server-everything"])?,
676    /// #     None,
677    /// #     None
678    /// # );
679    /// # client.start()?;
680    /// if client.supports_logging() {
681    ///     println!("Server supports logging capabilities");
682    /// }
683    /// # Ok::<(), sovran_mcp::McpError>(())
684    /// ```
685    pub fn supports_logging(&self) -> bool {
686        self.has_capability(|caps| caps.logging.is_some())
687    }
688
689    /// Checks if the server supports resource subscriptions.
690    ///
691    /// Resource subscriptions allow the client to receive notifications when resources change.
692    ///
693    /// # Returns
694    ///
695    /// Returns `true` if the server supports resource subscriptions, `false` otherwise.
696    ///
697    /// # Examples
698    ///
699    /// ```
700    /// # use sovran_mcp::{McpClient, transport::StdioTransport};
701    /// # let mut client = McpClient::new(
702    /// #     StdioTransport::new("npx", &["-y", "@modelcontextprotocol/server-everything"])?,
703    /// #     None,
704    /// #     None
705    /// # );
706    /// # client.start()?;
707    /// if client.supports_resource_subscription() {
708    ///     println!("Server supports resource change notifications");
709    /// }
710    /// # Ok::<(), sovran_mcp::McpError>(())
711    /// ```
712    pub fn supports_resource_subscription(&self) -> bool {
713        self.has_capability(|caps| {
714            caps.resources
715                .as_ref()
716                .and_then(|r| r.subscribe)
717                .unwrap_or(false)
718        })
719    }
720
721    /// Checks if the server supports resource list change notifications.
722    pub fn supports_resource_list_changed(&self) -> bool {
723        self.has_capability(|caps| {
724            caps.resources
725                .as_ref()
726                .and_then(|r| r.list_changed)
727                .unwrap_or(false)
728        })
729    }
730
731    /// Checks if the server supports prompt list change notifications.
732    pub fn supports_prompt_list_changed(&self) -> bool {
733        self.has_capability(|caps| {
734            caps.prompts
735                .as_ref()
736                .and_then(|p| p.list_changed)
737                .unwrap_or(false)
738        })
739    }
740
741    /// Checks if the server supports a specific experimental feature.
742    ///
743    /// # Arguments
744    ///
745    /// * `feature` - The name of the experimental feature to check
746    ///
747    /// # Examples
748    ///
749    /// ```
750    /// # use sovran_mcp::{McpClient, transport::StdioTransport};
751    /// # let mut client = McpClient::new(
752    /// #     StdioTransport::new("npx", &["-y", "@modelcontextprotocol/server-everything"])?,
753    /// #     None,
754    /// #     None
755    /// # );
756    /// # client.start()?;
757    /// if client.supports_experimental_feature("my_feature") {
758    ///     println!("Server supports experimental feature 'my_feature'");
759    /// }
760    /// # Ok::<(), sovran_mcp::McpError>(())
761    /// ```
762    pub fn supports_experimental_feature(&self, feature: &str) -> bool {
763        self.has_capability(|caps| {
764            caps.experimental
765                .as_ref()
766                .and_then(|e| e.get(feature))
767                .is_some()
768        })
769    }
770
771    /// Lists all available tools provided by the server.
772    ///
773    /// # Errors
774    ///
775    /// Returns `McpError` if:
776    /// - The server doesn't support tools (`UnsupportedCapability`)
777    /// - The request fails or times out
778    /// - The response cannot be deserialized
779    ///
780    /// # Examples
781    ///
782    /// ```
783    /// # use sovran_mcp::{McpClient, transport::StdioTransport};
784    /// # let mut client = McpClient::new(
785    /// #     StdioTransport::new("npx", &["-y", "@modelcontextprotocol/server-everything"])?,
786    /// #     None,
787    /// #     None
788    /// # );
789    /// # client.start()?;
790    /// let tools = client.list_tools()?;
791    ///
792    /// for tool in tools.tools {
793    ///     println!("Tool: {} - {:?}", tool.name, tool.description);
794    /// }
795    /// # Ok::<(), sovran_mcp::McpError>(())
796    /// ```
797    pub fn list_tools(&self) -> Result<ListToolsResponse, McpError> {
798        if !self.supports_tools() {
799            return Err(McpError::UnsupportedCapability("tools"));
800        }
801        let request = ListToolsRequest {
802            cursor: None,
803            meta: None,
804        };
805        self.execute::<ListTools>(request)
806    }
807
808    /// Calls a tool on the server with the specified arguments.
809    ///
810    /// # Arguments
811    ///
812    /// * `name` - The name of the tool to call
813    /// * `arguments` - Optional JSON-formatted arguments for the tool
814    ///
815    /// # Errors
816    ///
817    /// Returns `McpError` if:
818    /// - The server doesn't support tools (`UnsupportedCapability`)
819    /// - The specified tool doesn't exist
820    /// - The arguments are invalid for the tool
821    /// - The request fails or times out
822    /// - The response cannot be deserialized
823    ///
824    /// # Examples
825    ///
826    /// Simple tool call (echo):
827    /// ```
828    /// # use sovran_mcp::{McpClient, transport::StdioTransport};
829    /// # use sovran_mcp::types::ToolResponseContent;
830    /// # let mut client = McpClient::new(
831    /// #     StdioTransport::new("npx", &["-y", "@modelcontextprotocol/server-everything"])?,
832    /// #     None,
833    /// #     None
834    /// # );
835    /// # client.start()?;
836    /// let response = client.call_tool(
837    ///     "echo".to_string(),
838    ///     Some(serde_json::json!({
839    ///         "message": "Hello, MCP!"
840    ///     }))
841    /// )?;
842    ///
843    /// // Handle the response
844    /// if let Some(content) = response.content.first() {
845    ///     match content {
846    ///         ToolResponseContent::Text { text } => println!("Got response: {}", text),
847    ///         _ => println!("Got non-text response"),
848    ///     }
849    /// }
850    /// # Ok::<(), sovran_mcp::McpError>(())
851    /// ```
852    ///
853    /// Tool with numeric arguments:
854    /// ```
855    /// # use sovran_mcp::{McpClient, transport::StdioTransport};
856    /// # use sovran_mcp::types::ToolResponseContent;
857    /// # let mut client = McpClient::new(
858    /// #     StdioTransport::new("npx", &["-y", "@modelcontextprotocol/server-everything"])?,
859    /// #     None,
860    /// #     None
861    /// # );
862    /// # client.start()?;
863    /// // Call an "add" tool that sums two numbers
864    /// let response = client.call_tool(
865    ///     "add".to_string(),
866    ///     Some(serde_json::json!({
867    ///         "a": 2,
868    ///         "b": 3
869    ///     }))
870    /// )?;
871    ///
872    /// // Process the response
873    /// if let Some(ToolResponseContent::Text { text }) = response.content.first() {
874    ///     println!("Result: {}", text); // "The sum of 2 and 3 is 5."
875    /// }
876    /// # Ok::<(), sovran_mcp::McpError>(())
877    /// ```
878    pub fn call_tool(
879        &self,
880        name: String,
881        arguments: Option<serde_json::Value>,
882    ) -> Result<CallToolResponse, McpError> {
883        if !self.supports_tools() {
884            return Err(McpError::UnsupportedCapability("tools"));
885        }
886        let request = CallToolRequest {
887            name,
888            arguments,
889            meta: None,
890        };
891        self.execute::<CallTool>(request)
892    }
893
894    /// Lists all available prompts from the server.
895    ///
896    /// # Errors
897    ///
898    /// Returns `McpError` if:
899    /// - The server doesn't support prompts (`UnsupportedCapability`)
900    /// - The request fails or times out
901    /// - The response cannot be deserialized
902    ///
903    /// # Examples
904    ///
905    /// ```
906    /// # use sovran_mcp::{McpClient, transport::StdioTransport};
907    /// # let mut client = McpClient::new(
908    /// #     StdioTransport::new("npx", &["-y", "@modelcontextprotocol/server-everything"])?,
909    /// #     None,
910    /// #     None
911    /// # );
912    /// # client.start()?;
913    /// let prompts = client.list_prompts()?;
914    ///
915    /// for prompt in prompts.prompts {
916    ///     println!("Prompt: {} - {:?}", prompt.name, prompt.description);
917    ///
918    ///     // Check for required arguments
919    ///     if let Some(args) = prompt.arguments {
920    ///         for arg in args {
921    ///             println!("  Argument: {} (required: {})",
922    ///                 arg.name,
923    ///                 arg.required.unwrap_or(false)
924    ///             );
925    ///         }
926    ///     }
927    /// }
928    /// # Ok::<(), sovran_mcp::McpError>(())
929    /// ```
930    pub fn list_prompts(&self) -> Result<ListPromptsResponse, McpError> {
931        if !self.supports_prompts() {
932            return Err(McpError::UnsupportedCapability("prompts"));
933        }
934        let request = ListPromptsRequest {
935            cursor: None,
936            meta: None,
937        };
938        self.execute::<ListPrompts>(request)
939    }
940
941    /// Retrieves a specific prompt from the server with optional arguments.
942    ///
943    /// # Arguments
944    ///
945    /// * `name` - The name of the prompt to retrieve
946    /// * `arguments` - Optional key-value pairs of arguments for the prompt
947    ///
948    /// # Errors
949    ///
950    /// Returns `McpError` if:
951    /// - The server doesn't support prompts (`UnsupportedCapability`)
952    /// - The specified prompt doesn't exist
953    /// - Required arguments are missing
954    /// - The request fails or times out
955    /// - The response cannot be deserialized
956    ///
957    /// # Examples
958    ///
959    /// Simple prompt without arguments:
960    /// ```
961    /// # use sovran_mcp::{McpClient, transport::StdioTransport};
962    /// # use sovran_mcp::types::Role;
963    /// # let mut client = McpClient::new(
964    /// #     StdioTransport::new("npx", &["-y", "@modelcontextprotocol/server-everything"])?,
965    /// #     None,
966    /// #     None
967    /// # );
968    /// # client.start()?;
969    /// let response = client.get_prompt("simple_prompt".to_string(), None)?;
970    ///
971    /// // Process the messages
972    /// for message in response.messages {
973    ///     match message.role {
974    ///         Role::System => println!("System: {:?}", message.content),
975    ///         Role::User => println!("User: {:?}", message.content),
976    ///         Role::Assistant => println!("Assistant: {:?}", message.content),
977    ///     }
978    /// }
979    /// # Ok::<(), sovran_mcp::McpError>(())
980    /// ```
981    ///
982    /// Prompt with arguments:
983    /// ```
984    /// # use sovran_mcp::{McpClient, transport::StdioTransport};
985    /// # use std::collections::HashMap;
986    /// # use sovran_mcp::types::PromptContent;
987    /// # let mut client = McpClient::new(
988    /// #     StdioTransport::new("npx", &["-y", "@modelcontextprotocol/server-everything"])?,
989    /// #     None,
990    /// #     None
991    /// # );
992    /// # client.start()?;
993    /// let mut args = HashMap::new();
994    /// args.insert("temperature".to_string(), "0.7".to_string());
995    /// args.insert("style".to_string(), "formal".to_string());
996    ///
997    /// let response = client.get_prompt("complex_prompt".to_string(), Some(args))?;
998    ///
999    /// // Process different content types
1000    /// for message in response.messages {
1001    ///     match &message.content {
1002    ///         PromptContent::Text(text) => {
1003    ///             println!("{}: {}", message.role, text.text);
1004    ///         }
1005    ///         PromptContent::Image(img) => {
1006    ///             println!("{}: Image ({:?})", message.role, img.mime_type);
1007    ///         }
1008    ///         PromptContent::Resource(res) => {
1009    ///             println!("{}: Resource at {}", message.role, res.resource.uri);
1010    ///         }
1011    ///     }
1012    /// }
1013    /// # client.stop()?;
1014    /// # Ok::<(), sovran_mcp::McpError>(())
1015    /// ```
1016    pub fn get_prompt(
1017        &self,
1018        name: String,
1019        arguments: Option<HashMap<String, String>>,
1020    ) -> Result<GetPromptResponse, McpError> {
1021        if !self.supports_prompts() {
1022            return Err(McpError::UnsupportedCapability("prompts"));
1023        }
1024        let request = GetPromptRequest { name, arguments };
1025        self.execute::<GetPrompt>(request)
1026    }
1027
1028    /// Lists all available resources from the server.
1029    ///
1030    /// # Errors
1031    ///
1032    /// Returns `McpError` if:
1033    /// - The server doesn't support resources (`UnsupportedCapability`)
1034    /// - The request fails or times out
1035    /// - The response cannot be deserialized
1036    ///
1037    /// # Examples
1038    ///
1039    /// ```
1040    /// # use sovran_mcp::{McpClient, transport::StdioTransport};
1041    /// # let mut client = McpClient::new(
1042    /// #     StdioTransport::new("npx", &["-y", "@modelcontextprotocol/server-everything"])?,
1043    /// #     None,
1044    /// #     None
1045    /// # );
1046    /// # client.start()?;
1047    /// let resources = client.list_resources()?;
1048    ///
1049    /// for resource in resources.resources {
1050    ///     println!("Resource: {} ({})", resource.name, resource.uri);
1051    ///     if let Some(mime_type) = resource.mime_type {
1052    ///         println!("  Type: {}", mime_type);
1053    ///     }
1054    ///     if let Some(description) = resource.description {
1055    ///         println!("  Description: {}", description);
1056    ///     }
1057    /// }
1058    /// # Ok::<(), sovran_mcp::McpError>(())
1059    /// ```
1060    pub fn list_resources(&self) -> Result<ListResourcesResponse, McpError> {
1061        if !self.supports_resources() {
1062            return Err(McpError::UnsupportedCapability("resources"));
1063        }
1064        let request = ListResourcesRequest { cursor: None };
1065        self.execute::<ListResources>(request)
1066    }
1067
1068    /// Reads the content of a specific resource.
1069    ///
1070    /// # Arguments
1071    ///
1072    /// * `uri` - The URI of the resource to read
1073    ///
1074    /// # Errors
1075    ///
1076    /// Returns `McpError` if:
1077    /// - The server doesn't support resources (`UnsupportedCapability`)
1078    /// - The specified resource doesn't exist
1079    /// - The request fails or times out
1080    /// - The response cannot be deserialized
1081    ///
1082    /// # Examples
1083    ///
1084    /// ```
1085    /// # use sovran_mcp::{McpClient, transport::StdioTransport};
1086    /// # use sovran_mcp::types::ResourceContent;
1087    /// # let mut client = McpClient::new(
1088    /// #     StdioTransport::new("npx", &["-y", "@modelcontextprotocol/server-everything"])?,
1089    /// #     None,
1090    /// #     None
1091    /// # );
1092    /// # client.start()?;
1093    /// # let resources = client.list_resources()?;
1094    /// # let resource = &resources.resources[0];
1095    /// // Read a resource's contents
1096    /// let content = client.read_resource(&resource.uri)?;
1097    ///
1098    /// // Handle different content types
1099    /// for item in content.contents {
1100    ///     match item {
1101    ///         ResourceContent::Text(text) => {
1102    ///             println!("Text content: {}", text.text);
1103    ///             if let Some(mime) = text.mime_type {
1104    ///                 println!("MIME type: {}", mime);
1105    ///             }
1106    ///         }
1107    ///         ResourceContent::Blob(blob) => {
1108    ///             println!("Binary content ({} bytes)", blob.blob.len());
1109    ///             if let Some(mime) = blob.mime_type {
1110    ///                 println!("MIME type: {}", mime);
1111    ///             }
1112    ///         }
1113    ///     }
1114    /// }
1115    /// # Ok::<(), sovran_mcp::McpError>(())
1116    /// ```
1117    pub fn read_resource(&self, uri: &Url) -> Result<ReadResourceResponse, McpError> {
1118        if !self.supports_resources() {
1119            return Err(McpError::UnsupportedCapability("resources"));
1120        }
1121        let request = ReadResourceRequest { uri: uri.clone() };
1122        self.execute::<ReadResource>(request)
1123    }
1124
1125    /// Subscribes to changes for a specific resource.
1126    ///
1127    /// When subscribed, the client will receive notifications through the `NotificationHandler`
1128    /// whenever the resource changes.
1129    ///
1130    /// # Arguments
1131    ///
1132    /// * `uri` - The URI of the resource to subscribe to
1133    ///
1134    /// # Errors
1135    ///
1136    /// Returns `McpError` if:
1137    /// - The server doesn't support resource subscriptions (`UnsupportedCapability`)
1138    /// - The specified resource doesn't exist
1139    /// - The request fails or times out
1140    ///
1141    /// # Examples
1142    ///
1143    /// ```no_run
1144    /// # use sovran_mcp::{McpClient, transport::StdioTransport, messaging::*, types::*, McpError};
1145    /// # use url::Url;
1146    /// # use std::sync::Arc;
1147    /// # use serde_json::Value;
1148    /// // Create a notification handler
1149    /// struct MyNotificationHandler;
1150    /// impl NotificationHandler for MyNotificationHandler {
1151    ///     fn handle_resource_update(&self, uri: &Url) -> Result<(), McpError> {
1152    ///         println!("Resource updated: {}", uri);
1153    ///         Ok(())
1154    ///     }
1155    ///     fn handle_log_message(&self, level: &LogLevel, data: &Value, logger: &Option<String>) {
1156    ///         todo!()
1157    ///     }
1158    ///     fn handle_progress_update(&self, token: &String, progress: &f64, total: &Option<f64>) {
1159    ///         todo!()
1160    ///     }
1161    ///     fn handle_initialized(&self) {
1162    ///         todo!()
1163    ///     }
1164    ///     fn handle_list_changed(&self, method: &NotificationMethod) {
1165    ///         todo!()
1166    ///     }
1167    /// }
1168    ///
1169    /// # let transport = StdioTransport::new("npx", &["-y", "@modelcontextprotocol/server-everything"])?;
1170    /// # let mut client = McpClient::new(
1171    /// #     transport,
1172    /// #     None,
1173    /// #     Some(Box::new(MyNotificationHandler))
1174    /// # );
1175    /// # client.start()?;
1176    /// # let resources = client.list_resources()?;
1177    /// # let resource = &resources.resources[0];
1178    /// // Subscribe to a resource
1179    /// if client.supports_resource_subscription() {
1180    ///     client.subscribe(&resource.uri)?;
1181    ///     println!("Subscribed to {}", resource.uri);
1182    ///
1183    ///     // ... wait for notifications through handler ...
1184    ///
1185    ///     // Unsubscribe when done
1186    ///     client.unsubscribe(&resource.uri)?;
1187    /// }
1188    /// # Ok::<(), McpError>(())
1189    /// ```
1190    pub fn subscribe(&self, uri: &Url) -> Result<EmptyResult, McpError> {
1191        if !self.supports_resource_subscription() {
1192            return Err(McpError::UnsupportedCapability("resources"));
1193        }
1194        let request = SubscribeRequest { uri: uri.clone() };
1195        self.execute::<Subscribe>(request)
1196    }
1197
1198    /// Unsubscribes from changes for a specific resource.
1199    ///
1200    /// # Arguments
1201    ///
1202    /// * `uri` - The URI of the resource to unsubscribe from
1203    ///
1204    /// # Errors
1205    ///
1206    /// Returns `McpError` if:
1207    /// - The server doesn't support resource subscriptions (`UnsupportedCapability`)
1208    /// - The specified resource doesn't exist
1209    /// - The request fails or times out
1210    ///
1211    /// # Examples
1212    ///
1213    /// See the `subscribe` method for a complete example including unsubscribe.
1214    pub fn unsubscribe(&self, uri: &Url) -> Result<EmptyResult, McpError> {
1215        if !self.supports_resource_subscription() {
1216            return Err(McpError::UnsupportedCapability("resources"));
1217        }
1218        let request = UnsubscribeRequest { uri: uri.clone() };
1219        self.execute::<Unsubscribe>(request)
1220    }
1221}