Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 30 additions & 6 deletions backend/app/api/endpoints/openapi_responses.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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}")
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions backend/app/services/chat/trigger/unified.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
46 changes: 41 additions & 5 deletions backend/app/services/execution/request_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -1549,16 +1553,18 @@ 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.

Merges MCP servers from multiple sources:
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": {...}}}
Expand All @@ -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
Expand Down Expand Up @@ -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)
Comment on lines +1635 to +1655
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Preserve user MCP transport and auth fields during normalization.

The system MCP loader accepts transport as a fallback for type, but user-provided MCP servers ignore it and default to streamable-http. A user config using transport: "stdio" would be emitted as HTTP even though command/args are present. Also preserve already-normalized auth when callers provide it directly.

🐛 Proposed normalization fix
                     server_entry = {
                         "name": server_config.get("name", "user-server"),
                         "url": server_config.get("url", ""),
-                        "type": server_config.get("type", "streamable-http"),
+                        "type": server_config.get(
+                            "type",
+                            server_config.get("transport", "streamable-http"),
+                        ),
                     }
                     # Convert "headers" to "auth" for chat_shell compatibility
                     if "headers" in server_config:
                         server_entry["auth"] = server_config["headers"]
+                    elif "auth" in server_config:
+                        server_entry["auth"] = server_config["auth"]
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/app/services/execution/request_builder.py` around lines 1635 - 1655,
The normalization currently forces server_entry["type"] =
server_config.get("type", "streamable-http") and overwrites/ignores
server_config["auth"]; update the user_mcp_servers loop (the code building
server_entry and appending to user_mcp_list) so that: 1) if server_config
contains "type" use it, else if it contains "transport" use that value as the
type fallback, else default to "streamable-http"; 2) preserve
server_config["auth"] if present (do not replace it), and only convert "headers"
into "auth" when "auth" is absent; keep the existing handling for "command",
"args", and "env" and continue appending server_entry to user_mcp_list.


# 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],
)

Expand Down
Loading