Skip to main content

composio_sdk/
session.rs

1//! Session management for Tool Router
2
3use crate::client::ComposioClient;
4use crate::error::ComposioError;
5use crate::models::request::{
6    SessionConfig, TagsConfig, ToolFilter, ToolsConfig, ToolkitFilter, WorkbenchConfig,
7};
8use crate::models::response::ToolSchema;
9use crate::models::enums::TagType;
10use std::collections::HashMap;
11use std::sync::Arc;
12
13/// Represents a Tool Router session
14///
15/// A session provides scoped access to tools and toolkits for a specific user.
16/// It maintains a reference to the client for making API calls and stores
17/// session metadata including available tools.
18///
19/// # Example
20///
21/// ```no_run
22/// use composio_sdk::ComposioClient;
23///
24/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
25/// let client = ComposioClient::builder()
26///     .api_key("your-api-key")
27///     .build()?;
28///
29/// let session = client
30///     .create_session("user_123")
31///     .toolkits(vec!["github", "gmail"])
32///     .send()
33///     .await?;
34///
35/// println!("Session ID: {}", session.session_id());
36/// println!("MCP URL: {}", session.mcp_url());
37/// println!("Available tools: {}", session.tools().len());
38/// # Ok(())
39/// # }
40/// ```
41#[derive(Debug, Clone)]
42pub struct Session {
43    /// Shared reference to the Composio client for making API calls
44    client: Arc<ComposioClient>,
45    /// Unique identifier for this session
46    session_id: String,
47    /// MCP server URL for this session
48    mcp_url: String,
49    /// List of available tool slugs in this session
50    tools: Vec<String>,
51}
52
53impl Session {
54    /// Get the session ID
55    ///
56    /// Returns the unique identifier for this session. This ID is used
57    /// for all session-scoped API operations.
58    ///
59    /// # Example
60    ///
61    /// ```no_run
62    /// # use composio_sdk::ComposioClient;
63    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
64    /// # let client = ComposioClient::builder().api_key("key").build()?;
65    /// let session = client.create_session("user_123").send().await?;
66    /// println!("Session ID: {}", session.session_id());
67    /// # Ok(())
68    /// # }
69    /// ```
70    pub fn session_id(&self) -> &str {
71        &self.session_id
72    }
73
74    /// Get the MCP URL
75    ///
76    /// Returns the Model Context Protocol (MCP) server URL for this session.
77    /// This URL can be used to connect MCP-compatible clients to access
78    /// the session's tools.
79    ///
80    /// # Example
81    ///
82    /// ```no_run
83    /// # use composio_sdk::ComposioClient;
84    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
85    /// # let client = ComposioClient::builder().api_key("key").build()?;
86    /// let session = client.create_session("user_123").send().await?;
87    /// println!("MCP URL: {}", session.mcp_url());
88    /// # Ok(())
89    /// # }
90    /// ```
91    pub fn mcp_url(&self) -> &str {
92        &self.mcp_url
93    }
94
95    /// Get the available tool slugs
96    ///
97    /// Returns a slice of tool slugs available in this session.
98    /// These are the meta tools (COMPOSIO_SEARCH_TOOLS,
99    /// COMPOSIO_MULTI_EXECUTE_TOOL, etc.) that can be used with this session.
100    ///
101    /// # Example
102    ///
103    /// ```no_run
104    /// # use composio_sdk::ComposioClient;
105    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
106    /// # let client = ComposioClient::builder().api_key("key").build()?;
107    /// let session = client.create_session("user_123").send().await?;
108    /// 
109    /// for tool_slug in session.tools() {
110    ///     println!("Tool: {}", tool_slug);
111    /// }
112    /// # Ok(())
113    /// # }
114    /// ```
115    pub fn tools(&self) -> &[String] {
116        &self.tools
117    }
118
119    /// Create a Session from a SessionResponse
120    ///
121    /// Internal method used to construct a Session from an API response.
122    /// This is used by both session creation and retrieval.
123    ///
124    /// # Arguments
125    ///
126    /// * `client` - The ComposioClient to use for API calls
127    /// * `response` - The SessionResponse from the API
128    pub(crate) fn from_response(
129        client: ComposioClient,
130        response: crate::models::response::SessionResponse,
131    ) -> Self {
132        Self {
133            client: Arc::new(client),
134            session_id: response.session_id,
135            mcp_url: response.mcp.url,
136            tools: response.tool_router_tools,
137        }
138    }
139
140    /// Execute a tool within this session
141    ///
142    /// Executes a specific tool with the provided arguments. The tool must be
143    /// available in this session (either through enabled toolkits or via
144    /// COMPOSIO_SEARCH_TOOLS).
145    ///
146    /// # Arguments
147    ///
148    /// * `tool_slug` - The tool identifier (e.g., "GITHUB_CREATE_ISSUE")
149    /// * `arguments` - JSON value containing the tool's input parameters
150    ///
151    /// # Returns
152    ///
153    /// Returns a `ToolExecutionResponse` containing:
154    /// - `data`: The tool's output data
155    /// - `error`: Optional error message if execution failed
156    /// - `log_id`: Unique identifier for this execution (for debugging)
157    ///
158    /// # Errors
159    ///
160    /// Returns an error if:
161    /// - The tool is not found or not available in this session
162    /// - The user doesn't have a connected account for the toolkit
163    /// - The arguments are invalid or missing required fields
164    /// - Network error occurs
165    /// - API returns an error response
166    ///
167    /// # Example
168    ///
169    /// ```no_run
170    /// use composio_sdk::ComposioClient;
171    /// use serde_json::json;
172    ///
173    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
174    /// let client = ComposioClient::builder()
175    ///     .api_key("your-api-key")
176    ///     .build()?;
177    ///
178    /// let session = client
179    ///     .create_session("user_123")
180    ///     .toolkits(vec!["github"])
181    ///     .send()
182    ///     .await?;
183    ///
184    /// let result = session
185    ///     .execute_tool(
186    ///         "GITHUB_CREATE_ISSUE",
187    ///         json!({
188    ///             "owner": "composio",
189    ///             "repo": "composio",
190    ///             "title": "Test issue",
191    ///             "body": "Created via Rust SDK"
192    ///         })
193    ///     )
194    ///     .await?;
195    ///
196    /// println!("Result: {:?}", result.data);
197    /// println!("Log ID: {}", result.log_id);
198    ///
199    /// if let Some(error) = result.error {
200    ///     eprintln!("Tool execution error: {}", error);
201    /// }
202    /// # Ok(())
203    /// # }
204    /// ```
205    pub async fn execute_tool(
206        &self,
207        tool_slug: impl Into<String>,
208        arguments: serde_json::Value,
209    ) -> Result<crate::models::response::ToolExecutionResponse, ComposioError> {
210        use crate::models::request::ToolExecutionRequest;
211        use crate::models::response::ToolExecutionResponse;
212        use crate::retry::with_retry;
213
214        let tool_slug = tool_slug.into();
215        let url = format!(
216            "{}/tool_router/session/{}/execute",
217            self.client.config().base_url,
218            self.session_id
219        );
220
221        // Create request body
222        let request_body = ToolExecutionRequest {
223            tool_slug: tool_slug.clone(),
224            arguments: Some(arguments),
225        };
226
227        let policy = &self.client.config().retry_policy;
228
229        // Execute request with retry logic
230        let response = with_retry(policy, || {
231            let url = url.clone();
232            let request_body = request_body.clone();
233            let client = self.client.http_client().clone();
234
235            async move {
236                let response = client
237                    .post(&url)
238                    .json(&request_body)
239                    .send()
240                    .await
241                    .map_err(ComposioError::NetworkError)?;
242
243                // Check for HTTP errors
244                if !response.status().is_success() {
245                    return Err(ComposioError::from_response(response).await);
246                }
247
248                Ok(response)
249            }
250        })
251        .await?;
252
253        // Parse successful response
254        let execution_response: ToolExecutionResponse = response
255            .json()
256            .await
257            .map_err(ComposioError::NetworkError)?;
258
259        Ok(execution_response)
260    }
261
262    /// Execute a meta tool within this session
263    ///
264    /// Meta tools are special tools provided by Composio for runtime tool discovery,
265    /// connection management, and advanced operations:
266    /// - `COMPOSIO_SEARCH_TOOLS`: Discover relevant tools across 1000+ apps
267    /// - `COMPOSIO_MULTI_EXECUTE_TOOL`: Execute up to 20 tools in parallel
268    /// - `COMPOSIO_MANAGE_CONNECTIONS`: Handle OAuth and API key authentication
269    /// - `COMPOSIO_REMOTE_WORKBENCH`: Run Python code in persistent sandbox
270    /// - `COMPOSIO_REMOTE_BASH_TOOL`: Execute bash commands for file/data processing
271    ///
272    /// # Arguments
273    ///
274    /// * `slug` - The meta tool identifier (MetaToolSlug enum)
275    /// * `arguments` - JSON value containing the meta tool's input parameters
276    ///
277    /// # Returns
278    ///
279    /// Returns a `MetaToolExecutionResponse` containing:
280    /// - `data`: The meta tool's output data
281    /// - `error`: Optional error message if execution failed
282    /// - `log_id`: Unique identifier for this execution (for debugging)
283    ///
284    /// # Errors
285    ///
286    /// Returns an error if:
287    /// - The arguments are invalid or missing required fields
288    /// - Network error occurs
289    /// - API returns an error response
290    ///
291    /// # Example
292    ///
293    /// ```no_run
294    /// use composio_sdk::{ComposioClient, MetaToolSlug};
295    /// use serde_json::json;
296    ///
297    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
298    /// let client = ComposioClient::builder()
299    ///     .api_key("your-api-key")
300    ///     .build()?;
301    ///
302    /// let session = client
303    ///     .create_session("user_123")
304    ///     .toolkits(vec!["github"])
305    ///     .send()
306    ///     .await?;
307    ///
308    /// // Search for tools
309    /// let search_result = session
310    ///     .execute_meta_tool(
311    ///         MetaToolSlug::ComposioSearchTools,
312    ///         json!({
313    ///             "query": "create a GitHub issue"
314    ///         })
315    ///     )
316    ///     .await?;
317    ///
318    /// println!("Search result: {:?}", search_result.data);
319    ///
320    /// // Multi-execute tools
321    /// let multi_result = session
322    ///     .execute_meta_tool(
323    ///         MetaToolSlug::ComposioMultiExecuteTool,
324    ///         json!({
325    ///             "tools": [
326    ///                 {
327    ///                     "tool_slug": "GITHUB_GET_REPOS",
328    ///                     "arguments": {"owner": "composio"}
329    ///                 },
330    ///                 {
331    ///                     "tool_slug": "GITHUB_GET_ISSUES",
332    ///                     "arguments": {"owner": "composio", "repo": "composio"}
333    ///                 }
334    ///             ]
335    ///         })
336    ///     )
337    ///     .await?;
338    ///
339    /// println!("Multi-execute result: {:?}", multi_result.data);
340    /// # Ok(())
341    /// # }
342    /// ```
343    pub async fn execute_meta_tool(
344        &self,
345        slug: crate::models::enums::MetaToolSlug,
346        arguments: serde_json::Value,
347    ) -> Result<crate::models::response::MetaToolExecutionResponse, ComposioError> {
348        use crate::models::request::MetaToolExecutionRequest;
349        use crate::models::response::MetaToolExecutionResponse;
350        use crate::retry::with_retry;
351
352        let url = format!(
353            "{}/tool_router/session/{}/execute_meta",
354            self.client.config().base_url,
355            self.session_id
356        );
357
358        // Create request body
359        let request_body = MetaToolExecutionRequest {
360            slug,
361            arguments: Some(arguments),
362        };
363
364        let policy = &self.client.config().retry_policy;
365
366        // Execute request with retry logic
367        let response = with_retry(policy, || {
368            let url = url.clone();
369            let request_body = request_body.clone();
370            let client = self.client.http_client().clone();
371
372            async move {
373                let response = client
374                    .post(&url)
375                    .json(&request_body)
376                    .send()
377                    .await
378                    .map_err(ComposioError::NetworkError)?;
379
380                // Check for HTTP errors
381                if !response.status().is_success() {
382                    return Err(ComposioError::from_response(response).await);
383                }
384
385                Ok(response)
386            }
387        })
388        .await?;
389
390        // Parse successful response
391        let execution_response: MetaToolExecutionResponse = response
392            .json()
393            .await
394            .map_err(ComposioError::NetworkError)?;
395
396        Ok(execution_response)
397    }
398
399    /// List available toolkits in this session
400    ///
401    /// Returns a builder for listing toolkits with optional filtering.
402    /// The builder allows you to configure pagination, search, and filtering
403    /// options before executing the request.
404    ///
405    /// # Example
406    ///
407    /// ```no_run
408    /// # use composio_sdk::ComposioClient;
409    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
410    /// # let client = ComposioClient::builder().api_key("key").build()?;
411    /// let session = client.create_session("user_123").send().await?;
412    ///
413    /// // List all toolkits
414    /// let toolkits = session.list_toolkits().send().await?;
415    ///
416    /// // List only connected toolkits
417    /// let connected = session.list_toolkits()
418    ///     .is_connected(true)
419    ///     .send()
420    ///     .await?;
421    ///
422    /// // Search for specific toolkits
423    /// let github_toolkits = session.list_toolkits()
424    ///     .search("github")
425    ///     .send()
426    ///     .await?;
427    /// # Ok(())
428    /// # }
429    /// ```
430    pub fn list_toolkits(&self) -> ToolkitListBuilder<'_> {
431        ToolkitListBuilder::new(self)
432    }
433
434    /// Get meta tools schemas for this session
435    ///
436    /// Retrieves the complete schemas for all meta tools available in this session.
437    /// Meta tools include:
438    /// - `COMPOSIO_SEARCH_TOOLS`: Discover relevant tools across 1000+ apps
439    /// - `COMPOSIO_MULTI_EXECUTE_TOOL`: Execute up to 20 tools in parallel
440    /// - `COMPOSIO_MANAGE_CONNECTIONS`: Handle OAuth and API key authentication
441    /// - `COMPOSIO_REMOTE_WORKBENCH`: Run Python code in persistent sandbox
442    /// - `COMPOSIO_REMOTE_BASH_TOOL`: Execute bash commands for file/data processing
443    ///
444    /// The returned schemas include detailed information about input parameters,
445    /// output parameters, descriptions, and other metadata needed to use the tools.
446    ///
447    /// # Returns
448    ///
449    /// Returns a vector of `ToolSchema` objects, each containing:
450    /// - `slug`: Tool identifier (e.g., "COMPOSIO_SEARCH_TOOLS")
451    /// - `name`: Human-readable name
452    /// - `description`: Detailed functionality explanation
453    /// - `input_parameters`: JSON schema of required inputs
454    /// - `output_parameters`: JSON schema of return values
455    /// - `version`: Current version
456    /// - Other metadata fields
457    ///
458    /// # Errors
459    ///
460    /// Returns an error if:
461    /// - Network error occurs
462    /// - API returns an error response
463    /// - Response cannot be parsed
464    ///
465    /// # Example
466    ///
467    /// ```no_run
468    /// use composio_sdk::ComposioClient;
469    ///
470    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
471    /// let client = ComposioClient::builder()
472    ///     .api_key("your-api-key")
473    ///     .build()?;
474    ///
475    /// let session = client
476    ///     .create_session("user_123")
477    ///     .send()
478    ///     .await?;
479    ///
480    /// // Get all meta tool schemas
481    /// let meta_tools = session.get_meta_tools().await?;
482    ///
483    /// for tool in meta_tools {
484    ///     println!("Tool: {}", tool.slug);
485    ///     println!("  Name: {}", tool.name);
486    ///     println!("  Description: {}", tool.description);
487    ///     println!("  Version: {}", tool.version);
488    ///     println!("  Input schema: {}", tool.input_parameters);
489    ///     println!("  Output schema: {}", tool.output_parameters);
490    ///     println!();
491    /// }
492    /// # Ok(())
493    /// # }
494    /// ```
495    pub async fn get_meta_tools(&self) -> Result<Vec<ToolSchema>, ComposioError> {
496        use crate::retry::with_retry;
497
498        let url = format!(
499            "{}/tool_router/session/{}/tools",
500            self.client.config().base_url,
501            self.session_id
502        );
503
504        let policy = &self.client.config().retry_policy;
505
506        // Execute request with retry logic
507        let response = with_retry(policy, || {
508            let url = url.clone();
509            let client = self.client.http_client().clone();
510
511            async move {
512                let response = client
513                    .get(&url)
514                    .send()
515                    .await
516                    .map_err(ComposioError::NetworkError)?;
517
518                // Check for HTTP errors
519                if !response.status().is_success() {
520                    return Err(ComposioError::from_response(response).await);
521                }
522
523                Ok(response)
524            }
525        })
526        .await?;
527
528        // Parse successful response - API returns array of ToolSchema
529        let tools: Vec<ToolSchema> = response
530            .json()
531            .await
532            .map_err(ComposioError::NetworkError)?;
533
534        Ok(tools)
535    }
536
537    /// Create an authentication link for a toolkit
538    ///
539    /// Generates a Connect Link URL that users can visit to authenticate with
540    /// a specific toolkit (e.g., GitHub, Gmail, Slack). This is used for both
541    /// in-chat authentication and manual authentication flows.
542    ///
543    /// # Arguments
544    ///
545    /// * `toolkit` - The toolkit slug to create an auth link for (e.g., "github", "gmail")
546    /// * `callback_url` - Optional URL to redirect to after authentication completes.
547    ///                    Query parameters `status` and `connected_account_id` will be appended.
548    ///
549    /// # Returns
550    ///
551    /// Returns a `LinkResponse` containing:
552    /// - `link_token`: Token identifying this auth link session
553    /// - `redirect_url`: URL for the user to visit to complete authentication
554    /// - `connected_account_id`: Optional ID if account already exists
555    ///
556    /// # Errors
557    ///
558    /// Returns an error if:
559    /// - Toolkit slug is invalid or not found (400 Bad Request)
560    /// - Connected account already exists for this toolkit (400 Bad Request)
561    /// - Network error occurs
562    /// - API returns an error response
563    /// - Response cannot be parsed
564    ///
565    /// # Example
566    ///
567    /// ```no_run
568    /// use composio_sdk::ComposioClient;
569    ///
570    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
571    /// let client = ComposioClient::builder()
572    ///     .api_key("your-api-key")
573    ///     .build()?;
574    ///
575    /// let session = client
576    ///     .create_session("user_123")
577    ///     .send()
578    ///     .await?;
579    ///
580    /// // Create auth link without callback
581    /// let link = session.create_auth_link("github", None).await?;
582    /// println!("Visit: {}", link.redirect_url);
583    ///
584    /// // Create auth link with callback
585    /// let link = session.create_auth_link(
586    ///     "gmail",
587    ///     Some("https://example.com/callback".to_string())
588    /// ).await?;
589    /// println!("Link token: {}", link.link_token);
590    /// println!("Redirect URL: {}", link.redirect_url);
591    /// # Ok(())
592    /// # }
593    /// ```
594    ///
595    /// # See Also
596    ///
597    /// - [Manual Authentication Guide](https://docs.composio.dev/guides/authentication)
598    /// - [In-Chat Authentication](https://docs.composio.dev/guides/in-chat-auth)
599    pub async fn create_auth_link(
600        &self,
601        toolkit: impl Into<String>,
602        callback_url: Option<String>,
603    ) -> Result<crate::models::response::LinkResponse, ComposioError> {
604        use crate::models::request::LinkRequest;
605        use crate::retry::with_retry;
606
607        let toolkit = toolkit.into();
608        let url = format!(
609            "{}/tool_router/session/{}/link",
610            self.client.config().base_url,
611            self.session_id
612        );
613
614        let request_body = LinkRequest {
615            toolkit: toolkit.clone(),
616            callback_url,
617        };
618
619        let policy = &self.client.config().retry_policy;
620
621        // Execute request with retry logic
622        let response = with_retry(policy, || {
623            let url = url.clone();
624            let client = self.client.http_client().clone();
625            let body = request_body.clone();
626
627            async move {
628                let response = client
629                    .post(&url)
630                    .json(&body)
631                    .send()
632                    .await
633                    .map_err(ComposioError::NetworkError)?;
634
635                // Check for HTTP errors
636                if !response.status().is_success() {
637                    return Err(ComposioError::from_response(response).await);
638                }
639
640                Ok(response)
641            }
642        })
643        .await?;
644
645        // Parse successful response
646        let link_response: crate::models::response::LinkResponse = response
647            .json()
648            .await
649            .map_err(ComposioError::NetworkError)?;
650
651        Ok(link_response)
652    }
653}
654
655/// Builder for creating sessions with fluent API
656///
657/// # Example
658///
659/// ```no_run
660/// use composio_sdk::ComposioClient;
661///
662/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
663/// let client = ComposioClient::builder()
664///     .api_key("your-api-key")
665///     .build()?;
666///
667/// let session = client
668///     .create_session("user_123")
669///     .toolkits(vec!["github", "gmail"])
670///     .manage_connections(true)
671///     .send()
672///     .await?;
673/// # Ok(())
674/// # }
675/// ```
676pub struct SessionBuilder<'a> {
677    client: &'a ComposioClient,
678    #[allow(dead_code)]
679    user_id: String,
680    config: SessionConfig,
681}
682
683impl<'a> SessionBuilder<'a> {
684    /// Create a new session builder
685    ///
686    /// # Arguments
687    ///
688    /// * `client` - Reference to the ComposioClient
689    /// * `user_id` - User identifier for session isolation
690    pub fn new(client: &'a ComposioClient, user_id: String) -> Self {
691        Self {
692            client,
693            user_id: user_id.clone(),
694            config: SessionConfig {
695                user_id,
696                toolkits: None,
697                auth_configs: None,
698                connected_accounts: None,
699                manage_connections: None,
700                tools: None,
701                tags: None,
702                workbench: None,
703            },
704        }
705    }
706
707    /// Enable specific toolkits for this session
708    ///
709    /// By default, all toolkits are accessible via COMPOSIO_SEARCH_TOOLS.
710    /// Use this method to restrict the session to specific toolkits.
711    ///
712    /// # Arguments
713    ///
714    /// * `toolkits` - Vector of toolkit slugs to enable (e.g., "github", "gmail")
715    ///
716    /// # Example
717    ///
718    /// ```no_run
719    /// # use composio_sdk::ComposioClient;
720    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
721    /// # let client = ComposioClient::builder().api_key("key").build()?;
722    /// let session = client
723    ///     .create_session("user_123")
724    ///     .toolkits(vec!["github", "gmail", "slack"])
725    ///     .send()
726    ///     .await?;
727    /// # Ok(())
728    /// # }
729    /// ```
730    pub fn toolkits(mut self, toolkits: Vec<impl Into<String>>) -> Self {
731        self.config.toolkits = Some(ToolkitFilter::Enable(
732            toolkits.into_iter().map(|t| t.into()).collect(),
733        ));
734        self
735    }
736
737    /// Disable specific toolkits for this session
738    ///
739    /// Use this to exclude certain toolkits while keeping all others accessible.
740    ///
741    /// # Arguments
742    ///
743    /// * `toolkits` - Vector of toolkit slugs to disable
744    ///
745    /// # Example
746    ///
747    /// ```no_run
748    /// # use composio_sdk::ComposioClient;
749    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
750    /// # let client = ComposioClient::builder().api_key("key").build()?;
751    /// let session = client
752    ///     .create_session("user_123")
753    ///     .disable_toolkits(vec!["exa", "firecrawl"])
754    ///     .send()
755    ///     .await?;
756    /// # Ok(())
757    /// # }
758    /// ```
759    pub fn disable_toolkits(mut self, toolkits: Vec<impl Into<String>>) -> Self {
760        self.config.toolkits = Some(ToolkitFilter::Disable {
761            disable: toolkits.into_iter().map(|t| t.into()).collect(),
762        });
763        self
764    }
765
766    /// Override the default auth config for a specific toolkit
767    ///
768    /// Use this to specify a custom auth configuration (e.g., your own OAuth app)
769    /// instead of Composio's managed authentication.
770    ///
771    /// # Arguments
772    ///
773    /// * `toolkit` - Toolkit slug (e.g., "github")
774    /// * `auth_config_id` - Auth config ID (e.g., "ac_your_config")
775    ///
776    /// # Example
777    ///
778    /// ```no_run
779    /// # use composio_sdk::ComposioClient;
780    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
781    /// # let client = ComposioClient::builder().api_key("key").build()?;
782    /// let session = client
783    ///     .create_session("user_123")
784    ///     .auth_config("github", "ac_custom_github_oauth")
785    ///     .send()
786    ///     .await?;
787    /// # Ok(())
788    /// # }
789    /// ```
790    pub fn auth_config(
791        mut self,
792        toolkit: impl Into<String>,
793        auth_config_id: impl Into<String>,
794    ) -> Self {
795        self.config
796            .auth_configs
797            .get_or_insert_with(HashMap::new)
798            .insert(toolkit.into(), auth_config_id.into());
799        self
800    }
801
802    /// Select a specific connected account for a toolkit
803    ///
804    /// Use this when a user has multiple connected accounts for the same toolkit
805    /// (e.g., work and personal email) and you want to specify which one to use.
806    ///
807    /// # Arguments
808    ///
809    /// * `toolkit` - Toolkit slug (e.g., "gmail")
810    /// * `connected_account_id` - Connected account ID (e.g., "ca_work_gmail")
811    ///
812    /// # Example
813    ///
814    /// ```no_run
815    /// # use composio_sdk::ComposioClient;
816    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
817    /// # let client = ComposioClient::builder().api_key("key").build()?;
818    /// let session = client
819    ///     .create_session("user_123")
820    ///     .connected_account("gmail", "ca_work_gmail")
821    ///     .send()
822    ///     .await?;
823    /// # Ok(())
824    /// # }
825    /// ```
826    pub fn connected_account(
827        mut self,
828        toolkit: impl Into<String>,
829        connected_account_id: impl Into<String>,
830    ) -> Self {
831        self.config
832            .connected_accounts
833            .get_or_insert_with(HashMap::new)
834            .insert(toolkit.into(), connected_account_id.into());
835        self
836    }
837
838    /// Enable or disable automatic connection management
839    ///
840    /// When enabled (default), the agent will automatically prompt users with
841    /// Connect Links during chat when authentication is needed.
842    ///
843    /// # Arguments
844    ///
845    /// * `enabled` - Whether to enable automatic connection management
846    ///
847    /// # Example
848    ///
849    /// ```no_run
850    /// # use composio_sdk::ComposioClient;
851    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
852    /// # let client = ComposioClient::builder().api_key("key").build()?;
853    /// // Disable in-chat authentication (use manual auth flow instead)
854    /// let session = client
855    ///     .create_session("user_123")
856    ///     .manage_connections(false)
857    ///     .send()
858    ///     .await?;
859    /// # Ok(())
860    /// # }
861    /// ```
862    pub fn manage_connections(mut self, enabled: bool) -> Self {
863        self.config.manage_connections = Some(crate::models::ManageConnectionsConfig::Bool(enabled));
864        self
865    }
866
867    /// Configure per-toolkit tool filtering
868    ///
869    /// Use this to enable or disable specific tools within a toolkit.
870    ///
871    /// # Arguments
872    ///
873    /// * `toolkit` - Toolkit slug (e.g., "github")
874    /// * `tools` - Vector of tool slugs to enable
875    ///
876    /// # Example
877    ///
878    /// ```no_run
879    /// # use composio_sdk::ComposioClient;
880    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
881    /// # let client = ComposioClient::builder().api_key("key").build()?;
882    /// let session = client
883    ///     .create_session("user_123")
884    ///     .tools("github", vec!["GITHUB_CREATE_ISSUE", "GITHUB_GET_REPOS"])
885    ///     .send()
886    ///     .await?;
887    /// # Ok(())
888    /// # }
889    /// ```
890    pub fn tools(mut self, toolkit: impl Into<String>, tools: Vec<impl Into<String>>) -> Self {
891        let tool_filter = ToolFilter::EnableList(tools.into_iter().map(|t| t.into()).collect());
892
893        self.config
894            .tools
895            .get_or_insert_with(|| ToolsConfig(HashMap::new()))
896            .0
897            .insert(toolkit.into(), tool_filter);
898        self
899    }
900
901    /// Configure tag-based tool filtering
902    ///
903    /// Tags are MCP annotation hints that categorize tools by behavior:
904    /// - `readOnlyHint`: Read-only tools (safe, no modifications)
905    /// - `destructiveHint`: Tools that modify or delete data
906    /// - `idempotentHint`: Tools that can be safely retried
907    /// - `openWorldHint`: Tools that interact with external world
908    ///
909    /// # Arguments
910    ///
911    /// * `enabled` - Tags that tools must have (at least one)
912    /// * `disabled` - Tags that tools must NOT have (any)
913    ///
914    /// # Example
915    ///
916    /// ```no_run
917    /// # use composio_sdk::ComposioClient;
918    /// # use composio_sdk::models::enums::TagType;
919    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
920    /// # let client = ComposioClient::builder().api_key("key").build()?;
921    /// // Only allow read-only tools, exclude destructive ones
922    /// let session = client
923    ///     .create_session("user_123")
924    ///     .tags(
925    ///         Some(vec![TagType::ReadOnlyHint]),
926    ///         Some(vec![TagType::DestructiveHint])
927    ///     )
928    ///     .send()
929    ///     .await?;
930    /// # Ok(())
931    /// # }
932    /// ```
933    pub fn tags(
934        mut self,
935        enabled: Option<Vec<TagType>>,
936        disabled: Option<Vec<TagType>>,
937    ) -> Self {
938        self.config.tags = Some(TagsConfig { enabled, disabled });
939        self
940    }
941
942    /// Configure workbench settings
943    ///
944    /// The workbench is a persistent Python sandbox for complex operations.
945    ///
946    /// # Arguments
947    ///
948    /// * `proxy_execution` - Whether to enable proxy execution
949    /// * `auto_offload_threshold` - Threshold for automatic offloading
950    ///
951    /// # Example
952    ///
953    /// ```no_run
954    /// # use composio_sdk::ComposioClient;
955    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
956    /// # let client = ComposioClient::builder().api_key("key").build()?;
957    /// let session = client
958    ///     .create_session("user_123")
959    ///     .workbench(Some(true), Some(1000))
960    ///     .send()
961    ///     .await?;
962    /// # Ok(())
963    /// # }
964    /// ```
965    pub fn workbench(
966        mut self,
967        proxy_execution: Option<bool>,
968        auto_offload_threshold: Option<u32>,
969    ) -> Self {
970        self.config.workbench = Some(WorkbenchConfig {
971            proxy_execution,
972            auto_offload_threshold,
973        });
974        self
975    }
976
977    /// Send the session creation request
978    ///
979    /// This consumes the builder and creates the session on the Composio API.
980    ///
981    /// # Errors
982    ///
983    /// Returns an error if:
984    /// - The API request fails
985    /// - The response cannot be parsed
986    /// - Authentication is invalid
987    ///
988    /// # Example
989    ///
990    /// ```no_run
991    /// # use composio_sdk::ComposioClient;
992    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
993    /// # let client = ComposioClient::builder().api_key("key").build()?;
994    /// let session = client
995    ///     .create_session("user_123")
996    ///     .toolkits(vec!["github"])
997    ///     .send()
998    ///     .await?;
999    ///
1000    /// println!("Session ID: {}", session.session_id());
1001    /// println!("MCP URL: {}", session.mcp_url());
1002    /// # Ok(())
1003    /// # }
1004    /// ```
1005    pub async fn send(self) -> Result<Session, ComposioError> {
1006        use crate::models::response::SessionResponse;
1007        use crate::retry::with_retry;
1008
1009        let url = format!("{}/tool_router/session", self.client.config().base_url);
1010        let policy = &self.client.config().retry_policy;
1011
1012        // Execute request with retry logic
1013        let response = with_retry(policy, || {
1014            let url = url.clone();
1015            let config = self.config.clone();
1016            let client = self.client.http_client().clone();
1017
1018            async move {
1019                let response = client
1020                    .post(&url)
1021                    .json(&config)
1022                    .send()
1023                    .await
1024                    .map_err(ComposioError::NetworkError)?;
1025
1026                // Check for HTTP errors
1027                if !response.status().is_success() {
1028                    return Err(ComposioError::from_response(response).await);
1029                }
1030
1031                Ok(response)
1032            }
1033        })
1034        .await?;
1035
1036        // Parse successful response
1037        let session_response: SessionResponse = response
1038            .json()
1039            .await
1040            .map_err(ComposioError::NetworkError)?;
1041
1042        // Create Session struct with Arc-wrapped client
1043        Ok(Session {
1044            client: Arc::new(self.client.clone()),
1045            session_id: session_response.session_id,
1046            mcp_url: session_response.mcp.url,
1047            tools: session_response.tool_router_tools,
1048        })
1049    }
1050}
1051
1052/// Builder for listing toolkits with filtering options
1053///
1054/// This builder provides a fluent API for configuring toolkit listing requests.
1055/// It supports pagination, filtering by connection status, searching, and
1056/// filtering by specific toolkit slugs.
1057///
1058/// # Example
1059///
1060/// ```no_run
1061/// # use composio_sdk::ComposioClient;
1062/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
1063/// # let client = ComposioClient::builder().api_key("key").build()?;
1064/// let session = client.create_session("user_123").send().await?;
1065///
1066/// // List first 10 connected toolkits
1067/// let toolkits = session.list_toolkits()
1068///     .limit(10)
1069///     .is_connected(true)
1070///     .send()
1071///     .await?;
1072///
1073/// // Paginate through results
1074/// let mut cursor = None;
1075/// loop {
1076///     let mut builder = session.list_toolkits().limit(20);
1077///     if let Some(c) = cursor {
1078///         builder = builder.cursor(c);
1079///     }
1080///     
1081///     let response = builder.send().await?;
1082///     // Process response.items...
1083///     
1084///     cursor = response.next_cursor;
1085///     if cursor.is_none() {
1086///         break;
1087///     }
1088/// }
1089/// # Ok(())
1090/// # }
1091/// ```
1092#[derive(Debug)]
1093pub struct ToolkitListBuilder<'a> {
1094    session: &'a Session,
1095    limit: Option<u32>,
1096    cursor: Option<String>,
1097    toolkits: Option<Vec<String>>,
1098    is_connected: Option<bool>,
1099    search: Option<String>,
1100}
1101
1102impl<'a> ToolkitListBuilder<'a> {
1103    /// Create a new toolkit list builder
1104    fn new(session: &'a Session) -> Self {
1105        Self {
1106            session,
1107            limit: None,
1108            cursor: None,
1109            toolkits: None,
1110            is_connected: None,
1111            search: None,
1112        }
1113    }
1114
1115    /// Set the maximum number of toolkits to return
1116    ///
1117    /// # Arguments
1118    ///
1119    /// * `limit` - Maximum number of toolkits (default: 20)
1120    ///
1121    /// # Example
1122    ///
1123    /// ```no_run
1124    /// # use composio_sdk::ComposioClient;
1125    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
1126    /// # let client = ComposioClient::builder().api_key("key").build()?;
1127    /// # let session = client.create_session("user_123").send().await?;
1128    /// let toolkits = session.list_toolkits()
1129    ///     .limit(50)
1130    ///     .send()
1131    ///     .await?;
1132    /// # Ok(())
1133    /// # }
1134    /// ```
1135    pub fn limit(mut self, limit: u32) -> Self {
1136        self.limit = Some(limit);
1137        self
1138    }
1139
1140    /// Set the pagination cursor
1141    ///
1142    /// Use the `next_cursor` value from a previous response to fetch
1143    /// the next page of results.
1144    ///
1145    /// # Arguments
1146    ///
1147    /// * `cursor` - Pagination cursor from previous response
1148    ///
1149    /// # Example
1150    ///
1151    /// ```no_run
1152    /// # use composio_sdk::ComposioClient;
1153    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
1154    /// # let client = ComposioClient::builder().api_key("key").build()?;
1155    /// # let session = client.create_session("user_123").send().await?;
1156    /// let first_page = session.list_toolkits().limit(20).send().await?;
1157    ///
1158    /// if let Some(cursor) = first_page.next_cursor {
1159    ///     let second_page = session.list_toolkits()
1160    ///         .limit(20)
1161    ///         .cursor(cursor)
1162    ///         .send()
1163    ///         .await?;
1164    /// }
1165    /// # Ok(())
1166    /// # }
1167    /// ```
1168    pub fn cursor(mut self, cursor: impl Into<String>) -> Self {
1169        self.cursor = Some(cursor.into());
1170        self
1171    }
1172
1173    /// Filter by specific toolkit slugs
1174    ///
1175    /// Only return toolkits matching the provided slugs.
1176    ///
1177    /// # Arguments
1178    ///
1179    /// * `toolkits` - List of toolkit slugs to filter by
1180    ///
1181    /// # Example
1182    ///
1183    /// ```no_run
1184    /// # use composio_sdk::ComposioClient;
1185    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
1186    /// # let client = ComposioClient::builder().api_key("key").build()?;
1187    /// # let session = client.create_session("user_123").send().await?;
1188    /// let toolkits = session.list_toolkits()
1189    ///     .toolkits(vec!["github", "gmail", "slack"])
1190    ///     .send()
1191    ///     .await?;
1192    /// # Ok(())
1193    /// # }
1194    /// ```
1195    pub fn toolkits(mut self, toolkits: Vec<impl Into<String>>) -> Self {
1196        self.toolkits = Some(toolkits.into_iter().map(|t| t.into()).collect());
1197        self
1198    }
1199
1200    /// Filter by connection status
1201    ///
1202    /// When set to `true`, only returns toolkits that have an active
1203    /// connected account. When set to `false`, only returns toolkits
1204    /// without a connected account.
1205    ///
1206    /// # Arguments
1207    ///
1208    /// * `is_connected` - Whether to filter by connection status
1209    ///
1210    /// # Example
1211    ///
1212    /// ```no_run
1213    /// # use composio_sdk::ComposioClient;
1214    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
1215    /// # let client = ComposioClient::builder().api_key("key").build()?;
1216    /// # let session = client.create_session("user_123").send().await?;
1217    /// // Get only connected toolkits
1218    /// let connected = session.list_toolkits()
1219    ///     .is_connected(true)
1220    ///     .send()
1221    ///     .await?;
1222    ///
1223    /// // Get only disconnected toolkits
1224    /// let disconnected = session.list_toolkits()
1225    ///     .is_connected(false)
1226    ///     .send()
1227    ///     .await?;
1228    /// # Ok(())
1229    /// # }
1230    /// ```
1231    pub fn is_connected(mut self, is_connected: bool) -> Self {
1232        self.is_connected = Some(is_connected);
1233        self
1234    }
1235
1236    /// Search toolkits by name or slug
1237    ///
1238    /// Returns toolkits whose name or slug contains the search query
1239    /// (case-insensitive).
1240    ///
1241    /// # Arguments
1242    ///
1243    /// * `search` - Search query string
1244    ///
1245    /// # Example
1246    ///
1247    /// ```no_run
1248    /// # use composio_sdk::ComposioClient;
1249    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
1250    /// # let client = ComposioClient::builder().api_key("key").build()?;
1251    /// # let session = client.create_session("user_123").send().await?;
1252    /// let github_toolkits = session.list_toolkits()
1253    ///     .search("github")
1254    ///     .send()
1255    ///     .await?;
1256    /// # Ok(())
1257    /// # }
1258    /// ```
1259    pub fn search(mut self, search: impl Into<String>) -> Self {
1260        self.search = Some(search.into());
1261        self
1262    }
1263
1264    /// Execute the toolkit listing request
1265    ///
1266    /// Sends the request to the Composio API and returns the list of toolkits
1267    /// matching the configured filters.
1268    ///
1269    /// # Errors
1270    ///
1271    /// Returns an error if:
1272    /// - The API request fails
1273    /// - The response cannot be parsed
1274    /// - Authentication is invalid
1275    ///
1276    /// # Example
1277    ///
1278    /// ```no_run
1279    /// # use composio_sdk::ComposioClient;
1280    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
1281    /// # let client = ComposioClient::builder().api_key("key").build()?;
1282    /// # let session = client.create_session("user_123").send().await?;
1283    /// let response = session.list_toolkits()
1284    ///     .limit(10)
1285    ///     .is_connected(true)
1286    ///     .send()
1287    ///     .await?;
1288    ///
1289    /// println!("Found {} toolkits", response.items.len());
1290    /// println!("Total: {}", response.total_items);
1291    /// println!("Page {} of {}", response.current_page, response.total_pages);
1292    ///
1293    /// for toolkit in response.items {
1294    ///     println!("- {} ({})", toolkit.name, toolkit.slug);
1295    ///     if let Some(account) = toolkit.connected_account {
1296    ///         println!("  Connected: {} ({})", account.id, account.status);
1297    ///     }
1298    /// }
1299    /// # Ok(())
1300    /// # }
1301    /// ```
1302    pub async fn send(self) -> Result<crate::models::response::ToolkitListResponse, ComposioError> {
1303        use crate::models::response::ToolkitListResponse;
1304        use crate::retry::with_retry;
1305
1306        let url = format!(
1307            "{}/tool_router/session/{}/toolkits",
1308            self.session.client.config().base_url,
1309            self.session.session_id
1310        );
1311
1312        // Build query parameters
1313        let mut query_params = Vec::new();
1314
1315        if let Some(limit) = self.limit {
1316            query_params.push(("limit", limit.to_string()));
1317        }
1318
1319        if let Some(cursor) = &self.cursor {
1320            query_params.push(("cursor", cursor.clone()));
1321        }
1322
1323        if let Some(toolkits) = &self.toolkits {
1324            query_params.push(("toolkits", toolkits.join(",")));
1325        }
1326
1327        if let Some(is_connected) = self.is_connected {
1328            query_params.push(("is_connected", is_connected.to_string()));
1329        }
1330
1331        if let Some(search) = &self.search {
1332            query_params.push(("search", search.clone()));
1333        }
1334
1335        let policy = &self.session.client.config().retry_policy;
1336
1337        // Execute request with retry logic
1338        let response = with_retry(policy, || {
1339            let url = url.clone();
1340            let query_params = query_params.clone();
1341            let client = self.session.client.http_client().clone();
1342
1343            async move {
1344                let response = client
1345                    .get(&url)
1346                    .query(&query_params)
1347                    .send()
1348                    .await
1349                    .map_err(ComposioError::NetworkError)?;
1350
1351                // Check for HTTP errors
1352                if !response.status().is_success() {
1353                    return Err(ComposioError::from_response(response).await);
1354                }
1355
1356                Ok(response)
1357            }
1358        })
1359        .await?;
1360
1361        // Parse successful response
1362        let toolkit_response: ToolkitListResponse = response
1363            .json()
1364            .await
1365            .map_err(ComposioError::NetworkError)?;
1366
1367        Ok(toolkit_response)
1368    }
1369}
1370
1371#[cfg(test)]
1372mod tests {
1373    use super::*;
1374    use crate::client::ComposioClient;
1375    use crate::models::enums::TagType;
1376    use crate::models::request::{ManageConnectionsConfig, ToolFilter, ToolkitFilter};
1377
1378    fn create_test_client() -> ComposioClient {
1379        ComposioClient::builder()
1380            .api_key("test_api_key")
1381            .build()
1382            .unwrap()
1383    }
1384
1385    #[test]
1386    fn test_session_builder_new() {
1387        let client = create_test_client();
1388        let builder = SessionBuilder::new(&client, "user_123".to_string());
1389
1390        assert_eq!(builder.user_id, "user_123");
1391        assert!(builder.config.toolkits.is_none());
1392        assert!(builder.config.auth_configs.is_none());
1393        assert!(builder.config.connected_accounts.is_none());
1394        assert!(builder.config.manage_connections.is_none());
1395        assert!(builder.config.tools.is_none());
1396        assert!(builder.config.tags.is_none());
1397        assert!(builder.config.workbench.is_none());
1398    }
1399
1400    #[test]
1401    fn test_session_builder_toolkits_enable() {
1402        let client = create_test_client();
1403        let builder = SessionBuilder::new(&client, "user_123".to_string())
1404            .toolkits(vec!["github", "gmail"]);
1405
1406        match builder.config.toolkits {
1407            Some(ToolkitFilter::Enable(toolkits)) => {
1408                assert_eq!(toolkits.len(), 2);
1409                assert!(toolkits.contains(&"github".to_string()));
1410                assert!(toolkits.contains(&"gmail".to_string()));
1411            }
1412            _ => panic!("Expected Enable variant"),
1413        }
1414    }
1415
1416    #[test]
1417    fn test_session_builder_disable_toolkits() {
1418        let client = create_test_client();
1419        let builder = SessionBuilder::new(&client, "user_123".to_string())
1420            .disable_toolkits(vec!["exa", "firecrawl"]);
1421
1422        match builder.config.toolkits {
1423            Some(ToolkitFilter::Disable { disable }) => {
1424                assert_eq!(disable.len(), 2);
1425                assert!(disable.contains(&"exa".to_string()));
1426                assert!(disable.contains(&"firecrawl".to_string()));
1427            }
1428            _ => panic!("Expected Disable variant"),
1429        }
1430    }
1431
1432    #[test]
1433    fn test_session_builder_auth_config() {
1434        let client = create_test_client();
1435        let builder = SessionBuilder::new(&client, "user_123".to_string())
1436            .auth_config("github", "ac_custom_config");
1437
1438        let auth_configs = builder.config.auth_configs.unwrap();
1439        assert_eq!(auth_configs.len(), 1);
1440        assert_eq!(auth_configs.get("github"), Some(&"ac_custom_config".to_string()));
1441    }
1442
1443    #[test]
1444    fn test_session_builder_multiple_auth_configs() {
1445        let client = create_test_client();
1446        let builder = SessionBuilder::new(&client, "user_123".to_string())
1447            .auth_config("github", "ac_github_config")
1448            .auth_config("gmail", "ac_gmail_config");
1449
1450        let auth_configs = builder.config.auth_configs.unwrap();
1451        assert_eq!(auth_configs.len(), 2);
1452        assert_eq!(auth_configs.get("github"), Some(&"ac_github_config".to_string()));
1453        assert_eq!(auth_configs.get("gmail"), Some(&"ac_gmail_config".to_string()));
1454    }
1455
1456    #[test]
1457    fn test_session_builder_connected_account() {
1458        let client = create_test_client();
1459        let builder = SessionBuilder::new(&client, "user_123".to_string())
1460            .connected_account("gmail", "ca_work_gmail");
1461
1462        let connected_accounts = builder.config.connected_accounts.unwrap();
1463        assert_eq!(connected_accounts.len(), 1);
1464        assert_eq!(connected_accounts.get("gmail"), Some(&"ca_work_gmail".to_string()));
1465    }
1466
1467    #[test]
1468    fn test_session_builder_multiple_connected_accounts() {
1469        let client = create_test_client();
1470        let builder = SessionBuilder::new(&client, "user_123".to_string())
1471            .connected_account("gmail", "ca_work_gmail")
1472            .connected_account("github", "ca_personal_github");
1473
1474        let connected_accounts = builder.config.connected_accounts.unwrap();
1475        assert_eq!(connected_accounts.len(), 2);
1476        assert_eq!(connected_accounts.get("gmail"), Some(&"ca_work_gmail".to_string()));
1477        assert_eq!(connected_accounts.get("github"), Some(&"ca_personal_github".to_string()));
1478    }
1479
1480    #[test]
1481    fn test_session_builder_manage_connections_true() {
1482        let client = create_test_client();
1483        let builder = SessionBuilder::new(&client, "user_123".to_string())
1484            .manage_connections(true);
1485
1486        match builder.config.manage_connections {
1487            Some(ManageConnectionsConfig::Bool(enabled)) => {
1488                assert!(enabled);
1489            }
1490            _ => panic!("Expected Bool variant with true"),
1491        }
1492    }
1493
1494    #[test]
1495    fn test_session_builder_manage_connections_false() {
1496        let client = create_test_client();
1497        let builder = SessionBuilder::new(&client, "user_123".to_string())
1498            .manage_connections(false);
1499
1500        match builder.config.manage_connections {
1501            Some(ManageConnectionsConfig::Bool(enabled)) => {
1502                assert!(!enabled);
1503            }
1504            _ => panic!("Expected Bool variant with false"),
1505        }
1506    }
1507
1508    #[test]
1509    fn test_session_builder_tools_enable() {
1510        let client = create_test_client();
1511        let builder = SessionBuilder::new(&client, "user_123".to_string())
1512            .tools("github", vec!["GITHUB_CREATE_ISSUE", "GITHUB_GET_REPOS"]);
1513
1514        let tools_config = builder.config.tools.unwrap();
1515        let github_filter = tools_config.0.get("github").unwrap();
1516
1517        match github_filter {
1518            ToolFilter::EnableList(tools) => {
1519                assert_eq!(tools.len(), 2);
1520                assert!(tools.contains(&"GITHUB_CREATE_ISSUE".to_string()));
1521                assert!(tools.contains(&"GITHUB_GET_REPOS".to_string()));
1522            }
1523            _ => panic!("Expected EnableList variant"),
1524        }
1525    }
1526
1527    #[test]
1528    fn test_session_builder_multiple_toolkit_tools() {
1529        let client = create_test_client();
1530        let builder = SessionBuilder::new(&client, "user_123".to_string())
1531            .tools("github", vec!["GITHUB_CREATE_ISSUE"])
1532            .tools("gmail", vec!["GMAIL_SEND_EMAIL"]);
1533
1534        let tools_config = builder.config.tools.unwrap();
1535        assert_eq!(tools_config.0.len(), 2);
1536        assert!(tools_config.0.contains_key("github"));
1537        assert!(tools_config.0.contains_key("gmail"));
1538    }
1539
1540    #[test]
1541    fn test_session_builder_tags_enabled() {
1542        let client = create_test_client();
1543        let builder = SessionBuilder::new(&client, "user_123".to_string())
1544            .tags(Some(vec![TagType::ReadOnlyHint, TagType::IdempotentHint]), None);
1545
1546        let tags_config = builder.config.tags.unwrap();
1547        let enabled = tags_config.enabled.unwrap();
1548        assert_eq!(enabled.len(), 2);
1549        assert!(enabled.contains(&TagType::ReadOnlyHint));
1550        assert!(enabled.contains(&TagType::IdempotentHint));
1551        assert!(tags_config.disabled.is_none());
1552    }
1553
1554    #[test]
1555    fn test_session_builder_tags_disabled() {
1556        let client = create_test_client();
1557        let builder = SessionBuilder::new(&client, "user_123".to_string())
1558            .tags(None, Some(vec![TagType::DestructiveHint]));
1559
1560        let tags_config = builder.config.tags.unwrap();
1561        let disabled = tags_config.disabled.unwrap();
1562        assert_eq!(disabled.len(), 1);
1563        assert!(disabled.contains(&TagType::DestructiveHint));
1564        assert!(tags_config.enabled.is_none());
1565    }
1566
1567    #[test]
1568    fn test_session_builder_tags_both() {
1569        let client = create_test_client();
1570        let builder = SessionBuilder::new(&client, "user_123".to_string())
1571            .tags(
1572                Some(vec![TagType::ReadOnlyHint]),
1573                Some(vec![TagType::DestructiveHint])
1574            );
1575
1576        let tags_config = builder.config.tags.unwrap();
1577        assert!(tags_config.enabled.is_some());
1578        assert!(tags_config.disabled.is_some());
1579    }
1580
1581    #[test]
1582    fn test_session_builder_workbench() {
1583        let client = create_test_client();
1584        let builder = SessionBuilder::new(&client, "user_123".to_string())
1585            .workbench(Some(true), Some(1000));
1586
1587        let workbench_config = builder.config.workbench.unwrap();
1588        assert_eq!(workbench_config.proxy_execution, Some(true));
1589        assert_eq!(workbench_config.auto_offload_threshold, Some(1000));
1590    }
1591
1592    #[test]
1593    fn test_session_builder_workbench_no_threshold() {
1594        let client = create_test_client();
1595        let builder = SessionBuilder::new(&client, "user_123".to_string())
1596            .workbench(Some(false), None);
1597
1598        let workbench_config = builder.config.workbench.unwrap();
1599        assert_eq!(workbench_config.proxy_execution, Some(false));
1600        assert_eq!(workbench_config.auto_offload_threshold, None);
1601    }
1602
1603    #[test]
1604    fn test_session_builder_method_chaining() {
1605        let client = create_test_client();
1606        let builder = SessionBuilder::new(&client, "user_123".to_string())
1607            .toolkits(vec!["github", "gmail"])
1608            .auth_config("github", "ac_custom")
1609            .connected_account("gmail", "ca_work")
1610            .manage_connections(true)
1611            .tools("github", vec!["GITHUB_CREATE_ISSUE"])
1612            .tags(Some(vec![TagType::ReadOnlyHint]), None)
1613            .workbench(Some(true), Some(500));
1614
1615        // Verify all configurations are set
1616        assert!(builder.config.toolkits.is_some());
1617        assert!(builder.config.auth_configs.is_some());
1618        assert!(builder.config.connected_accounts.is_some());
1619        assert!(builder.config.manage_connections.is_some());
1620        assert!(builder.config.tools.is_some());
1621        assert!(builder.config.tags.is_some());
1622        assert!(builder.config.workbench.is_some());
1623    }
1624
1625    #[test]
1626    fn test_session_session_id_accessor() {
1627        let client = Arc::new(create_test_client());
1628        let session = Session {
1629            client,
1630            session_id: "sess_123".to_string(),
1631            mcp_url: "https://mcp.composio.dev".to_string(),
1632            tools: vec!["COMPOSIO_SEARCH_TOOLS".to_string()],
1633        };
1634
1635        assert_eq!(session.session_id(), "sess_123");
1636    }
1637
1638    #[test]
1639    fn test_session_mcp_url_accessor() {
1640        let client = Arc::new(create_test_client());
1641        let session = Session {
1642            client,
1643            session_id: "sess_123".to_string(),
1644            mcp_url: "https://mcp.composio.dev".to_string(),
1645            tools: vec!["COMPOSIO_SEARCH_TOOLS".to_string()],
1646        };
1647
1648        assert_eq!(session.mcp_url(), "https://mcp.composio.dev");
1649    }
1650
1651    #[test]
1652    fn test_session_tools_accessor() {
1653        let client = Arc::new(create_test_client());
1654        let tools = vec![
1655            "COMPOSIO_SEARCH_TOOLS".to_string(),
1656            "COMPOSIO_MULTI_EXECUTE_TOOL".to_string(),
1657        ];
1658        let session = Session {
1659            client,
1660            session_id: "sess_123".to_string(),
1661            mcp_url: "https://mcp.composio.dev".to_string(),
1662            tools: tools.clone(),
1663        };
1664
1665        assert_eq!(session.tools(), &tools);
1666        assert_eq!(session.tools().len(), 2);
1667    }
1668
1669    #[test]
1670    fn test_toolkit_list_builder_new() {
1671        let client = Arc::new(create_test_client());
1672        let session = Session {
1673            client,
1674            session_id: "sess_123".to_string(),
1675            mcp_url: "https://mcp.composio.dev".to_string(),
1676            tools: vec![],
1677        };
1678
1679        let builder = ToolkitListBuilder::new(&session);
1680        assert!(builder.limit.is_none());
1681        assert!(builder.cursor.is_none());
1682        assert!(builder.toolkits.is_none());
1683        assert!(builder.is_connected.is_none());
1684        assert!(builder.search.is_none());
1685    }
1686
1687    #[test]
1688    fn test_toolkit_list_builder_limit() {
1689        let client = Arc::new(create_test_client());
1690        let session = Session {
1691            client,
1692            session_id: "sess_123".to_string(),
1693            mcp_url: "https://mcp.composio.dev".to_string(),
1694            tools: vec![],
1695        };
1696
1697        let builder = session.list_toolkits().limit(50);
1698        assert_eq!(builder.limit, Some(50));
1699    }
1700
1701    #[test]
1702    fn test_toolkit_list_builder_cursor() {
1703        let client = Arc::new(create_test_client());
1704        let session = Session {
1705            client,
1706            session_id: "sess_123".to_string(),
1707            mcp_url: "https://mcp.composio.dev".to_string(),
1708            tools: vec![],
1709        };
1710
1711        let builder = session.list_toolkits().cursor("cursor_abc");
1712        assert_eq!(builder.cursor, Some("cursor_abc".to_string()));
1713    }
1714
1715    #[test]
1716    fn test_toolkit_list_builder_toolkits() {
1717        let client = Arc::new(create_test_client());
1718        let session = Session {
1719            client,
1720            session_id: "sess_123".to_string(),
1721            mcp_url: "https://mcp.composio.dev".to_string(),
1722            tools: vec![],
1723        };
1724
1725        let builder = session.list_toolkits().toolkits(vec!["github", "gmail"]);
1726        let toolkits = builder.toolkits.unwrap();
1727        assert_eq!(toolkits.len(), 2);
1728        assert!(toolkits.contains(&"github".to_string()));
1729        assert!(toolkits.contains(&"gmail".to_string()));
1730    }
1731
1732    #[test]
1733    fn test_toolkit_list_builder_is_connected() {
1734        let client = Arc::new(create_test_client());
1735        let session = Session {
1736            client,
1737            session_id: "sess_123".to_string(),
1738            mcp_url: "https://mcp.composio.dev".to_string(),
1739            tools: vec![],
1740        };
1741
1742        let builder = session.list_toolkits().is_connected(true);
1743        assert_eq!(builder.is_connected, Some(true));
1744    }
1745
1746    #[test]
1747    fn test_toolkit_list_builder_search() {
1748        let client = Arc::new(create_test_client());
1749        let session = Session {
1750            client,
1751            session_id: "sess_123".to_string(),
1752            mcp_url: "https://mcp.composio.dev".to_string(),
1753            tools: vec![],
1754        };
1755
1756        let builder = session.list_toolkits().search("communication");
1757        assert_eq!(builder.search, Some("communication".to_string()));
1758    }
1759
1760    #[test]
1761    fn test_toolkit_list_builder_method_chaining() {
1762        let client = Arc::new(create_test_client());
1763        let session = Session {
1764            client,
1765            session_id: "sess_123".to_string(),
1766            mcp_url: "https://mcp.composio.dev".to_string(),
1767            tools: vec![],
1768        };
1769
1770        let builder = session.list_toolkits()
1771            .limit(25)
1772            .cursor("cursor_xyz")
1773            .toolkits(vec!["github"])
1774            .is_connected(true)
1775            .search("git");
1776
1777        assert_eq!(builder.limit, Some(25));
1778        assert_eq!(builder.cursor, Some("cursor_xyz".to_string()));
1779        assert!(builder.toolkits.is_some());
1780        assert_eq!(builder.is_connected, Some(true));
1781        assert_eq!(builder.search, Some("git".to_string()));
1782    }
1783}