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}