Files
composiohq--composio/python/composio/core/models/tool_router.py
T
2026-07-13 12:38:34 +08:00

1322 lines
52 KiB
Python

"""
ToolRouter class for managing tool router sessions.
This module provides tool routing session management with enhanced functionality
for creating isolated MCP sessions with provider-wrapped tools.
"""
from __future__ import annotations
import typing as t
from dataclasses import dataclass
from enum import Enum
import typing_extensions as te
from composio_client import omit
from composio_client.types.tool_router import session_create_params
from composio_client.types.tool_router.session_attach_response import (
SessionAttachResponse,
)
from composio_client.types.tool_router.session_retrieve_response import (
SessionRetrieveResponse,
)
from composio.client import HttpClient
from composio.core.models.base import Resource
from composio.core.models.custom_tool import (
ExperimentalToolkit,
assert_no_custom_tool_slugs_in_preload,
build_custom_tools_map,
build_custom_tools_map_from_response,
get_preloaded_custom_tool_slugs,
serialize_custom_toolkits,
serialize_custom_tools,
)
from composio.core.models.custom_tool_types import (
CustomTool,
CustomToolsMap,
InlineCustomToolsWirePayload,
)
from composio.core.models.inline_custom_tools_payload import (
inline_custom_tools_attach_experimental,
)
from composio.core.models.tool_router_constants import (
PRELOAD_TOOLS_ALL,
SESSION_PRESET_DIRECT_TOOLS,
)
from composio.core.models.tool_router_session import (
ToolRouterSession,
ToolRouterSessionPreloadConfig,
ToolRouterSessionWithMcp,
)
from composio.core.models.tool_router_session_delete import (
ToolRouterSessionDeleteResponse,
delete_tool_router_session,
)
from composio.core.models.tool_router_session_files import ToolRouterSessionFilesMount
from composio.core.provider import TTool, TToolCollection
from composio.core.provider.base import BaseProvider
from composio.exceptions import InvalidParams
# Type alias for MCP tag literals
ToolRouterTag = t.Literal[
"readOnlyHint", "destructiveHint", "idempotentHint", "openWorldHint"
]
# Type alias for sandbox compute tier on the session sandbox
# +----------+------+------+
# | Tier | vCPU | RAM |
# +----------+------+------+
# | standard | 1 | 1 GB |
# | medium | 2 | 2 GB |
# | large | 4 | 4 GB |
# | xlarge | 8 | 8 GB |
# +----------+------+------+
# Defaults to "standard" server-side when omitted.
SandboxSize = t.Literal["standard", "medium", "large", "xlarge"]
SessionPreset = t.Literal["direct_tools"]
class ToolRouterToolkitsEnableConfig(te.TypedDict, total=False):
"""Configuration for enabling specific toolkits in tool router session.
Attributes:
enable: List of toolkit slugs to enable in the tool router session.
"""
enable: t.List[str]
class ToolRouterToolkitsDisableConfig(te.TypedDict, total=False):
"""Configuration for disabling specific toolkits in tool router session.
Attributes:
disable: List of toolkit slugs to disable in the tool router session.
"""
disable: t.List[str]
class ToolRouterToolsEnableConfig(te.TypedDict, total=False):
"""Configuration for enabling specific tools for a toolkit.
Attributes:
enable: List of tool slugs to enable for this toolkit.
"""
enable: t.List[str]
class ToolRouterToolsDisableConfig(te.TypedDict, total=False):
"""Configuration for disabling specific tools for a toolkit.
Attributes:
disable: List of tool slugs to disable for this toolkit.
"""
disable: t.List[str]
class ToolRouterToolsTagsConfig(te.TypedDict, total=False):
"""Configuration for filtering tools by MCP tags.
Attributes:
tags: Tags configuration - can be a list of tags (shorthand for enable)
or an object with enable/disable keys.
Only tools matching these tags will be available.
"""
tags: ToolRouterConfigTags
# Type alias for per-toolkit tool configuration
# Can be:
# - List[str]: List of tool slugs (shorthand for enable)
# - ToolRouterToolsEnableConfig: Dict with 'enable' key (whitelist)
# - ToolRouterToolsDisableConfig: Dict with 'disable' key (blacklist)
# - ToolRouterToolsTagsConfig: Dict with 'tags' key (filter by MCP tags)
ToolRouterToolsConfig = t.Union[
t.List[str],
ToolRouterToolsEnableConfig,
ToolRouterToolsDisableConfig,
ToolRouterToolsTagsConfig,
]
class ToolRouterTagsEnableDisableConfig(te.TypedDict, total=False):
"""Configuration for tags in tool router session.
Attributes:
enable: List of tags to enable in the tool router session.
disable: List of tags to disable in the tool router session.
"""
enable: t.Optional[t.List[ToolRouterTag]]
disable: t.Optional[t.List[ToolRouterTag]]
# Type alias for tags configuration
# Can be:
# - List[ToolRouterTag]: List of tag literals (shorthand for enable)
# - ToolRouterTagsEnableDisableConfig: Dict with 'enable' and/or 'disable' keys
ToolRouterConfigTags = t.Union[
t.List[ToolRouterTag],
ToolRouterTagsEnableDisableConfig,
]
def _is_tools_tags_config(
config: ToolRouterToolsConfig,
) -> t.TypeGuard[ToolRouterToolsTagsConfig]:
"""Type guard to check if config is ToolRouterToolsTagsConfig."""
return isinstance(config, dict) and "tags" in config
class ToolRouterSandboxConfig(te.TypedDict, total=False):
"""Configuration for session sandbox settings.
Attributes:
enable: Whether to enable the sandbox entirely. Defaults to True.
When set to False, no code execution tools
(COMPOSIO_REMOTE_WORKBENCH, COMPOSIO_REMOTE_BASH_TOOL) are
available in the session, sandbox-related prompt lines are
stripped, and direct sandbox calls are rejected.
enable_proxy_execution: Whether to allow proxy execute calls in the sandbox.
If False, prevents arbitrary HTTP requests.
auto_offload_threshold: Maximum execution payload size to offload to the sandbox.
sandbox_size: Sandbox compute tier. One of ``"standard"`` (1 vCPU / 1 GB),
``"medium"`` (2 vCPU / 2 GB), ``"large"`` (4 vCPU / 4 GB), or
``"xlarge"`` (8 vCPU / 8 GB). Defaults to ``"standard"``
server-side when omitted. Changing this on an existing session
recreates the sandbox on next access; the in-memory FS state
is lost, but the ``/mnt/files/`` mount persists.
"""
enable: bool
enable_proxy_execution: bool
auto_offload_threshold: int
sandbox_size: SandboxSize
class ToolRouterWorkbenchConfig(ToolRouterSandboxConfig, total=False):
"""Deprecated alias for :class:`ToolRouterSandboxConfig`.
``workbench`` is still accepted for backwards compatibility; prefer
``sandbox`` in new code.
"""
class ToolRouterManageConnectionsConfig(te.TypedDict, total=False):
"""Configuration for connection management in tool router session.
Attributes:
enable: Whether to use tools to manage connections. Defaults to True.
If False, you need to manage connections manually.
callback_url: Optional callback URL to use for OAuth redirects.
wait_for_connections: Whether to wait for users to finish authenticating
connections before proceeding to the next step. Defaults to False.
If set to True, a wait for connections tool call will happen and
finish when the connections are ready.
"""
enable: bool
callback_url: str
wait_for_connections: bool
class ToolRouterMultiAccountConfig(te.TypedDict, total=False):
"""Configuration for multi-account mode in tool router session.
Attributes:
enable: When True, enables multi-account mode. When not set, falls back to
org/project-level configuration.
max_accounts_per_toolkit: Maximum connected accounts allowed per toolkit (2-10).
Defaults to 5 when multi-account is enabled.
require_explicit_selection: When True, require explicit account selection when
multiple accounts are connected. Defaults to False.
"""
enable: bool
max_accounts_per_toolkit: int
require_explicit_selection: bool
class ToolRouterPreloadConfig(te.TypedDict, total=False):
"""Configuration for tools preloaded into a tool router session.
Attributes:
tools: Tool slugs to expose directly in ``session.tools()`` and MCP
tool lists without first calling search.
"""
tools: t.Union[t.List[str], t.Literal["all"]]
class _SessionPreloadLike(te.Protocol):
@property
def tools(self) -> t.Union[t.List[str], str, None]: ...
def _session_preload_config(
preload: _SessionPreloadLike,
) -> ToolRouterSessionPreloadConfig:
tools = preload.tools
if tools == PRELOAD_TOOLS_ALL:
return ToolRouterSessionPreloadConfig(tools=PRELOAD_TOOLS_ALL)
if isinstance(tools, list):
return ToolRouterSessionPreloadConfig(tools=t.cast(t.List[str], tools))
return ToolRouterSessionPreloadConfig(tools=[])
def _preloads_all_custom_tools(
preload: t.Union[ToolRouterPreloadConfig, _SessionPreloadLike, None],
) -> bool:
tools: t.Union[t.List[str], str, None]
if preload is None:
tools = None
elif isinstance(preload, dict):
tools = preload.get("tools")
else:
tools = preload.tools
return tools == PRELOAD_TOOLS_ALL
def _apply_session_preset_defaults(
session_preset: t.Optional[SessionPreset],
manage_connections: t.Optional[t.Union[bool, ToolRouterManageConnectionsConfig]],
workbench: t.Optional[ToolRouterSandboxConfig],
preload: t.Optional[ToolRouterPreloadConfig],
) -> t.Tuple[
t.Optional[t.Union[bool, ToolRouterManageConnectionsConfig]],
t.Optional[ToolRouterSandboxConfig],
t.Optional[ToolRouterPreloadConfig],
]:
if session_preset != SESSION_PRESET_DIRECT_TOOLS:
return manage_connections, workbench, preload
return (
False if manage_connections is None else manage_connections,
{"enable": False} if workbench is None else workbench,
{"tools": PRELOAD_TOOLS_ALL} if preload is None else preload,
)
def _prepare_inline_custom_tools(
*,
custom_tools: t.Optional[t.List[CustomTool]],
custom_toolkits: t.Optional[t.List[ExperimentalToolkit]],
default_preload: bool = False,
preload_tools: t.Optional[t.Union[t.Sequence[str], t.Literal["all"]]] = None,
) -> t.Optional[InlineCustomToolsWirePayload]:
has_customs = bool(custom_tools or custom_toolkits)
local_custom_tools_map = (
build_custom_tools_map(custom_tools or [], custom_toolkits)
if has_customs
else None
)
# Top-level preload["tools"] is for Composio-managed slugs only. Custom tools
# use their own preload flag, so reject LOCAL/custom slugs there before
# serializing the inline custom definitions.
assert_no_custom_tool_slugs_in_preload(preload_tools, local_custom_tools_map)
inline_custom_tools_payload: t.Optional[InlineCustomToolsWirePayload] = None
if has_customs:
inline_custom_tools_payload = InlineCustomToolsWirePayload()
if custom_tools:
inline_custom_tools_payload["custom_tools"] = serialize_custom_tools(
custom_tools,
default_preload=default_preload,
)
if custom_toolkits:
inline_custom_tools_payload["custom_toolkits"] = serialize_custom_toolkits(
custom_toolkits,
default_preload=default_preload,
)
return inline_custom_tools_payload
class ToolRouterAssistivePromptConfig(te.TypedDict, total=False):
"""Configuration for assistive prompt generation.
Attributes:
user_timezone: IANA timezone identifier (e.g., "America/New_York", "Europe/London")
for timezone-aware assistive prompts.
"""
user_timezone: str
class ToolRouterExperimentalConfig(te.TypedDict, total=False):
"""Experimental configuration for tool router session.
Note: These features are experimental and may be modified or removed in future versions.
Attributes:
assistive_prompt: Configuration for assistive prompt generation.
custom_tools: Custom tools to include in the session.
custom_toolkits: Custom toolkits with grouped tools.
"""
assistive_prompt: ToolRouterAssistivePromptConfig
custom_tools: t.List[CustomTool]
custom_toolkits: t.List[ExperimentalToolkit]
@dataclass
class ToolkitConnectionAuthConfig:
"""Auth config information for a toolkit connection.
Attributes:
id: The id of the auth config
mode: The auth scheme used by the auth config
is_composio_managed: Whether the auth config is managed by Composio
"""
id: str
mode: str
is_composio_managed: bool
@dataclass
class ToolkitConnectedAccount:
"""Connected account information for a toolkit.
Attributes:
id: The id of the connected account
status: The status of the connected account
"""
id: str
status: str
@dataclass
class ToolkitConnection:
"""Connection information for a toolkit.
Attributes:
is_active: Whether the connection is active or not
auth_config: The auth config of a toolkit
connected_account: The connected account of a toolkit
"""
is_active: bool
auth_config: t.Optional[ToolkitConnectionAuthConfig] = None
connected_account: t.Optional[ToolkitConnectedAccount] = None
@dataclass
class ToolkitConnectionState:
"""The connection state of a toolkit.
Attributes:
slug: The slug of a toolkit
name: The name of a toolkit
logo: The logo of a toolkit (optional)
is_no_auth: Whether the toolkit is no auth or not
connection: The connection information
"""
slug: str
name: str
is_no_auth: bool
connection: t.Optional[ToolkitConnection] = None
logo: t.Optional[str] = None
@dataclass
class ToolkitConnectionsDetails:
"""Details of toolkit connections.
Attributes:
items: List of toolkit connection states
next_cursor: Optional cursor for pagination
total_pages: Total number of pages
"""
items: t.List[ToolkitConnectionState]
total_pages: int
next_cursor: t.Optional[str] = None
class ToolRouterMCPServerType(str, Enum):
"""Enum for MCP server types."""
HTTP = "http"
SSE = "sse"
@dataclass
class ToolRouterMCPServerConfig:
"""Configuration for MCP server.
Attributes:
type: The type of MCP server (HTTP or SSE)
url: The URL of the MCP server
headers: Optional authentication headers (includes x-api-key)
"""
type: ToolRouterMCPServerType
url: str
headers: t.Optional[t.Dict[str, t.Optional[str]]] = None
@dataclass
class ToolRouterSessionExperimental:
"""Experimental features in session response.
Note: These features are experimental and may be modified or removed in future versions.
Attributes:
files: File mount for list, upload, download, delete operations.
assistive_prompt: The generated assistive system prompt based on the experimental config.
"""
files: "ToolRouterSessionFilesMount"
assistive_prompt: t.Optional[str] = None
class ToolRouter(Resource, t.Generic[TTool, TToolCollection]):
"""
ToolRouter class for managing tool routing sessions.
Provides functionality to create isolated tool router sessions with provider-wrapped tools,
authorization helpers, and connection management.
Example:
```python
from composio import Composio
composio = Composio()
# Create a session for a user
session = composio.tool_router.create(
user_id='user_123',
manage_connections=True
)
# Get tools wrapped for the provider
tools = session.tools()
# Authorize a toolkit
connection_request = session.authorize('github')
print(f"Redirect URL: {connection_request.redirect_url}")
```
"""
def __init__(
self,
client: HttpClient,
provider: t.Optional["BaseProvider[TTool, TToolCollection]"] = None,
dangerously_allow_auto_upload_download_files: bool = False,
sensitive_file_upload_protection: bool = True,
file_upload_path_deny_segments: t.Optional[t.Sequence[str]] = None,
file_upload_dirs: t.Union[t.Sequence[str], t.Literal[False], None] = None,
):
"""
Initialize ToolRouter instance.
:param client: HTTP client for API calls
:param provider: Optional provider for tool wrapping
:param dangerously_allow_auto_upload_download_files: Opt-in for automatic file upload/download. Defaults to False.
:param sensitive_file_upload_protection: When True, block local paths on the built-in sensitive-path denylist before upload.
:param file_upload_path_deny_segments: Extra path segment names to merge with the built-in denylist.
:param file_upload_dirs: Allowlist of directories for auto-upload. See ``Composio`` for details.
"""
super().__init__(client)
self._provider = provider
self._sensitive_file_upload_protection = sensitive_file_upload_protection
self._file_upload_path_deny_segments = file_upload_path_deny_segments
self._file_upload_dirs = file_upload_dirs
self._auto_upload_download_files = bool(
dangerously_allow_auto_upload_download_files
)
def _create_mcp_server_config(
self,
mcp_type: ToolRouterMCPServerType,
url: str,
) -> ToolRouterMCPServerConfig:
"""
Create an MCP server config object with authentication headers.
:param mcp_type: The type of MCP server (HTTP or SSE)
:param url: The URL of the MCP server
:return: MCP server config with headers
"""
return ToolRouterMCPServerConfig(
type=mcp_type,
url=url,
headers={
"x-api-key": self._client.api_key,
},
)
def _transform_tags_params(
self, tags: t.Optional[ToolRouterConfigTags]
) -> t.Optional[session_create_params.TagsUnionMember1]:
"""Transform tags configuration to API format.
Args:
tags: Tags configuration - can be a list (shorthand for enable)
or an object with enable/disable keys.
Returns:
Transformed tags payload in API format, or None if tags is None.
"""
if tags is None:
return None
if isinstance(tags, list):
# List shorthand means enable these tags
# Return value structure matches TagsUnionMember1: {"enable": [...]}
return {"enable": tags}
elif isinstance(tags, dict):
# Object format with enable/disable
# Only include keys that are present and not None
# Return value structure matches TagsUnionMember1
enable_value = tags.get("enable")
disable_value = tags.get("disable")
# Build result dict only with non-None values
if enable_value is not None and disable_value is not None:
return {
"enable": enable_value,
"disable": disable_value,
}
elif enable_value is not None:
return {"enable": enable_value}
elif disable_value is not None:
return {"disable": disable_value}
else:
return None
# Overloads: pass ``mcp=True`` to surface ``session.mcp`` in the returned
# type (ToolRouterSessionWithMcp). The MCP endpoint exists at runtime on
# every session regardless. See https://docs.composio.dev/docs/sessions-via-mcp
@t.overload
def create(
self,
*,
user_id: str,
toolkits: t.Optional[
t.Union[
t.List[str],
ToolRouterToolkitsEnableConfig,
ToolRouterToolkitsDisableConfig,
]
] = None,
tools: t.Optional[t.Dict[str, ToolRouterToolsConfig]] = None,
tags: t.Optional[ToolRouterConfigTags] = None,
manage_connections: t.Optional[
t.Union[bool, ToolRouterManageConnectionsConfig]
] = None,
auth_configs: t.Optional[t.Dict[str, str]] = None,
connected_accounts: t.Optional[t.Dict[str, t.Union[str, t.List[str]]]] = None,
sandbox: t.Optional[ToolRouterSandboxConfig] = None,
workbench: t.Optional[ToolRouterWorkbenchConfig] = None,
multi_account: t.Optional[ToolRouterMultiAccountConfig] = None,
preload: t.Optional[ToolRouterPreloadConfig] = None,
session_preset: t.Optional[SessionPreset] = None,
experimental: t.Optional[ToolRouterExperimentalConfig] = None,
mcp: t.Literal[True],
) -> ToolRouterSessionWithMcp[TTool, TToolCollection]: ...
@t.overload
def create(
self,
*,
user_id: str,
toolkits: t.Optional[
t.Union[
t.List[str],
ToolRouterToolkitsEnableConfig,
ToolRouterToolkitsDisableConfig,
]
] = None,
tools: t.Optional[t.Dict[str, ToolRouterToolsConfig]] = None,
tags: t.Optional[ToolRouterConfigTags] = None,
manage_connections: t.Optional[
t.Union[bool, ToolRouterManageConnectionsConfig]
] = None,
auth_configs: t.Optional[t.Dict[str, str]] = None,
connected_accounts: t.Optional[t.Dict[str, t.Union[str, t.List[str]]]] = None,
sandbox: t.Optional[ToolRouterSandboxConfig] = None,
workbench: t.Optional[ToolRouterWorkbenchConfig] = None,
multi_account: t.Optional[ToolRouterMultiAccountConfig] = None,
preload: t.Optional[ToolRouterPreloadConfig] = None,
session_preset: t.Optional[SessionPreset] = None,
experimental: t.Optional[ToolRouterExperimentalConfig] = None,
mcp: t.Literal[False] = False,
) -> ToolRouterSession[TTool, TToolCollection]: ...
def create(
self,
*,
user_id: str,
toolkits: t.Optional[
t.Union[
t.List[str],
ToolRouterToolkitsEnableConfig,
ToolRouterToolkitsDisableConfig,
]
] = None,
tools: t.Optional[t.Dict[str, ToolRouterToolsConfig]] = None,
tags: t.Optional[ToolRouterConfigTags] = None,
manage_connections: t.Optional[
t.Union[bool, ToolRouterManageConnectionsConfig]
] = None,
auth_configs: t.Optional[t.Dict[str, str]] = None,
connected_accounts: t.Optional[t.Dict[str, t.Union[str, t.List[str]]]] = None,
sandbox: t.Optional[ToolRouterSandboxConfig] = None,
workbench: t.Optional[ToolRouterWorkbenchConfig] = None,
multi_account: t.Optional[ToolRouterMultiAccountConfig] = None,
preload: t.Optional[ToolRouterPreloadConfig] = None,
session_preset: t.Optional[SessionPreset] = None,
experimental: t.Optional[ToolRouterExperimentalConfig] = None,
mcp: bool = False,
) -> ToolRouterSession[TTool, TToolCollection]:
"""
Create a new tool router session for a user.
:param user_id: The user ID to create the session for.
:param toolkits: Optional toolkit configuration. Can be:
- List[str]: List of toolkit slugs to enable.
Example: ['github', 'slack']
- ToolRouterToolkitsEnableConfig: Dict with 'enable' key.
Example: {'enable': ['github', 'slack']}
- ToolRouterToolkitsDisableConfig: Dict with 'disable' key.
Example: {'disable': ['linear']}
:param tools: Optional per-toolkit tool configuration. Key is toolkit slug,
value is ToolRouterToolsConfig which can be:
- List[str]: List of tool slugs (shorthand for enable).
Example: ['GMAIL_SEND_EMAIL', 'GMAIL_SEARCH']
- ToolRouterToolsEnableConfig: Dict with 'enable' key.
Example: {'enable': ['GMAIL_SEND_EMAIL']}
- ToolRouterToolsDisableConfig: Dict with 'disable' key.
Example: {'disable': ['GMAIL_DELETE_EMAIL']}
- ToolRouterToolsTagsConfig: Dict with 'tags' key.
Tags can be a list (shorthand for enable) or object with enable/disable.
Example: {'tags': ['readOnlyHint', 'idempotentHint']}
Example: {'tags': {'enable': ['readOnlyHint'], 'disable': ['destructiveHint']}}
Example: {
'gmail': ['GMAIL_SEND_EMAIL', 'GMAIL_SEARCH'],
'github': {'enable': ['GITHUB_CREATE_ISSUE']},
'slack': {'disable': ['SLACK_DELETE_MESSAGE']},
'linear': {'tags': ['readOnlyHint']}
}
:param tags: Optional global MCP tags to filter tools by.
Can be:
- List[str]: List of tag literals (shorthand for enable).
Example: ['readOnlyHint', 'idempotentHint']
- ToolRouterTagsEnableDisableConfig: Dict with 'enable' and/or 'disable' keys.
Example: {'enable': ['readOnlyHint'], 'disable': ['destructiveHint']}
Available tag values: 'readOnlyHint', 'destructiveHint',
'idempotentHint', 'openWorldHint'.
Toolkit-level tags override this global setting.
:param manage_connections: Optional connection management configuration. Can be:
- bool: Simple boolean to enable/disable.
Example: True or False
- ToolRouterManageConnectionsConfig: Dict with:
- 'enable' (bool): Whether to use tools to manage
connections. Defaults to True.
- 'callback_url' (str, optional): Callback URL for
OAuth redirects.
- 'wait_for_connections' (bool, optional): Whether to wait
for users to finish authenticating connections before
proceeding to the next step. Defaults to False.
Example: {'enable': True, 'callback_url': 'https://example.com/callback', 'wait_for_connections': True}
:param auth_configs: Optional mapping of toolkit slug to auth config ID.
Example: {'github': 'ac_xxx', 'slack': 'ac_yyy'}
:param connected_accounts: Optional mapping of toolkit slug to connected account ID(s).
Accepts a single string or a list of strings per toolkit.
Only one account per toolkit is allowed when multi-account
mode is disabled.
Example: {'github': 'ca_xxx', 'slack': ['ca_yyy']}
:param sandbox: Optional sandbox configuration. Dict with:
- 'enable' (bool): Whether to enable the sandbox entirely.
Defaults to True. When set to False, no code execution tools
(COMPOSIO_REMOTE_WORKBENCH, COMPOSIO_REMOTE_BASH_TOOL) are
available in the session.
- 'enable_proxy_execution' (bool): Whether to allow proxy execute
calls in the sandbox. If False, prevents arbitrary HTTP requests.
- 'auto_offload_threshold' (int): Maximum execution payload size to
offload to the sandbox.
- 'sandbox_size' (SandboxSize): Sandbox compute tier. One of
'standard' (1 vCPU / 1 GB, default), 'medium' (2 vCPU / 2 GB),
'large' (4 vCPU / 4 GB), or 'xlarge' (8 vCPU / 8 GB).
Example: {'enable': False}
Example: {'enable_proxy_execution': False, 'auto_offload_threshold': 300}
Example: {'sandbox_size': 'large'}
:param workbench: Deprecated alias for sandbox. Accepted for backwards compatibility.
:param multi_account: Optional multi-account configuration (ToolRouterMultiAccountConfig).
Dict with:
- 'enable' (bool): When True, enables multi-account mode.
Falls back to org/project-level config when not set.
- 'max_accounts_per_toolkit' (int): Max connected accounts per
toolkit (2-10). Defaults to 5.
- 'require_explicit_selection' (bool): When True, require explicit
account selection when multiple accounts are connected.
Example: {'enable': True, 'max_accounts_per_toolkit': 3}
:param preload: Optional preload configuration. Dict with:
- 'tools' (List[str] | 'all'): Tool slugs to expose directly in
session.tools() and MCP tool lists. 'all' exposes every app
tool allowed by positive session filters such as toolkits,
tools, or tags; the backend validates and caps the final set.
Example: {'tools': ['GMAIL_FETCH_EMAILS']}
:param session_preset: Optional session preset. Use
SESSION_PRESET_DIRECT_TOOLS when all needed tools are
known upfront and should be exposed directly from
session.tools() and the MCP tool list. It disables
meta/helper tools by default; explicit overrides still win.
Loading many tools can increase model context usage.
:param experimental: Optional experimental configuration (ToolRouterExperimentalConfig).
Note: These features are experimental and may change.
Dict with:
- 'assistive_prompt' (dict): Configuration for assistive prompt generation.
- 'user_timezone' (str): IANA timezone identifier
(e.g., "America/New_York", "Europe/London").
- 'custom_tools' / 'custom_toolkits': SDK custom tools.
Set preload=True on a custom tool or toolkit to expose
it directly from session.tools(); otherwise custom tools
remain search-only.
Example: {'assistive_prompt': {'user_timezone': 'America/New_York'}}
:param mcp: When True, the returned session surfaces its hosted MCP
endpoint (``session.mcp.url`` / ``session.mcp.headers``) in
the type (returns ToolRouterSessionWithMcp). The endpoint
exists on every session at runtime regardless of this flag;
native tools (``session.tools()``) are unaffected.
See https://docs.composio.dev/docs/sessions-via-mcp
:return: Tool router session object
Example:
```python
# Create a basic session
session = tool_router.create(user_id='user_123')
# Create a session with specific toolkits
session = tool_router.create(
user_id='user_123',
toolkits=['github', 'slack']
)
# Create a direct-tools session when all needed tools are known upfront
session = tool_router.create(
user_id='user_123',
session_preset=SESSION_PRESET_DIRECT_TOOLS,
toolkits=['github']
)
# Create a session with per-toolkit tool configuration
session = tool_router.create(
user_id='user_123',
tools={
'gmail': ['GMAIL_SEND_EMAIL', 'GMAIL_SEARCH'], # List shorthand
'github': {'enable': ['GITHUB_CREATE_ISSUE']}, # Explicit enable
'slack': {'disable': ['SLACK_DELETE_MESSAGE']}, # Explicit disable
}
)
# Create a session with global tag filtering
session = tool_router.create(
user_id='user_123',
tags=['readOnlyHint', 'idempotentHint']
)
# Create a session with toolkit-specific tag filtering (array format)
session = tool_router.create(
user_id='user_123',
tools={
'gmail': {'tags': ['readOnlyHint']},
'github': {'tags': ['readOnlyHint', 'idempotentHint']}
}
)
# Create a session with toolkit-specific tag filtering (object format)
session = tool_router.create(
user_id='user_123',
tools={
'gmail': {'tags': {'enable': ['readOnlyHint']}},
'github': {'tags': {'enable': ['readOnlyHint'], 'disable': ['destructiveHint']}}
}
)
# Create a session with connection management
session = tool_router.create(
user_id='user_123',
manage_connections={
'enable': True,
'callback_url': 'https://example.com/callback',
'wait_for_connections': True,
}
)
# Create a session with sandbox disabled
session = tool_router.create(
user_id='user_123',
sandbox={
'enable': False
}
)
# Create a session with sandbox config
session = tool_router.create(
user_id='user_123',
sandbox={
'enable_proxy_execution': False,
'auto_offload_threshold': 300
}
)
# Use the session
tools = session.tools()
connection = session.authorize('github')
toolkit_states = session.toolkits()
```
"""
if sandbox is not None and workbench is not None:
raise InvalidParams(
"Pass either `sandbox` or `workbench`, not both. "
"`workbench` is a backwards-compatible alias for `sandbox`."
)
sandbox_config: t.Optional[ToolRouterSandboxConfig] = (
sandbox if sandbox is not None else workbench
)
direct_tools_preset = session_preset == SESSION_PRESET_DIRECT_TOOLS
manage_connections, sandbox_config, preload = _apply_session_preset_defaults(
session_preset=session_preset,
manage_connections=manage_connections,
workbench=sandbox_config,
preload=preload,
)
default_custom_preload = _preloads_all_custom_tools(preload)
# Parse manage_connections config
manage_connections = (
manage_connections if manage_connections is not None else True
)
auto_manage_connections = (
manage_connections
if isinstance(manage_connections, bool)
else manage_connections.get("enable", True)
)
# Parse toolkits config
toolkits_payload: t.Optional[t.Dict[str, t.List[str]]] = None
if toolkits is not None:
if isinstance(toolkits, list):
toolkits_payload = {"enable": toolkits}
else:
toolkits_payload = t.cast(t.Dict[str, t.List[str]], toolkits)
# Parse tools config - transform to API format
# Transform tools from Dict[str, Union[List, enable/disable/tags]] to client SDK format
tools_payload: t.Optional[t.Dict[str, t.Any]] = None
if tools is not None:
tools_payload = {}
for toolkit_slug, config in tools.items():
if isinstance(config, list):
# List shorthand means enable these tools
tools_payload[toolkit_slug] = {"enable": config}
elif isinstance(config, dict):
# Transform config dict - handle 'tags' specially if present
# Build the transformed config explicitly to maintain proper typing
transformed_config: t.Dict[
str,
t.Union[
t.List[str],
session_create_params.TagsUnionMember1,
],
] = {}
# Copy existing keys (enable, disable) if present
if "enable" in config:
# config is ToolRouterToolsEnableConfig when "enable" is present
enable_config = t.cast(ToolRouterToolsEnableConfig, config)
transformed_config["enable"] = enable_config["enable"]
if "disable" in config:
# config is ToolRouterToolsDisableConfig when "disable" is present
disable_config = t.cast(ToolRouterToolsDisableConfig, config)
transformed_config["disable"] = disable_config["disable"]
# Use type guard to narrow the type when "tags" is present
if _is_tools_tags_config(config):
# Type narrowed: config is now ToolRouterToolsTagsConfig
tags_value = config["tags"]
transformed_tags = self._transform_tags_params(tags_value)
if transformed_tags is not None:
transformed_config["tags"] = transformed_tags
tools_payload[toolkit_slug] = transformed_config
# Parse callback_url and wait_for_connections from manage_connections config
callback_url = (
manage_connections.get("callback_url")
if isinstance(manage_connections, dict)
else omit
)
wait_for_connections = (
manage_connections.get("wait_for_connections")
if isinstance(manage_connections, dict)
else omit
)
# Build the API payload
create_params: t.Dict[str, t.Any] = {
"user_id": user_id,
}
# Build connections config
connections_config: t.Dict[str, t.Any] = {
"enable": auto_manage_connections,
}
if callback_url is not None and callback_url is not omit:
connections_config["callback_url"] = callback_url
if wait_for_connections is not None and wait_for_connections is not omit:
connections_config["enable_wait_for_connections"] = wait_for_connections
create_params["manage_connections"] = connections_config
# Add optional fields
if auth_configs is not None:
create_params["auth_configs"] = auth_configs
if connected_accounts is not None:
create_params["connected_accounts"] = {
k: [v] if isinstance(v, str) else v
for k, v in connected_accounts.items()
}
if toolkits_payload is not None:
create_params["toolkits"] = toolkits_payload
if tools_payload:
create_params["tools"] = tools_payload
# Transform tags config
tags_payload = self._transform_tags_params(tags)
if tags_payload is not None:
create_params["tags"] = tags_payload
if sandbox_config is not None:
execution_payload: t.Dict[str, t.Any] = {
"enable": sandbox_config.get("enable", True),
}
if "enable_proxy_execution" in sandbox_config:
execution_payload["enable_proxy_execution"] = sandbox_config[
"enable_proxy_execution"
]
if "auto_offload_threshold" in sandbox_config:
execution_payload["auto_offload_threshold"] = int(
sandbox_config["auto_offload_threshold"]
)
if "sandbox_size" in sandbox_config:
execution_payload["sandbox_size"] = sandbox_config["sandbox_size"]
if execution_payload:
create_params["workbench"] = execution_payload
if multi_account is not None:
ma = dict(multi_account)
if ma.get("enable") and "require_explicit_selection" not in ma:
ma["require_explicit_selection"] = True
create_params["multi_account"] = ma
if preload is not None:
create_params["preload"] = preload
if direct_tools_preset:
create_params["search"] = {"enable": False}
create_params["execute"] = {"enable_multi_execute": False}
# Build experimental config
# Map SDK's experimental.assistive_prompt.user_timezone to API's
# experimental.assistive_prompt_config.user_timezone
custom_tools: t.Optional[t.List[CustomTool]] = None
custom_toolkits: t.Optional[t.List[ExperimentalToolkit]] = None
inline_custom_tools_payload: t.Optional[InlineCustomToolsWirePayload] = None
experimental_payload: t.Dict[str, t.Any] = {}
if experimental is not None:
assistive_prompt_config = experimental.get("assistive_prompt")
if assistive_prompt_config is not None:
user_timezone = assistive_prompt_config.get("user_timezone")
if user_timezone:
experimental_payload["assistive_prompt_config"] = {
"user_timezone": user_timezone,
}
# Serialize custom tools and toolkits for the backend
custom_tools = experimental.get("custom_tools")
custom_toolkits = experimental.get("custom_toolkits")
inline_custom_tools_payload = _prepare_inline_custom_tools(
custom_tools=custom_tools,
custom_toolkits=custom_toolkits,
default_preload=default_custom_preload,
preload_tools=preload.get("tools") if preload is not None else None,
)
if inline_custom_tools_payload:
if "custom_tools" in inline_custom_tools_payload:
experimental_payload["custom_tools"] = inline_custom_tools_payload[
"custom_tools"
]
if "custom_toolkits" in inline_custom_tools_payload:
experimental_payload["custom_toolkits"] = inline_custom_tools_payload[
"custom_toolkits"
]
if experimental_payload:
create_params["experimental"] = experimental_payload
if self._provider is None:
raise ValueError(
"Provider is required for tool router. "
"Please initialize ToolRouter with a provider."
)
# Make API call to create session
session = self._client.tool_router.session.create(**create_params)
# Build custom tools routing map from backend response
custom_tools_map: t.Optional[CustomToolsMap] = None
if custom_tools or custom_toolkits:
custom_tools_map = build_custom_tools_map_from_response(
tools=custom_tools or [],
toolkits=custom_toolkits,
experimental=session.experimental,
)
preloaded_custom_tool_slugs = get_preloaded_custom_tool_slugs(
custom_tools_map,
default_preload=default_custom_preload,
)
# Transform experimental response:
# API's assistive_prompt -> SDK's assistive_prompt
# files mount is always present
files_mount = ToolRouterSessionFilesMount(self._client, session.session_id)
experimental_response = ToolRouterSessionExperimental(
files=files_mount,
assistive_prompt=(
session.experimental.assistive_prompt if session.experimental else None
),
)
# Create and return the session. `mcp=True` returns the subclass that
# surfaces `session.mcp` in the type; the runtime object is otherwise
# identical (the base always stores the MCP endpoint).
session_cls: t.Type[ToolRouterSession[TTool, TToolCollection]] = (
ToolRouterSessionWithMcp if mcp else ToolRouterSession
)
return session_cls(
client=self._client,
provider=self._provider,
dangerously_allow_auto_upload_download_files=self._auto_upload_download_files,
sensitive_file_upload_protection=self._sensitive_file_upload_protection,
file_upload_path_deny_segments=self._file_upload_path_deny_segments,
file_upload_dirs=self._file_upload_dirs,
session_id=session.session_id,
mcp=self._create_mcp_server_config(
mcp_type=ToolRouterMCPServerType(session.mcp.type.lower()),
url=session.mcp.url,
),
experimental=experimental_response,
custom_tools_map=custom_tools_map,
user_id=user_id,
preload=_session_preload_config(session.config.preload),
preloaded_custom_tool_slugs=preloaded_custom_tool_slugs,
inline_custom_tools_payload=inline_custom_tools_payload,
)
# Overloads mirror create(): pass ``mcp=True`` to surface ``session.mcp`` in
# the returned type. See https://docs.composio.dev/docs/sessions-via-mcp
@t.overload
def use(
self,
session_id: str,
*,
custom_tools: t.Optional[t.List[CustomTool]] = None,
custom_toolkits: t.Optional[t.List[ExperimentalToolkit]] = None,
mcp: t.Literal[True],
) -> ToolRouterSessionWithMcp[TTool, TToolCollection]: ...
@t.overload
def use(
self,
session_id: str,
*,
custom_tools: t.Optional[t.List[CustomTool]] = None,
custom_toolkits: t.Optional[t.List[ExperimentalToolkit]] = None,
mcp: t.Literal[False] = False,
) -> ToolRouterSession[TTool, TToolCollection]: ...
def use(
self,
session_id: str,
*,
custom_tools: t.Optional[t.List[CustomTool]] = None,
custom_toolkits: t.Optional[t.List[ExperimentalToolkit]] = None,
mcp: bool = False,
) -> ToolRouterSession[TTool, TToolCollection]:
"""
Use an existing tool router session.
Provide ``custom_tools`` or ``custom_toolkits`` to bind SDK-local tools
to the session for search and execution.
If the existing session has ``preload.tools = "all"``, every bound
custom tool is also exposed via ``session.tools()`` and re-injected
with ``preload=True`` on subsequent search/execute calls. A custom tool
with explicit ``preload=False`` still opts out.
:param session_id: The session ID to use.
:param custom_tools: Optional custom tools to bind to the session.
:param custom_toolkits: Optional custom toolkits to bind to the session.
:param mcp: When True, the returned session surfaces its hosted MCP
endpoint in the type (returns ToolRouterSessionWithMcp).
See https://docs.composio.dev/docs/sessions-via-mcp
:return: Tool router session object
Example:
```python
from composio import Composio
composio = Composio()
# Use an existing session
session = composio.use('session_123')
# Use an existing session with custom tools
session = composio.use(
'session_123',
custom_tools=[my_tool],
custom_toolkits=[my_toolkit],
)
```
"""
if self._provider is None:
raise ValueError(
"Provider is required for tool router. "
"Please initialize ToolRouter with a provider."
)
has_customs = bool(custom_tools or custom_toolkits)
attach_inline_custom_tools_payload = _prepare_inline_custom_tools(
custom_tools=custom_tools,
custom_toolkits=custom_toolkits,
)
inline_custom_tools_payload = attach_inline_custom_tools_payload
session: t.Union[SessionAttachResponse, SessionRetrieveResponse]
if has_customs:
session = self._client.tool_router.session.attach(
session_id,
experimental=inline_custom_tools_attach_experimental(
attach_inline_custom_tools_payload
),
)
else:
session = self._client.tool_router.session.retrieve(session_id)
default_custom_preload = _preloads_all_custom_tools(session.config.preload)
if has_customs and default_custom_preload:
# preload.tools = "all" on the existing session is server-authoritative:
# the backend exposes every custom tool regardless of per-definition
# preload flags, so the initial attach above (which used the caller's
# explicit hints) doesn't need re-sending. We only rebuild the in-memory
# payload so future search/execute calls re-inject with preload=True and
# session.tools() locally mirrors what the server returns.
inline_custom_tools_payload = _prepare_inline_custom_tools(
custom_tools=custom_tools,
custom_toolkits=custom_toolkits,
default_preload=True,
)
custom_tools_map: t.Optional[CustomToolsMap] = None
user_id: t.Optional[str] = None
if has_customs:
custom_tools_map = build_custom_tools_map_from_response(
tools=custom_tools or [],
toolkits=custom_toolkits,
experimental=session.experimental,
)
user_id = session.config.user_id
preloaded_custom_tool_slugs = get_preloaded_custom_tool_slugs(
custom_tools_map,
default_preload=default_custom_preload,
)
files_mount = ToolRouterSessionFilesMount(self._client, session.session_id)
experimental_response = ToolRouterSessionExperimental(
files=files_mount,
assistive_prompt=None,
)
session_cls: t.Type[ToolRouterSession[TTool, TToolCollection]] = (
ToolRouterSessionWithMcp if mcp else ToolRouterSession
)
return session_cls(
client=self._client,
provider=self._provider,
dangerously_allow_auto_upload_download_files=self._auto_upload_download_files,
sensitive_file_upload_protection=self._sensitive_file_upload_protection,
file_upload_path_deny_segments=self._file_upload_path_deny_segments,
file_upload_dirs=self._file_upload_dirs,
session_id=session.session_id,
mcp=self._create_mcp_server_config(
mcp_type=ToolRouterMCPServerType(session.mcp.type.lower()),
url=session.mcp.url,
),
experimental=experimental_response,
custom_tools_map=custom_tools_map,
user_id=user_id,
preload=_session_preload_config(session.config.preload),
preloaded_custom_tool_slugs=preloaded_custom_tool_slugs,
inline_custom_tools_payload=inline_custom_tools_payload,
)
def delete(self, session_id: str) -> ToolRouterSessionDeleteResponse:
"""
Delete a tool router session by ID.
Deleted sessions immediately stop being retrievable or executable. A
missing or already-deleted session surfaces the backend 404.
"""
return delete_tool_router_session(self._client, session_id)
__all__ = [
"ToolRouter",
"ToolRouterSession",
"ToolRouterSessionWithMcp",
"ToolRouterSessionDeleteResponse",
"ToolRouterSessionExperimental",
"ToolRouterToolkitsEnableConfig",
"ToolRouterToolkitsDisableConfig",
"ToolRouterToolsEnableConfig",
"ToolRouterToolsDisableConfig",
"ToolRouterToolsTagsConfig",
"ToolRouterToolsConfig",
"ToolRouterTag",
"ToolRouterTagsEnableDisableConfig",
"ToolRouterConfigTags",
"ToolRouterManageConnectionsConfig",
"ToolRouterSandboxConfig",
"ToolRouterWorkbenchConfig",
"SandboxSize",
"SessionPreset",
"SESSION_PRESET_DIRECT_TOOLS",
"ToolRouterMultiAccountConfig",
"ToolRouterPreloadConfig",
"ToolRouterExperimentalConfig",
"ToolRouterAssistivePromptConfig",
"ToolRouterSessionPreloadConfig",
"ToolkitConnectionState",
"ToolkitConnectionsDetails",
"ToolRouterMCPServerConfig",
"ToolRouterMCPServerType",
]