diff --git a/backend/app/api/endpoints/openapi_responses.py b/backend/app/api/endpoints/openapi_responses.py index fb39f4e6a..ab087fa25 100644 --- a/backend/app/api/endpoints/openapi_responses.py +++ b/backend/app/api/endpoints/openapi_responses.py @@ -390,9 +390,20 @@ async def _create_non_streaming_response_unified( # Extract knowledge base names from tool settings knowledge_base_names = tool_settings.get("knowledge_base_names", []) - # Auto-enable tools when knowledge_base is specified - # This ensures KB tools and skill tools are actually added to the agent - enable_tools = enable_chat_bot or bool(knowledge_base_names) + # Extract user-provided MCP servers from tool settings + user_mcp_servers = tool_settings.get("mcp_servers", {}) + # Convert dict format to list format for build_execution_request + mcp_servers_list = [] + if user_mcp_servers: + for name, config in user_mcp_servers.items(): + if isinstance(config, dict): + mcp_servers_list.append({"name": name, **config}) + + # Auto-enable tools when knowledge_base or user MCP servers are specified + # This ensures KB tools, skill tools, and user MCP tools are actually added to the agent + enable_tools = ( + enable_chat_bot or bool(knowledge_base_names) or bool(mcp_servers_list) + ) # Link attachments to user subtask if provided if request_body.attachment_ids: @@ -428,6 +439,7 @@ async def _create_non_streaming_response_unified( preload_skills=preload_skills, knowledge_base_names=knowledge_base_names, reasoning_config=reasoning_config, + mcp_servers=mcp_servers_list, ) except Exception as e: logger.error(f"Failed to build execution request: {e}") @@ -697,9 +709,20 @@ async def _create_streaming_response_unified( # Extract knowledge base names from tool settings knowledge_base_names = tool_settings.get("knowledge_base_names", []) - # Auto-enable tools when knowledge_base is specified - # This ensures KB tools and skill tools are actually added to the agent - enable_tools = enable_chat_bot or bool(knowledge_base_names) + # Extract user-provided MCP servers from tool settings + user_mcp_servers = tool_settings.get("mcp_servers", {}) + # Convert dict format to list format for build_execution_request + mcp_servers_list = [] + if user_mcp_servers: + for name, config in user_mcp_servers.items(): + if isinstance(config, dict): + mcp_servers_list.append({"name": name, **config}) + + # Auto-enable tools when knowledge_base or user MCP servers are specified + # This ensures KB tools, skill tools, and user MCP tools are actually added to the agent + enable_tools = ( + enable_chat_bot or bool(knowledge_base_names) or bool(mcp_servers_list) + ) # Link attachments to user subtask if provided if request_body.attachment_ids: @@ -735,6 +758,7 @@ async def _create_streaming_response_unified( preload_skills=preload_skills, knowledge_base_names=knowledge_base_names, reasoning_config=reasoning_config, + mcp_servers=mcp_servers_list, ) finally: # Close the database session before streaming starts diff --git a/backend/app/services/chat/trigger/unified.py b/backend/app/services/chat/trigger/unified.py index 38a12171a..b63d6095a 100644 --- a/backend/app/services/chat/trigger/unified.py +++ b/backend/app/services/chat/trigger/unified.py @@ -187,6 +187,7 @@ async def build_execution_request( previous_bot_id: Optional[int] = None, knowledge_base_names: Optional[List[Dict[str, str]]] = None, reasoning_config: Optional[Dict[str, Any]] = None, + mcp_servers: Optional[List[Dict[str, Any]]] = None, ): """Build ExecutionRequest without dispatching. @@ -211,6 +212,7 @@ async def build_execution_request( preload_skills: Optional list of skills to preload knowledge_base_names: Optional list of KB names in {'namespace': str, 'name': str} format reasoning_config: Optional reasoning config dict with 'effort' and 'summary' keys + mcp_servers: Optional list of user-provided MCP server configs Returns: ExecutionRequest ready for dispatch @@ -272,6 +274,7 @@ async def build_execution_request( override_model_name=override_model_name, force_override=force_override, previous_bot_id=previous_bot_id, + user_mcp_servers=mcp_servers, ) # Merge reasoning_config from API request into model_config diff --git a/backend/app/services/execution/request_builder.py b/backend/app/services/execution/request_builder.py index 94df4e226..e62b67ab6 100644 --- a/backend/app/services/execution/request_builder.py +++ b/backend/app/services/execution/request_builder.py @@ -11,7 +11,7 @@ """ import logging -from typing import Any, List, Optional, Union +from typing import Any, Dict, List, Optional, Union from pydantic import BaseModel from sqlalchemy.orm import Session @@ -104,6 +104,8 @@ def build( override_model_name: Optional[str] = None, force_override: bool = False, team_member_prompt: Optional[str] = None, + # User-provided MCP servers (from API request) + user_mcp_servers: Optional[List[Dict[str, Any]]] = None, ) -> ExecutionRequest: """Build ExecutionRequest from database models. @@ -132,6 +134,7 @@ def build( override_model_name: Optional model name to override bot's model force_override: If True, override takes highest priority team_member_prompt: Optional additional prompt from team member + user_mcp_servers: Optional list of user-provided MCP server configs from API request Returns: ExecutionRequest ready for dispatch @@ -276,6 +279,7 @@ def build( user=user, is_subscription=is_subscription, auth_token=auth_token, + user_mcp_servers=user_mcp_servers, ) # Get collaboration model @@ -1549,6 +1553,7 @@ def _build_mcp_servers( user: User, is_subscription: bool = False, auth_token: str = "", + user_mcp_servers: Optional[List[Dict[str, Any]]] = None, ) -> list[dict]: """Build MCP servers configuration. @@ -1556,9 +1561,10 @@ def _build_mcp_servers( 1. System-level MCP servers (from CHAT_MCP_SERVERS setting) 2. Bot-level MCP servers (from Ghost CRD mcpServers config) 3. Auto-injected System MCP (for subscription tasks) + 4. User-provided MCP servers (from API request tools) - Bot-level servers take precedence over system-level servers - when there are name conflicts. + User-provided servers take highest precedence, then bot-level, + then system-level when there are name conflicts. Ghost CRD format (dict): {"server_name": {"url": "...", "type": "...", "headers": {...}}} @@ -1571,6 +1577,7 @@ def _build_mcp_servers( team: Team Kind object is_subscription: Whether this is a subscription task auth_token: Authentication token for MCP server + user_mcp_servers: Optional list of user-provided MCP server configs from API request Returns: List of MCP server configuration dictionaries @@ -1625,24 +1632,53 @@ def _build_mcp_servers( server_entry["env"] = server_config["env"] bot_mcp_servers.append(server_entry) - # Merge system and bot MCP servers (bot takes precedence) + # Process user-provided MCP servers from API request + user_mcp_list = [] + if user_mcp_servers: + for server_config in user_mcp_servers: + if isinstance(server_config, dict): + server_entry = { + "name": server_config.get("name", "user-server"), + "url": server_config.get("url", ""), + "type": server_config.get("type", "streamable-http"), + } + # Convert "headers" to "auth" for chat_shell compatibility + if "headers" in server_config: + server_entry["auth"] = server_config["headers"] + # Include stdio-specific fields (command, args, env) + if "command" in server_config: + server_entry["command"] = server_config["command"] + if "args" in server_config: + server_entry["args"] = server_config["args"] + if "env" in server_config: + server_entry["env"] = server_config["env"] + user_mcp_list.append(server_entry) + + # Merge MCP servers from all sources (user takes highest precedence) # Build a dict to deduplicate by server name servers_by_name = {} + # 1. System-level servers (lowest precedence) for server in system_mcp_servers: server_name = server.get("name", "server") servers_by_name[server_name] = server + # 2. Bot-level servers (medium precedence) for server in bot_mcp_servers: server_name = server.get("name", "server") servers_by_name[server_name] = server + # 3. User-provided servers (highest precedence) + for server in user_mcp_list: + server_name = server.get("name", "server") + servers_by_name[server_name] = server merged_servers = list(servers_by_name.values()) if merged_servers: logger.info( - "[TaskRequestBuilder] Built %d MCP servers (system=%d, bot=%d): %s", + "[TaskRequestBuilder] Built %d MCP servers (system=%d, bot=%d, user=%d): %s", len(merged_servers), len(system_mcp_servers), len(bot_mcp_servers), + len(user_mcp_list), [s["name"] for s in merged_servers], )