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}