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