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

501 lines
19 KiB
Python

"""
MCP (Model Control Protocol) module for Composio SDK.
This module provides MCP server operations
"""
from __future__ import annotations
import typing as t
import typing_extensions as te
from composio_client.types.mcp.custom_create_response import CustomCreateResponse
from composio.client import HttpClient
from composio.core.models.base import Resource
from composio.exceptions import ValidationError
from composio.utils.pydantic import none_to_omit
# Data Types (matching TypeScript specification)
class ConfigToolkit(te.TypedDict, total=False):
"""Toolkit configuration for an MCP server.
Defined locally rather than imported from ``composio_client``: the generated
client removed ``types.tool_router_create_session_params.ConfigToolkit`` in
1.41.0 when the tool-router session schema was redesigned. The MCP surface
only needs the toolkit slug and optional auth config, so we keep our own
minimal shape and stay decoupled from tool-router regen churn.
"""
toolkit: te.Required[str]
"""Toolkit identifier (e.g., gmail, slack, github)."""
auth_config_id: str
"""Specific auth configuration ID for this toolkit."""
class MCPCreateResponse(CustomCreateResponse):
"""MCP Create Response with generate method (extends CustomCreateResponse)."""
generate: t.Callable[[str, t.Optional[bool]], "MCPServerInstance"]
class MCPServerInstance(te.TypedDict):
"""MCP Server Instance data structure (matching TypeScript implementation)."""
id: str
name: str
type: str
url: str # User-specific connection URL
user_id: str # Associated user ID
allowed_tools: t.List[str] # Available tools for the user
auth_configs: t.List[str] # Associated auth configurations
class MCPItem(te.TypedDict):
"""Complete MCP server information."""
id: str # Unique server identifier
name: str # Human-readable server name
allowed_tools: t.List[str] # Array of enabled tool identifiers
auth_config_ids: t.List[str] # Array of auth configuration IDs
toolkits: t.List[str] # Array of toolkit names
commands: t.Dict[str, str] # Setup commands for different clients
mcp_url: str # Server connection URL
toolkit_icons: t.Dict[str, str] # Map of toolkit icons
server_instance_count: int # Number of active instances
created_at: te.NotRequired[t.Optional[str]]
updated_at: te.NotRequired[t.Optional[str]]
class MCPListResponse(te.TypedDict):
"""Paginated list response."""
items: t.List[t.Any] # Array of MCP server objects (raw API response)
current_page: int # Current page number
total_pages: int # Total number of pages
def _add_generate_method(
response: CustomCreateResponse, mcp_instance: "MCP"
) -> MCPCreateResponse:
"""Add generate method to CustomCreateResponse object."""
def generate(
user_id: str, manually_manage_connections: t.Optional[bool] = None
) -> MCPServerInstance:
"""
Generate server instance for this MCP configuration.
Matches TypeScript server.generate(userId) method.
:param user_id: External user ID from your database
:param manually_manage_connections: Whether to manually manage connections (optional)
:return: MCP server instance
"""
return mcp_instance.generate(user_id, response.id, manually_manage_connections)
# Add the generate method to the response object and cast to MCPCreateResponse
response.generate = generate # type: ignore
return t.cast(MCPCreateResponse, response)
class MCP(Resource):
"""
MCP (Model Control Protocol) class.
Provides enhanced MCP server operations
This matches the TypeScript ExperimentalMCP class functionality.
.. deprecated::
Use a session's MCP endpoint instead:
``composio.create(user_id, mcp=True)`` returns a session that exposes
``session.mcp.url`` / ``session.mcp.headers``. MCP is now opt-in per
session; this standalone ``composio.mcp`` server-management API is kept
only for backwards compatibility. Prefer the session MCP endpoint; do
not generate new code against ``composio.mcp``.
See https://docs.composio.dev/docs/sessions-via-mcp
"""
def __init__(self, client: HttpClient):
"""
Initialize MCP instance.
:param client: HTTP client for API calls
"""
super().__init__(client)
def create(
self,
name: str,
toolkits: t.List[t.Union[ConfigToolkit, str]],
manually_manage_connections: bool = False,
allowed_tools: t.Optional[t.List[str]] = None,
) -> MCPCreateResponse:
"""
Create a new MCP server configuration with specified toolkits and authentication settings.
:param name: Unique name for the MCP configuration
:param toolkits: List of toolkit configurations. Can be either:
- MCPToolkitConfig objects with detailed configuration
- Strings representing toolkit names (for simple cases)
:param manually_manage_connections: Whether to manually manage account connections (default: False)
:param allowed_tools: List of specific tools to enable across all toolkits (default: None for all tools)
:return: Created server details with generate method
Examples:
>>> # Using toolkit configuration objects with auth
>>> server = composio.experimental.mcp.create(
... 'personal-mcp-server',
... toolkits=[
... {
... 'toolkit': 'github',
... 'auth_config_id': 'ac_xyz',
... },
... {
... 'toolkit': 'slack',
... 'auth_config_id': 'ac_abc',
... },
... ],
... allowed_tools=['GITHUB_CREATE_ISSUE', 'GITHUB_LIST_REPOS', 'SLACK_SEND_MESSAGE'],
... manually_manage_connections=False
... )
>>>
>>> # Using simple toolkit names (most common usage)
>>> server = composio.experimental.mcp.create(
... 'simple-mcp-server',
... toolkits=['composio_search', 'text_to_pdf'],
... allowed_tools=['COMPOSIO_SEARCH_DUCK_DUCK_GO_SEARCH', 'TEXT_TO_PDF_CONVERT_TEXT_TO_PDF']
... )
>>>
>>> # Using all tools from toolkits (default behavior)
>>> server = composio.experimental.mcp.create(
... 'all-tools-server',
... toolkits=['composio_search', 'text_to_pdf']
... # allowed_tools=None means all tools from these toolkits
... )
>>>
>>> # Get server instance for a user
>>> mcp = server.generate('user_12345')
"""
if not toolkits:
raise ValidationError("At least one toolkit configuration is required")
try:
# Normalize toolkits to MCPToolkitConfig format
normalized_toolkit_configs: t.List[ConfigToolkit] = []
for toolkit in toolkits:
if isinstance(toolkit, str):
# Convert string to MCPToolkitConfig
normalized_toolkit_configs.append(ConfigToolkit(toolkit=toolkit))
else:
# Already MCPToolkitConfig, use as-is
normalized_toolkit_configs.append(toolkit)
# Extract toolkits and prepare for API call
toolkit_configs = normalized_toolkit_configs
# Get unique toolkits and auth config IDs
toolkit_names = []
auth_config_ids = []
for toolkit_config in toolkit_configs:
if (
"toolkit" in toolkit_config
and toolkit_config["toolkit"] not in toolkit_names
):
toolkit_names.append(toolkit_config["toolkit"])
if (
"auth_config_id" in toolkit_config
and toolkit_config["auth_config_id"] not in auth_config_ids
):
auth_config_ids.append(toolkit_config["auth_config_id"])
# Use the allowed_tools parameter instead of individual toolkit configs
custom_tools = none_to_omit(allowed_tools)
# Use the custom MCP create endpoint
response = self._client.mcp.custom.create(
name=name,
toolkits=toolkit_names,
auth_config_ids=auth_config_ids,
custom_tools=custom_tools,
managed_auth_via_composio=not manually_manage_connections,
)
# Return response with generate method (matching TypeScript behavior)
return _add_generate_method(response, self)
except Exception as e:
raise ValidationError("Failed to create MCP server") from e
def list(
self,
page_no: t.Optional[int] = None,
limit: t.Optional[int] = None,
toolkits: t.Optional[str] = None,
auth_config_ids: t.Optional[str] = None,
name: t.Optional[str] = None,
order_by: t.Optional[te.Literal["created_at", "updated_at"]] = None,
order_direction: t.Optional[te.Literal["asc", "desc"]] = None,
) -> MCPListResponse:
"""
List MCP servers with optional filtering and pagination.
:param page_no: Page number for pagination (default: 1)
:param limit: Maximum items per page (default: 10)
:param toolkits: Filter by toolkit name (single string)
:param auth_config_ids: Filter by auth configuration ID (single string)
:param name: Filter by server name (partial match)
:param order_by: Order by field ('created_at' or 'updated_at')
:param order_direction: Order direction ('asc' or 'desc')
:return: Paginated list of MCP servers
Examples:
>>> # List all servers
>>> all_servers = composio.experimental.mcp.list()
>>>
>>> # List with pagination
>>> paged_servers = composio.experimental.mcp.list(page_no=2, limit=5)
>>>
>>> # Filter by toolkit
>>> github_servers = composio.experimental.mcp.list(toolkits='github', name='personal')
"""
try:
# Use the MCP list endpoint with filters
response = self._client.mcp.list(
page_no=page_no,
limit=limit,
toolkits=none_to_omit(toolkits),
auth_config_ids=none_to_omit(auth_config_ids),
name=none_to_omit(name),
order_by=none_to_omit(order_by),
order_direction=none_to_omit(order_direction),
)
items = (
response.items if hasattr(response, "items") and response.items else []
)
return MCPListResponse(
items=items,
current_page=getattr(response, "current_page", page_no or 1),
total_pages=getattr(response, "total_pages", 1),
)
except Exception as e:
raise ValidationError("Failed to list MCP servers") from e
def get(self, server_id: str):
"""
Retrieve detailed information about a specific MCP server/config.
:param server_id: The unique identifier of the MCP server/config
:return: Complete MCP server information
Example:
>>> server = composio.experimental.mcp.get('mcp_12345')
>>>
>>> print(server['name']) # "My Personal MCP Server"
>>> print(server['allowed_tools']) # ["GITHUB_CREATE_ISSUE", "SLACK_SEND_MESSAGE"]
>>> print(server['toolkits']) # ["github", "slack"]
>>> print(server['server_instance_count']) # 3
"""
try:
response = self._client.mcp.retrieve(server_id)
return response
except Exception as e:
raise ValidationError(f"Failed to retrieve MCP server {server_id}") from e
def update(
self,
server_id: str,
name: t.Optional[str] = None,
toolkits: t.Optional[t.List[t.Union[ConfigToolkit, str]]] = None,
manually_manage_connections: t.Optional[bool] = None,
allowed_tools: t.Optional[t.List[str]] = None,
):
"""
Update an existing MCP server configuration.
:param server_id: The unique identifier of the MCP server to update
:param name: Optional new name for the MCP server
:param toolkits: Optional list of toolkit configurations (strings or objects)
:param manually_manage_connections: Optional flag for connection management
:param allowed_tools: Optional list of specific tools to enable across all toolkits
:return: Updated MCP server information
Examples:
>>> # Update server name only
>>> updated_server = composio.experimental.mcp.update(
... 'mcp_12345',
... name='My Updated MCP Server'
... )
>>>
>>> # Update toolkits and tools
>>> server_with_new_tools = composio.experimental.mcp.update(
... 'mcp_12345',
... toolkits=['github', 'slack'],
... allowed_tools=['GITHUB_CREATE_ISSUE', 'SLACK_SEND_MESSAGE']
... )
>>>
>>> # Update with auth configs
>>> server_with_auth = composio.experimental.mcp.update(
... 'mcp_12345',
... toolkits=[
... {'toolkit': 'github', 'auth_config_id': 'auth_abc123'},
... {'toolkit': 'slack', 'auth_config_id': 'auth_def456'}
... ],
... allowed_tools=['GITHUB_CREATE_ISSUE', 'SLACK_SEND_MESSAGE'],
... manually_manage_connections=False
... )
"""
try:
update_params: t.Dict[str, t.Any] = {}
if name is not None:
update_params["name"] = name
if toolkits is not None:
# Normalize toolkits to ConfigToolkit format (same as create method)
normalized_toolkit_configs: t.List[ConfigToolkit] = []
for toolkit in toolkits:
if isinstance(toolkit, str):
# Convert string to ConfigToolkit
normalized_toolkit_configs.append(
ConfigToolkit(toolkit=toolkit)
)
else:
# Already ConfigToolkit, use as-is
normalized_toolkit_configs.append(toolkit)
# Extract toolkits and prepare for API call
toolkit_names = []
auth_config_ids = []
for toolkit_config in normalized_toolkit_configs:
if (
"toolkit" in toolkit_config
and toolkit_config["toolkit"] not in toolkit_names
):
toolkit_names.append(toolkit_config["toolkit"])
if (
"auth_config_id" in toolkit_config
and toolkit_config["auth_config_id"] not in auth_config_ids
):
auth_config_ids.append(toolkit_config["auth_config_id"])
update_params["toolkits"] = toolkit_names
update_params["auth_config_ids"] = auth_config_ids
if allowed_tools is not None:
update_params["custom_tools"] = allowed_tools
if manually_manage_connections is not None:
update_params[
"managed_auth_via_composio"
] = not manually_manage_connections
# Use the MCP update endpoint
response = self._client.mcp.update(server_id, **update_params)
return response
except Exception as e:
raise ValidationError(f"Failed to update MCP server {server_id}") from e
def delete(self, server_id: str) -> t.Dict[str, t.Any]:
"""
Permanently delete an MCP server configuration.
:param server_id: The unique identifier of the MCP server to delete
:return: Deletion result
Example:
>>> # Delete a server
>>> result = composio.experimental.mcp.delete('mcp_12345')
>>>
>>> if result['deleted']:
... print(f"Server {result['id']} has been successfully deleted")
>>> else:
... print(f"Failed to delete server {result['id']}")
"""
try:
response = self._client.mcp.delete(server_id)
return {
"id": server_id,
"deleted": getattr(response, "deleted", True),
}
except Exception as e:
raise ValidationError(f"Failed to delete MCP server {server_id}") from e
def generate(
self,
user_id: str,
mcp_config_id: str,
manually_manage_connections: t.Optional[bool] = None,
) -> MCPServerInstance:
"""
Get server URLs for an existing MCP server.
This matches the TypeScript implementation exactly.
:param user_id: External user ID from your database
:param mcp_config_id: MCP configuration ID
:param manually_manage_connections: Whether to manually manage connections (optional)
:return: MCP server instance
Example:
>>> mcp = composio.experimental.mcp.generate(
... 'user_12345',
... 'mcp_67890',
... manually_manage_connections=False
... )
>>>
>>> print(mcp['url']) # Server URL for the user
>>> print(mcp['allowed_tools']) # Available tools
"""
try:
# Get server details first (matching TS: this.client.mcp.retrieve)
server_details = self._client.mcp.retrieve(mcp_config_id)
# Generate server URLs (matching TS: this.client.mcp.generate.url)
url_response = self._client.mcp.generate.url(
mcp_server_id=mcp_config_id,
user_ids=[user_id],
managed_auth_by_composio=False if manually_manage_connections else True,
)
# Get the generated URL (matching TS: urlResponse.user_ids_url[0])
if hasattr(url_response, "user_ids_url") and url_response.user_ids_url:
server_url = url_response.user_ids_url[0]
else:
raise ValidationError("No server URL generated")
# Create structured server instance (matching TS MCPServerInstance)
server_instance: MCPServerInstance = {
"id": server_details.id,
"name": server_details.name,
"type": "streamable_http",
"url": server_url,
"user_id": user_id,
"allowed_tools": getattr(server_details, "allowed_tools", []),
"auth_configs": getattr(server_details, "auth_config_ids", []),
}
return server_instance
except Exception as e:
if isinstance(e, ValidationError):
raise
raise ValidationError("Failed to parse MCP server instance") from e