Skip to main content

claude_code/
client.rs

1//! Session-based client for multi-turn interactions with Claude Code.
2//!
3//! This module provides [`ClaudeSdkClient`], which maintains a persistent session
4//! for multi-turn conversations. Use this when you need to send follow-up queries,
5//! interrupt operations, or manage the session lifecycle manually.
6//!
7//! For one-off queries without session management, see [`query()`](crate::query_fn::query).
8
9use std::collections::HashMap;
10use std::sync::Arc;
11use std::time::Duration;
12
13use futures::{Stream, StreamExt};
14use serde_json::Value;
15use tokio::task::JoinHandle;
16use tracing::warn;
17
18use crate::errors::{CLIConnectionError, Error, Result};
19use crate::query::{Query, build_hooks_config};
20use crate::sdk_mcp::McpSdkServer;
21use crate::transport::subprocess_cli::{Prompt as TransportPrompt, SubprocessCliTransport};
22use crate::transport::{Transport, TransportFactory};
23use crate::types::{
24    ClaudeAgentOptions, McpServerConfig, McpServersOption, McpStatusResponse, Message,
25};
26
27/// Input prompt for a query — either plain text or structured messages.
28///
29/// # Variants
30///
31/// - `Text` — A simple text prompt string.
32/// - `Messages` — A list of structured JSON messages for fine-grained control
33///   over the conversation input. Required when using [`can_use_tool`](crate::ClaudeAgentOptions::can_use_tool)
34///   callbacks.
35#[derive(Debug, Clone, PartialEq)]
36pub enum InputPrompt {
37    /// One plain-text prompt.
38    Text(String),
39    /// Pre-built structured protocol messages.
40    Messages(Vec<Value>),
41}
42
43/// Session-based client for multi-turn Claude Code interactions.
44///
45/// `ClaudeSdkClient` maintains a connection to the Claude Code CLI subprocess,
46/// allowing multiple queries within the same conversation context. The session
47/// preserves conversation history across calls.
48///
49/// # Lifecycle
50///
51/// 1. Create a client with [`new()`](Self::new)
52/// 2. Call [`connect()`](Self::connect) to start the session
53/// 3. Send queries with [`query()`](Self::query) and receive responses with
54///    [`receive_message()`](Self::receive_message) or [`receive_response()`](Self::receive_response)
55/// 4. Call [`disconnect()`](Self::disconnect) when done
56///
57/// # Concurrency
58///
59/// After connection, [`query()`](Self::query), [`interrupt()`](Self::interrupt),
60/// and control methods take `&self`, allowing concurrent operations from different
61/// tasks. Only [`connect()`](Self::connect), [`disconnect()`](Self::disconnect),
62/// and [`receive_message()`](Self::receive_message) require `&mut self`.
63///
64/// # Example
65///
66/// ```rust,no_run
67/// # use claude_code::{ClaudeSdkClient, InputPrompt, Message};
68/// # async fn example() -> claude_code::Result<()> {
69///     let mut client = ClaudeSdkClient::new(None, None);
70///     client.connect(None).await?;
71///
72///     client.query(InputPrompt::Text("Hello!".into()), "session-1").await?;
73///     let messages = client.receive_response().await?;
74///
75///     client.disconnect().await?;
76/// # Ok(())
77/// # }
78/// ```
79pub struct ClaudeSdkClient {
80    options: ClaudeAgentOptions,
81    transport_factory: Option<Box<dyn TransportFactory>>,
82    query: Option<Query>,
83    initial_message_stream_task: Option<JoinHandle<Result<()>>>,
84}
85
86/// Adapter that wraps a single pre-built transport instance as a one-shot factory.
87struct SingleUseTransportFactory(std::sync::Mutex<Option<Box<dyn Transport>>>);
88
89impl TransportFactory for SingleUseTransportFactory {
90    fn create_transport(&self) -> Result<Box<dyn Transport>> {
91        self.0
92            .lock()
93            .map_err(|_| Error::Other("Transport factory lock poisoned".to_string()))?
94            .take()
95            .ok_or_else(|| {
96                Error::Other(
97                    "Single-use transport already consumed. Use a TransportFactory for reconnect support."
98                        .to_string(),
99                )
100            })
101    }
102}
103
104impl ClaudeSdkClient {
105    /// Creates a new `ClaudeSdkClient` with optional configuration and transport factory.
106    ///
107    /// # Arguments
108    ///
109    /// * `options` — Optional [`ClaudeAgentOptions`] for configuring the session.
110    ///   If `None`, defaults are used.
111    /// * `transport_factory` — Optional [`TransportFactory`] for creating transport
112    ///   instances on each [`connect()`](Self::connect) call. If `None`, the default
113    ///   [`SubprocessCliTransport`] is used. Using a factory enables reconnect after
114    ///   disconnect with the same client instance.
115    ///
116    /// # Example
117    ///
118    /// ```rust
119    /// use claude_code::ClaudeSdkClient;
120    ///
121    /// let _client = ClaudeSdkClient::new(None, None);
122    /// ```
123    pub fn new(
124        options: Option<ClaudeAgentOptions>,
125        transport_factory: Option<Box<dyn TransportFactory>>,
126    ) -> Self {
127        Self {
128            options: options.unwrap_or_default(),
129            transport_factory,
130            query: None,
131            initial_message_stream_task: None,
132        }
133    }
134
135    /// Creates a new `ClaudeSdkClient` with a single-use custom transport.
136    ///
137    /// The transport is consumed on the first [`connect()`](Self::connect). Subsequent
138    /// `connect()` calls after [`disconnect()`](Self::disconnect) will return an error.
139    /// For reconnect support with custom transports, use [`new()`](Self::new) with a
140    /// [`TransportFactory`].
141    ///
142    /// # Example
143    ///
144    /// ```rust
145    /// use claude_code::transport::subprocess_cli::{Prompt, SubprocessCliTransport};
146    /// use claude_code::ClaudeSdkClient;
147    ///
148    /// let transport = SubprocessCliTransport::new(Prompt::Messages, Default::default()).unwrap();
149    /// let _client = ClaudeSdkClient::new_with_transport(None, Box::new(transport));
150    /// ```
151    pub fn new_with_transport(
152        options: Option<ClaudeAgentOptions>,
153        transport: Box<dyn Transport>,
154    ) -> Self {
155        Self {
156            options: options.unwrap_or_default(),
157            transport_factory: Some(Box::new(SingleUseTransportFactory(std::sync::Mutex::new(
158                Some(transport),
159            )))),
160            query: None,
161            initial_message_stream_task: None,
162        }
163    }
164
165    async fn handle_initial_message_stream_task(&mut self, abort_running: bool) -> Result<()> {
166        let Some(task) = self.initial_message_stream_task.take() else {
167            return Ok(());
168        };
169
170        if abort_running && !task.is_finished() {
171            task.abort();
172        }
173
174        match task.await {
175            Ok(Ok(())) => Ok(()),
176            Ok(Err(err)) => {
177                if abort_running {
178                    warn!("Initial message stream task ended with error during shutdown: {err}");
179                    Ok(())
180                } else {
181                    Err(err)
182                }
183            }
184            Err(join_err) => {
185                if join_err.is_cancelled() {
186                    Ok(())
187                } else {
188                    let message = format!("Initial message stream task panicked: {join_err}");
189                    if abort_running {
190                        warn!("{message}");
191                        Ok(())
192                    } else {
193                        Err(Error::Other(message))
194                    }
195                }
196            }
197        }
198    }
199
200    fn initialize_timeout() -> Duration {
201        let timeout_ms = std::env::var("CLAUDE_CODE_STREAM_CLOSE_TIMEOUT")
202            .ok()
203            .and_then(|value| value.parse::<u64>().ok())
204            .unwrap_or(60_000);
205        Duration::from_secs_f64((timeout_ms as f64 / 1000.0).max(60.0))
206    }
207
208    fn extract_sdk_mcp_servers(options: &ClaudeAgentOptions) -> HashMap<String, Arc<McpSdkServer>> {
209        let mut servers = HashMap::new();
210        if let McpServersOption::Servers(configs) = &options.mcp_servers {
211            for (name, config) in configs {
212                if let McpServerConfig::Sdk(sdk_config) = config {
213                    servers.insert(name.clone(), sdk_config.instance.clone());
214                }
215            }
216        }
217        servers
218    }
219
220    /// Establishes a connection to the Claude Code CLI and starts the session.
221    ///
222    /// If an existing connection exists, it is disconnected first.
223    ///
224    /// # Arguments
225    ///
226    /// * `prompt` — Optional initial prompt to send upon connection. When using
227    ///   `can_use_tool`, this must be [`InputPrompt::Messages`], not `Text`.
228    ///
229    /// # Errors
230    ///
231    /// Returns an error if:
232    /// - The CLI executable is not found
233    /// - `can_use_tool` is set with a `Text` prompt (requires `Messages`)
234    /// - `can_use_tool` is set alongside `permission_prompt_tool_name`
235    /// - The subprocess fails to start
236    ///
237    /// # Example
238    ///
239    /// ```rust,no_run
240    /// use claude_code::{ClaudeSdkClient, InputPrompt};
241    ///
242    /// # async fn example() -> claude_code::Result<()> {
243    /// let mut client = ClaudeSdkClient::new(None, None);
244    /// client.connect(Some(InputPrompt::Text("Hello".to_string()))).await?;
245    /// client.disconnect().await?;
246    /// # Ok(())
247    /// # }
248    /// ```
249    pub async fn connect(&mut self, prompt: Option<InputPrompt>) -> Result<()> {
250        self.handle_initial_message_stream_task(true).await?;
251
252        if self.query.is_some() {
253            self.disconnect().await?;
254        }
255
256        if self.options.can_use_tool.is_some() {
257            if matches!(prompt, Some(InputPrompt::Text(_))) {
258                return Err(Error::Other(
259                    "can_use_tool callback requires streaming mode. Please provide prompt as messages."
260                        .to_string(),
261                ));
262            }
263            if self.options.permission_prompt_tool_name.is_some() {
264                return Err(Error::Other(
265                    "can_use_tool callback cannot be used with permission_prompt_tool_name."
266                        .to_string(),
267                ));
268            }
269        }
270
271        let mut configured_options = self.options.clone();
272        if configured_options.can_use_tool.is_some() {
273            configured_options.permission_prompt_tool_name = Some("stdio".to_string());
274        }
275
276        let transport_prompt = match &prompt {
277            Some(InputPrompt::Text(text)) => TransportPrompt::Text(text.clone()),
278            _ => TransportPrompt::Messages,
279        };
280
281        let mut transport: Box<dyn Transport> = if let Some(factory) = &self.transport_factory {
282            factory.create_transport()?
283        } else {
284            Box::new(SubprocessCliTransport::new(
285                transport_prompt,
286                configured_options.clone(),
287            )?)
288        };
289        transport.connect().await?;
290
291        let hooks = configured_options.hooks.clone().unwrap_or_default();
292        let sdk_mcp_servers = Self::extract_sdk_mcp_servers(&configured_options);
293        let (hooks_config, hook_callbacks) = build_hooks_config(&hooks);
294
295        let (reader, writer, close_handle) = transport.into_split()?;
296
297        let mut query = Query::start(
298            reader,
299            writer,
300            close_handle,
301            true,
302            configured_options.can_use_tool.clone(),
303            hook_callbacks,
304            sdk_mcp_servers,
305            configured_options.agents.clone(),
306            Self::initialize_timeout(),
307        );
308        query.initialize(hooks_config).await?;
309
310        if let Some(InputPrompt::Messages(messages)) = prompt {
311            query.send_input_messages(messages).await?;
312        }
313
314        self.query = Some(query);
315        Ok(())
316    }
317
318    /// Establishes a connection and sends initial prompt messages from a stream.
319    ///
320    /// This is a Rust-idiomatic equivalent of Python SDK `connect(AsyncIterable)`.
321    /// The stream is consumed in a background task so this method returns once
322    /// connection is established. Unlike one-off query streaming helpers, this
323    /// keeps stdin open so the session can continue with follow-up
324    /// [`query()`](Self::query) calls.
325    ///
326    /// To synchronously wait for stream completion and surface stream write
327    /// errors, call [`wait_for_initial_messages()`](Self::wait_for_initial_messages).
328    ///
329    /// # Errors
330    ///
331    /// Returns the same errors as [`connect()`](Self::connect), plus errors
332    /// when starting the background stream task.
333    ///
334    /// # Example
335    ///
336    /// ```rust,no_run
337    /// use claude_code::ClaudeSdkClient;
338    /// use futures::stream;
339    /// use serde_json::json;
340    ///
341    /// # async fn example() -> claude_code::Result<()> {
342    /// let mut client = ClaudeSdkClient::new(None, None);
343    /// client.connect_with_messages(stream::iter(vec![
344    ///     json!({"type":"user","message":{"role":"user","content":"hello"}}),
345    /// ])).await?;
346    /// client.wait_for_initial_messages().await?;
347    /// client.disconnect().await?;
348    /// # Ok(())
349    /// # }
350    /// ```
351    pub async fn connect_with_messages<S>(&mut self, prompt: S) -> Result<()>
352    where
353        S: Stream<Item = Value> + Send + Unpin + 'static,
354    {
355        self.connect(None).await?;
356
357        let query = self.query.as_ref().ok_or_else(|| {
358            Error::CLIConnection(CLIConnectionError::new(
359                "Not connected. Call connect() first.",
360            ))
361        })?;
362
363        self.initial_message_stream_task = Some(query.spawn_input_from_stream(prompt)?);
364        Ok(())
365    }
366
367    /// Waits for completion of the initial background message stream task.
368    ///
369    /// This is only relevant after calling [`connect_with_messages()`](Self::connect_with_messages).
370    /// If no background stream is active, this returns immediately.
371    ///
372    /// # Example
373    ///
374    /// ```rust,no_run
375    /// use claude_code::ClaudeSdkClient;
376    ///
377    /// # async fn example() -> claude_code::Result<()> {
378    /// let mut client = ClaudeSdkClient::new(None, None);
379    /// client.wait_for_initial_messages().await?;
380    /// # Ok(())
381    /// # }
382    /// ```
383    pub async fn wait_for_initial_messages(&mut self) -> Result<()> {
384        self.handle_initial_message_stream_task(false).await
385    }
386
387    /// Sends a query within the current session.
388    ///
389    /// The session must be connected first via [`connect()`](Self::connect).
390    /// After sending, use [`receive_message()`](Self::receive_message) or
391    /// [`receive_response()`](Self::receive_response) to get the response.
392    ///
393    /// # Arguments
394    ///
395    /// * `prompt` — The prompt to send (text or structured messages).
396    /// * `session_id` — Session identifier for the query.
397    ///
398    /// # Errors
399    ///
400    /// Returns [`CLIConnectionError`] if not connected.
401    ///
402    /// # Example
403    ///
404    /// ```rust,no_run
405    /// use claude_code::{ClaudeSdkClient, InputPrompt};
406    ///
407    /// # async fn example() -> claude_code::Result<()> {
408    /// let mut client = ClaudeSdkClient::new(None, None);
409    /// client.connect(None).await?;
410    /// client.query(InputPrompt::Text("Summarize this repo".into()), "default").await?;
411    /// client.disconnect().await?;
412    /// # Ok(())
413    /// # }
414    /// ```
415    pub async fn query(&self, prompt: InputPrompt, session_id: &str) -> Result<()> {
416        let query = self.query.as_ref().ok_or_else(|| {
417            Error::CLIConnection(CLIConnectionError::new(
418                "Not connected. Call connect() first.",
419            ))
420        })?;
421
422        match prompt {
423            InputPrompt::Text(text) => {
424                query.send_user_message(&text, session_id).await?;
425            }
426            InputPrompt::Messages(messages) => {
427                for mut message in messages {
428                    if let Value::Object(ref mut obj) = message
429                        && !obj.contains_key("session_id")
430                    {
431                        obj.insert(
432                            "session_id".to_string(),
433                            Value::String(session_id.to_string()),
434                        );
435                    }
436                    query.send_raw_message(message).await?;
437                }
438            }
439        }
440
441        Ok(())
442    }
443
444    /// Streams JSON message prompts within the current session.
445    ///
446    /// # Errors
447    ///
448    /// Returns [`CLIConnectionError`] if not connected.
449    ///
450    /// # Example
451    ///
452    /// ```rust,no_run
453    /// use claude_code::ClaudeSdkClient;
454    /// use futures::stream;
455    /// use serde_json::json;
456    ///
457    /// # async fn example() -> claude_code::Result<()> {
458    /// let mut client = ClaudeSdkClient::new(None, None);
459    /// client.connect(None).await?;
460    /// client
461    ///     .query_stream(
462    ///         stream::iter(vec![json!({"type":"user","message":{"role":"user","content":"hello"}})]),
463    ///         "default",
464    ///     )
465    ///     .await?;
466    /// client.disconnect().await?;
467    /// # Ok(())
468    /// # }
469    /// ```
470    pub async fn query_stream<S>(&self, prompt: S, session_id: &str) -> Result<()>
471    where
472        S: Stream<Item = Value> + Unpin,
473    {
474        let query = self.query.as_ref().ok_or_else(|| {
475            Error::CLIConnection(CLIConnectionError::new(
476                "Not connected. Call connect() first.",
477            ))
478        })?;
479
480        let session_id = session_id.to_string();
481        let mapped = prompt.map(move |mut message| {
482            if let Value::Object(ref mut obj) = message
483                && !obj.contains_key("session_id")
484            {
485                obj.insert("session_id".to_string(), Value::String(session_id.clone()));
486            }
487            message
488        });
489        query.send_input_from_stream(mapped).await
490    }
491
492    /// Receives a single message from the current query.
493    ///
494    /// Returns `None` when no more messages are available.
495    ///
496    /// # Errors
497    ///
498    /// Returns [`CLIConnectionError`] if not connected.
499    ///
500    /// # Example
501    ///
502    /// ```rust,no_run
503    /// use claude_code::ClaudeSdkClient;
504    ///
505    /// # async fn example() -> claude_code::Result<()> {
506    /// let mut client = ClaudeSdkClient::new(None, None);
507    /// client.connect(None).await?;
508    /// let _next = client.receive_message().await?;
509    /// client.disconnect().await?;
510    /// # Ok(())
511    /// # }
512    /// ```
513    pub async fn receive_message(&mut self) -> Result<Option<Message>> {
514        let query = self.query.as_mut().ok_or_else(|| {
515            Error::CLIConnection(CLIConnectionError::new(
516                "Not connected. Call connect() first.",
517            ))
518        })?;
519        query.receive_next_message().await
520    }
521
522    /// Receives all messages for the current query until a [`Message::Result`] is received.
523    ///
524    /// # Errors
525    ///
526    /// Returns [`CLIConnectionError`] if not connected.
527    ///
528    /// # Example
529    ///
530    /// ```rust,no_run
531    /// use claude_code::{ClaudeSdkClient, InputPrompt};
532    ///
533    /// # async fn example() -> claude_code::Result<()> {
534    /// let mut client = ClaudeSdkClient::new(None, None);
535    /// client.connect(None).await?;
536    /// client.query(InputPrompt::Text("Hi".into()), "default").await?;
537    /// let _messages = client.receive_response().await?;
538    /// client.disconnect().await?;
539    /// # Ok(())
540    /// # }
541    /// ```
542    pub async fn receive_response(&mut self) -> Result<Vec<Message>> {
543        let mut messages = Vec::new();
544        while let Some(message) = self.receive_message().await? {
545            let is_result = matches!(message, Message::Result(_));
546            messages.push(message);
547            if is_result {
548                break;
549            }
550        }
551        Ok(messages)
552    }
553
554    /// Interrupts the current operation.
555    ///
556    /// # Errors
557    ///
558    /// Returns [`CLIConnectionError`] if not connected.
559    ///
560    /// # Example
561    ///
562    /// ```rust,no_run
563    /// use claude_code::ClaudeSdkClient;
564    ///
565    /// # async fn example() -> claude_code::Result<()> {
566    /// let mut client = ClaudeSdkClient::new(None, None);
567    /// client.connect(None).await?;
568    /// client.interrupt().await?;
569    /// client.disconnect().await?;
570    /// # Ok(())
571    /// # }
572    /// ```
573    pub async fn interrupt(&self) -> Result<()> {
574        let query = self.query.as_ref().ok_or_else(|| {
575            Error::CLIConnection(CLIConnectionError::new(
576                "Not connected. Call connect() first.",
577            ))
578        })?;
579        query.interrupt().await
580    }
581
582    /// Changes the permission mode for the current session.
583    ///
584    /// # Errors
585    ///
586    /// Returns [`CLIConnectionError`] if not connected.
587    ///
588    /// # Example
589    ///
590    /// ```rust,no_run
591    /// use claude_code::ClaudeSdkClient;
592    ///
593    /// # async fn example() -> claude_code::Result<()> {
594    /// let mut client = ClaudeSdkClient::new(None, None);
595    /// client.connect(None).await?;
596    /// client.set_permission_mode("plan").await?;
597    /// client.disconnect().await?;
598    /// # Ok(())
599    /// # }
600    /// ```
601    pub async fn set_permission_mode(&self, mode: &str) -> Result<()> {
602        let query = self.query.as_ref().ok_or_else(|| {
603            Error::CLIConnection(CLIConnectionError::new(
604                "Not connected. Call connect() first.",
605            ))
606        })?;
607        query.set_permission_mode(mode).await
608    }
609
610    /// Changes the model used for the current session.
611    ///
612    /// # Errors
613    ///
614    /// Returns [`CLIConnectionError`] if not connected.
615    ///
616    /// # Example
617    ///
618    /// ```rust,no_run
619    /// use claude_code::ClaudeSdkClient;
620    ///
621    /// # async fn example() -> claude_code::Result<()> {
622    /// let mut client = ClaudeSdkClient::new(None, None);
623    /// client.connect(None).await?;
624    /// client.set_model(Some("sonnet")).await?;
625    /// client.disconnect().await?;
626    /// # Ok(())
627    /// # }
628    /// ```
629    pub async fn set_model(&self, model: Option<&str>) -> Result<()> {
630        let query = self.query.as_ref().ok_or_else(|| {
631            Error::CLIConnection(CLIConnectionError::new(
632                "Not connected. Call connect() first.",
633            ))
634        })?;
635        query.set_model(model).await
636    }
637
638    /// Rewinds file changes to a specific user message checkpoint.
639    ///
640    /// # Errors
641    ///
642    /// Returns [`CLIConnectionError`] if not connected.
643    ///
644    /// # Example
645    ///
646    /// ```rust,no_run
647    /// use claude_code::ClaudeSdkClient;
648    ///
649    /// # async fn example() -> claude_code::Result<()> {
650    /// let mut client = ClaudeSdkClient::new(None, None);
651    /// client.connect(None).await?;
652    /// client.rewind_files("user-msg-1").await?;
653    /// client.disconnect().await?;
654    /// # Ok(())
655    /// # }
656    /// ```
657    pub async fn rewind_files(&self, user_message_id: &str) -> Result<()> {
658        let query = self.query.as_ref().ok_or_else(|| {
659            Error::CLIConnection(CLIConnectionError::new(
660                "Not connected. Call connect() first.",
661            ))
662        })?;
663        query.rewind_files(user_message_id).await
664    }
665
666    /// Queries the status of connected MCP servers.
667    ///
668    /// # Errors
669    ///
670    /// Returns [`CLIConnectionError`] if not connected.
671    ///
672    /// # Example
673    ///
674    /// ```rust,no_run
675    /// use claude_code::ClaudeSdkClient;
676    ///
677    /// # async fn example() -> claude_code::Result<()> {
678    /// let mut client = ClaudeSdkClient::new(None, None);
679    /// client.connect(None).await?;
680    /// let _status = client.get_mcp_status().await?;
681    /// client.disconnect().await?;
682    /// # Ok(())
683    /// # }
684    /// ```
685    pub async fn get_mcp_status(&self) -> Result<McpStatusResponse> {
686        let query = self.query.as_ref().ok_or_else(|| {
687            Error::CLIConnection(CLIConnectionError::new(
688                "Not connected. Call connect() first.",
689            ))
690        })?;
691        query.get_mcp_status().await
692    }
693
694    /// Reconnects a disconnected or failed MCP server.
695    ///
696    /// # Errors
697    ///
698    /// Returns [`CLIConnectionError`] if not connected.
699    pub async fn reconnect_mcp_server(&self, server_name: &str) -> Result<()> {
700        let query = self.query.as_ref().ok_or_else(|| {
701            Error::CLIConnection(CLIConnectionError::new(
702                "Not connected. Call connect() first.",
703            ))
704        })?;
705        query.reconnect_mcp_server(server_name).await
706    }
707
708    /// Enables or disables an MCP server.
709    ///
710    /// # Errors
711    ///
712    /// Returns [`CLIConnectionError`] if not connected.
713    pub async fn toggle_mcp_server(&self, server_name: &str, enabled: bool) -> Result<()> {
714        let query = self.query.as_ref().ok_or_else(|| {
715            Error::CLIConnection(CLIConnectionError::new(
716                "Not connected. Call connect() first.",
717            ))
718        })?;
719        query.toggle_mcp_server(server_name, enabled).await
720    }
721
722    /// Stops a running task.
723    ///
724    /// # Errors
725    ///
726    /// Returns [`CLIConnectionError`] if not connected.
727    pub async fn stop_task(&self, task_id: &str) -> Result<()> {
728        let query = self.query.as_ref().ok_or_else(|| {
729            Error::CLIConnection(CLIConnectionError::new(
730                "Not connected. Call connect() first.",
731            ))
732        })?;
733        query.stop_task(task_id).await
734    }
735
736    /// Returns the server initialization response, if available.
737    ///
738    /// # Errors
739    ///
740    /// Returns [`CLIConnectionError`] if not connected.
741    ///
742    /// # Example
743    ///
744    /// ```rust,no_run
745    /// use claude_code::ClaudeSdkClient;
746    ///
747    /// # async fn example() -> claude_code::Result<()> {
748    /// let mut client = ClaudeSdkClient::new(None, None);
749    /// client.connect(None).await?;
750    /// let _info = client.get_server_info()?;
751    /// client.disconnect().await?;
752    /// # Ok(())
753    /// # }
754    /// ```
755    pub fn get_server_info(&self) -> Result<Option<Value>> {
756        let query = self.query.as_ref().ok_or_else(|| {
757            Error::CLIConnection(CLIConnectionError::new(
758                "Not connected. Call connect() first.",
759            ))
760        })?;
761        Ok(query.initialization_result())
762    }
763
764    /// Disconnects from the Claude Code CLI and closes the session.
765    ///
766    /// After disconnecting, the client can be reconnected with [`connect()`](Self::connect).
767    ///
768    /// # Example
769    ///
770    /// ```rust,no_run
771    /// use claude_code::ClaudeSdkClient;
772    ///
773    /// # async fn example() -> claude_code::Result<()> {
774    /// let mut client = ClaudeSdkClient::new(None, None);
775    /// client.connect(None).await?;
776    /// client.disconnect().await?;
777    /// # Ok(())
778    /// # }
779    /// ```
780    pub async fn disconnect(&mut self) -> Result<()> {
781        self.handle_initial_message_stream_task(true).await?;
782        if let Some(query) = self.query.take() {
783            query.close().await?;
784        }
785        Ok(())
786    }
787}
788
789impl Drop for ClaudeSdkClient {
790    fn drop(&mut self) {
791        if let Some(task) = self.initial_message_stream_task.take() {
792            task.abort();
793        }
794    }
795}