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