diff --git a/surfsense_backend/app/agents/new_chat/tools/mcp_tool.py b/surfsense_backend/app/agents/new_chat/tools/mcp_tool.py index 81c7d074f..dd374cdae 100644 --- a/surfsense_backend/app/agents/new_chat/tools/mcp_tool.py +++ b/surfsense_backend/app/agents/new_chat/tools/mcp_tool.py @@ -141,40 +141,46 @@ async def load_mcp_tools( tools: list[StructuredTool] = [] for connector in result.scalars(): try: - # Extract server config + # Extract server configs array config = connector.config or {} - server_config = config.get("server_config", {}) - - command = server_config.get("command") - args = server_config.get("args", []) - env = server_config.get("env", {}) - - if not command: - logger.warning(f"MCP connector {connector.id} missing command, skipping") + server_configs = config.get("server_configs", []) + + if not server_configs: + logger.warning(f"MCP connector {connector.id} missing server_configs, skipping") continue - # Create MCP client - mcp_client = MCPClient(command, args, env) + # Process each server config + for server_config in server_configs: + command = server_config.get("command") + args = server_config.get("args", []) + env = server_config.get("env", {}) - # Connect and discover tools - async with mcp_client.connect(): - tool_definitions = await mcp_client.list_tools() + if not command: + logger.warning(f"MCP connector {connector.id} server config missing command, skipping") + continue - logger.info( - f"Discovered {len(tool_definitions)} tools from MCP server " - f"'{command}' (connector {connector.id})" - ) + # Create MCP client + mcp_client = MCPClient(command, args, env) - # Create LangChain tools from definitions - for tool_def in tool_definitions: - try: - tool = await _create_mcp_tool_from_definition(tool_def, mcp_client) - tools.append(tool) - except Exception as e: - logger.exception( - f"Failed to create tool '{tool_def.get('name')}' " - f"from connector {connector.id}: {e!s}", - ) + # Connect and discover tools + async with mcp_client.connect(): + tool_definitions = await mcp_client.list_tools() + + logger.info( + f"Discovered {len(tool_definitions)} tools from MCP server " + f"'{command}' (connector {connector.id})" + ) + + # Create LangChain tools from definitions + for tool_def in tool_definitions: + try: + tool = await _create_mcp_tool_from_definition(tool_def, mcp_client) + tools.append(tool) + except Exception as e: + logger.exception( + f"Failed to create tool '{tool_def.get('name')}' " + f"from connector {connector.id}: {e!s}", + ) except Exception as e: logger.exception( diff --git a/surfsense_backend/app/routes/search_source_connectors_routes.py b/surfsense_backend/app/routes/search_source_connectors_routes.py index a7c577bba..a3d9d10ef 100644 --- a/surfsense_backend/app/routes/search_source_connectors_routes.py +++ b/surfsense_backend/app/routes/search_source_connectors_routes.py @@ -2018,12 +2018,12 @@ async def create_mcp_connector( "You don't have permission to create connectors in this search space", ) - # Create the connector with server config + # Create the connector with server configs array db_connector = SearchSourceConnector( name=connector_data.name, connector_type=SearchSourceConnectorType.MCP_CONNECTOR, is_indexable=False, # MCP connectors are not indexable - config={"server_config": connector_data.server_config.model_dump()}, + config={"server_configs": [sc.model_dump() for sc in connector_data.server_configs]}, periodic_indexing_enabled=False, indexing_frequency_minutes=None, search_space_id=search_space_id, @@ -2035,7 +2035,7 @@ async def create_mcp_connector( await session.refresh(db_connector) logger.info( - f"Created MCP connector {db_connector.id} for server '{connector_data.server_config.command}' " + f"Created MCP connector {db_connector.id} with {len(connector_data.server_configs)} server(s) " f"for user {user.id} in search space {search_space_id}" ) @@ -2202,9 +2202,9 @@ async def update_mcp_connector( if connector_update.name is not None: connector.name = connector_update.name - if connector_update.server_config is not None: + if connector_update.server_configs is not None: connector.config = { - "server_config": connector_update.server_config.model_dump() + "server_configs": [sc.model_dump() for sc in connector_update.server_configs] } connector.updated_at = datetime.now(UTC) diff --git a/surfsense_backend/app/schemas/search_source_connector.py b/surfsense_backend/app/schemas/search_source_connector.py index e27cc775c..93d877261 100644 --- a/surfsense_backend/app/schemas/search_source_connector.py +++ b/surfsense_backend/app/schemas/search_source_connector.py @@ -93,23 +93,23 @@ class MCPConnectorCreate(BaseModel): """Schema for creating an MCP connector.""" name: str - server_config: MCPServerConfig + server_configs: list[MCPServerConfig] # Array of MCP server configurations class MCPConnectorUpdate(BaseModel): """Schema for updating an MCP connector.""" name: str | None = None - server_config: MCPServerConfig | None = None + server_configs: list[MCPServerConfig] | None = None class MCPConnectorRead(BaseModel): - """Schema for reading an MCP connector with server config.""" + """Schema for reading an MCP connector with server configs.""" id: int name: str connector_type: SearchSourceConnectorType - server_config: MCPServerConfig + server_configs: list[MCPServerConfig] search_space_id: int user_id: uuid.UUID created_at: datetime @@ -121,13 +121,14 @@ class MCPConnectorRead(BaseModel): def from_connector(cls, connector: SearchSourceConnectorRead) -> "MCPConnectorRead": """Convert from base SearchSourceConnectorRead.""" config = connector.config or {} - server_config = MCPServerConfig(**config.get("server_config", {})) + server_configs_data = config.get("server_configs", []) + server_configs = [MCPServerConfig(**sc) for sc in server_configs_data] return cls( id=connector.id, name=connector.name, connector_type=connector.connector_type, - server_config=server_config, + server_configs=server_configs, search_space_id=connector.search_space_id, user_id=connector.user_id, created_at=connector.created_at, diff --git a/surfsense_web/components/assistant-ui/connector-popup/connect-forms/components/mcp-connect-form.tsx b/surfsense_web/components/assistant-ui/connector-popup/connect-forms/components/mcp-connect-form.tsx index 3ef43b3db..34a07c6f5 100644 --- a/surfsense_web/components/assistant-ui/connector-popup/connect-forms/components/mcp-connect-form.tsx +++ b/surfsense_web/components/assistant-ui/connector-popup/connect-forms/components/mcp-connect-form.tsx @@ -164,20 +164,19 @@ export const MCPConnectForm: FC = ({ onSubmit, isSubmitting }) isSubmittingRef.current = true; try { - // Submit all servers - for (const config of configs) { - await onSubmit({ - name: config.name, - connector_type: EnumConnectorName.MCP_CONNECTOR, - config: { server_config: config }, - is_indexable: false, - is_active: true, - last_indexed_at: null, - periodic_indexing_enabled: false, - indexing_frequency_minutes: null, - next_scheduled_at: null, - }); - } + // Submit all servers as a single connector with server_configs array + // This creates one connector instead of N connectors (one toast instead of N toasts) + await onSubmit({ + name: configs.length === 1 ? configs[0].name : "MCPs", + connector_type: EnumConnectorName.MCP_CONNECTOR, + config: { server_configs: configs }, + is_indexable: false, + is_active: true, + last_indexed_at: null, + periodic_indexing_enabled: false, + indexing_frequency_minutes: null, + next_scheduled_at: null, + }); } finally { isSubmittingRef.current = false; } diff --git a/surfsense_web/components/assistant-ui/connector-popup/connector-configs/components/mcp-config.tsx b/surfsense_web/components/assistant-ui/connector-popup/connector-configs/components/mcp-config.tsx index 771107c93..d010967d2 100644 --- a/surfsense_web/components/assistant-ui/connector-popup/connector-configs/components/mcp-config.tsx +++ b/surfsense_web/components/assistant-ui/connector-popup/connector-configs/components/mcp-config.tsx @@ -56,12 +56,6 @@ export const MCPConfig: FC = ({ connector, onConfigChange, onNam const serverConfigs = mcpConn.config?.server_configs as MCPServerConfig[] | undefined; if (serverConfigs && Array.isArray(serverConfigs)) { allServerConfigs.push(...serverConfigs); - } else { - // Fallback to single server_config - const serverConfig = mcpConn.config?.server_config as MCPServerConfig | undefined; - if (serverConfig) { - allServerConfigs.push(serverConfig); - } } } diff --git a/surfsense_web/components/assistant-ui/connector-popup/connector-configs/views/connector-connect-view.tsx b/surfsense_web/components/assistant-ui/connector-popup/connector-configs/views/connector-connect-view.tsx index 5d198436d..ff69e3947 100644 --- a/surfsense_web/components/assistant-ui/connector-popup/connector-configs/views/connector-connect-view.tsx +++ b/surfsense_web/components/assistant-ui/connector-popup/connector-configs/views/connector-connect-view.tsx @@ -99,7 +99,7 @@ export const ConnectorConnectView: FC = ({

- Connect {getConnectorTypeDisplay(connectorType)} + Connect {connectorType === "MCP_CONNECTOR" ? "MCP(s)" : getConnectorTypeDisplay(connectorType)}

Enter your connection details @@ -139,7 +139,7 @@ export const ConnectorConnectView: FC = ({ Connecting... ) : ( - <>Connect {getConnectorTypeDisplay(connectorType)} + <>Connect {connectorType === "MCP_CONNECTOR" ? "MCP(s)" : getConnectorTypeDisplay(connectorType)} )}

diff --git a/surfsense_web/components/assistant-ui/connector-popup/tabs/active-connectors-tab.tsx b/surfsense_web/components/assistant-ui/connector-popup/tabs/active-connectors-tab.tsx index d05a4c8dd..1b1c3ea4d 100644 --- a/surfsense_web/components/assistant-ui/connector-popup/tabs/active-connectors-tab.tsx +++ b/surfsense_web/components/assistant-ui/connector-popup/tabs/active-connectors-tab.tsx @@ -130,6 +130,12 @@ export const ActiveConnectorsTab: FC = ({ (c) => !oauthConnectorTypes.has(c.connector_type) && c.connector_type !== "MCP_CONNECTOR" ); + // Calculate total number of MCP servers across all MCP connectors + const totalMCPServers = mcpConnectors.reduce((total, connector) => { + const serverConfigs = connector.config?.server_configs; + return total + (Array.isArray(serverConfigs) ? serverConfigs.length : 0); + }, 0); + // Group OAuth connectors by type const oauthConnectorsByType = oauthConnectors.reduce( (acc, connector) => { @@ -305,7 +311,7 @@ export const ActiveConnectorsTab: FC = ({ Active

- {mcpConnectors.length} {mcpConnectors.length === 1 ? "Server" : "Servers"} + {totalMCPServers} {totalMCPServers === 1 ? "Server" : "Servers"}