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}