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 95960dad2..50339fb93 100644 --- a/surfsense_backend/app/agents/new_chat/tools/mcp_tool.py +++ b/surfsense_backend/app/agents/new_chat/tools/mcp_tool.py @@ -146,19 +146,17 @@ async def load_mcp_tools( tools: list[StructuredTool] = [] for connector in result.scalars(): try: - # Extract server configs array + # Extract single server config config = connector.config or {} - server_configs = config.get("server_configs", []) + server_config = config.get("server_config", {}) - if not server_configs: - logger.warning(f"MCP connector {connector.id} missing server_configs, skipping") + if not server_config: + logger.warning(f"MCP connector {connector.id} missing server_config, skipping") continue - # 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", {}) + command = server_config.get("command") + args = server_config.get("args", []) + env = server_config.get("env", {}) if not command: logger.warning( @@ -166,34 +164,33 @@ async def load_mcp_tools( ) continue - # Create MCP client - mcp_client = MCPClient(command, args, env) + # Create MCP client + mcp_client = MCPClient(command, args, env) - # Connect and discover tools - async with mcp_client.connect(): - tool_definitions = await mcp_client.list_tools() + # 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})" + 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}" ) - - # 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( - f"Failed to load tools from MCP connector {connector.id}: {e!s}", + f"Failed to load tools from MCP connector {connector.id}: {e!s}" ) logger.info(f"Loaded {len(tools)} MCP tools for search space {search_space_id}") diff --git a/surfsense_backend/app/routes/search_source_connectors_routes.py b/surfsense_backend/app/routes/search_source_connectors_routes.py index a3d9d10ef..f9dd69235 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 configs array + # Create the connector with single server config db_connector = SearchSourceConnector( name=connector_data.name, connector_type=SearchSourceConnectorType.MCP_CONNECTOR, is_indexable=False, # MCP connectors are not indexable - config={"server_configs": [sc.model_dump() for sc in connector_data.server_configs]}, + config={"server_config": connector_data.server_config.model_dump()}, 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} with {len(connector_data.server_configs)} server(s) " + f"Created MCP connector {db_connector.id} " 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_configs is not None: + if connector_update.server_config is not None: connector.config = { - "server_configs": [sc.model_dump() for sc in connector_update.server_configs] + "server_config": connector_update.server_config.model_dump() } 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 73ab56e53..b45645053 100644 --- a/surfsense_backend/app/schemas/search_source_connector.py +++ b/surfsense_backend/app/schemas/search_source_connector.py @@ -95,14 +95,14 @@ class MCPConnectorCreate(BaseModel): """Schema for creating an MCP connector.""" name: str - server_configs: list[MCPServerConfig] # Array of MCP server configurations + server_config: MCPServerConfig # Single MCP server configuration class MCPConnectorUpdate(BaseModel): """Schema for updating an MCP connector.""" name: str | None = None - server_configs: list[MCPServerConfig] | None = None + server_config: MCPServerConfig | None = None class MCPConnectorRead(BaseModel): @@ -111,7 +111,7 @@ class MCPConnectorRead(BaseModel): id: int name: str connector_type: SearchSourceConnectorType - server_configs: list[MCPServerConfig] + server_config: MCPServerConfig search_space_id: int user_id: uuid.UUID created_at: datetime @@ -123,14 +123,14 @@ class MCPConnectorRead(BaseModel): def from_connector(cls, connector: SearchSourceConnectorRead) -> "MCPConnectorRead": """Convert from base SearchSourceConnectorRead.""" config = connector.config or {} - server_configs_data = config.get("server_configs", []) - server_configs = [MCPServerConfig(**sc) for sc in server_configs_data] + server_config_data = config.get("server_config", {}) + server_config = MCPServerConfig(**server_config_data) return cls( id=connector.id, name=connector.name, connector_type=connector.connector_type, - server_configs=server_configs, + server_config=server_config, 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.tsx b/surfsense_web/components/assistant-ui/connector-popup.tsx index 01670f3fd..664e13097 100644 --- a/surfsense_web/components/assistant-ui/connector-popup.tsx +++ b/surfsense_web/components/assistant-ui/connector-popup.tsx @@ -24,6 +24,7 @@ import { useConnectorDialog } from "./connector-popup/hooks/use-connector-dialog import { ActiveConnectorsTab } from "./connector-popup/tabs/active-connectors-tab"; import { AllConnectorsTab } from "./connector-popup/tabs/all-connectors-tab"; import { ConnectorAccountsListView } from "./connector-popup/views/connector-accounts-list-view"; +import { MCPConnectorListView } from "./connector-popup/views/mcp-connector-list-view"; import { YouTubeCrawlerView } from "./connector-popup/views/youtube-crawler-view"; export const ConnectorIndicator: FC = () => { @@ -63,6 +64,7 @@ export const ConnectorIndicator: FC = () => { frequencyMinutes, allConnectors, viewingAccountsType, + viewingMCPList, setSearchQuery, setStartDate, setEndDate, @@ -87,6 +89,8 @@ export const ConnectorIndicator: FC = () => { handleBackFromYouTube, handleViewAccountsList, handleBackFromAccountsList, + handleBackFromMCPList, + handleAddNewMCPFromList, handleQuickIndexConnector, connectorConfig, setConnectorConfig, @@ -157,14 +161,7 @@ export const ConnectorIndicator: FC = () => { const hasSources = hasConnectors || activeDocumentTypes.length > 0; const totalSourceCount = connectors.length + activeDocumentTypes.length; - // Count connectors properly: for MCP, count each server; for others, count connectors - const activeConnectorsCount = connectors.reduce((total, c: SearchSourceConnector) => { - if (c.connector_type === "MCP_CONNECTOR") { - const serverConfigs = c.config?.server_configs; - return total + (Array.isArray(serverConfigs) ? serverConfigs.length : 0); - } - return total + 1; - }, 0); + const activeConnectorsCount = connectors.length; // Check which connectors are already connected const connectedTypes = new Set( @@ -208,6 +205,19 @@ export const ConnectorIndicator: FC = () => { {/* YouTube Crawler View - shown when adding YouTube videos */} {isYouTubeView && searchSpaceId ? ( + ) : viewingMCPList ? ( +
+ c.connector_type === "MCP_CONNECTOR" + ) as SearchSourceConnector[] + } + onAddNew={handleAddNewMCPFromList} + onManageConnector={handleStartEdit} + onBack={handleBackFromMCPList} + /> +
) : viewingAccountsType ? ( = ({ onSubmit, isSubmitting }) => { const isSubmittingRef = useRef(false); - const [configJson, setConfigJson] = useState(DEFAULT_CONFIG); + const [configJson, setConfigJson] = useState(""); const [jsonError, setJsonError] = useState(null); const [isTesting, setIsTesting] = useState(false); const [showDetails, setShowDetails] = useState(false); @@ -35,62 +22,50 @@ export const MCPConnectForm: FC = ({ onSubmit, isSubmitting }) status: "success" | "error"; message: string; tools: MCPToolDefinition[]; - errors?: string[]; } | null>(null); - const parseConfigs = (): { configs: MCPServerWithName[] | null; error: string | null } => { + const DEFAULT_CONFIG = JSON.stringify( + { + name: "My MCP Server", + command: "npx", + args: ["-y", "@modelcontextprotocol/server-filesystem", "/path/to/directory"], + env: { + API_KEY: "your_api_key_here", + }, + transport: "stdio", + }, + null, + 2 + ); + + const parseConfig = (): MCPServerConfig | null => { try { const parsed = JSON.parse(configJson); - - // Must be an array - if (!Array.isArray(parsed)) { - return { - configs: null, - error: "Configuration must be an array of MCP server objects", - }; + + // Validate that it's an object, not an array + if (Array.isArray(parsed)) { + setJsonError("Please provide a single server configuration object, not an array"); + return null; } - if (parsed.length === 0) { - return { - configs: null, - error: "Array must contain at least one MCP server configuration", - }; + // Validate required fields + if (!parsed.command || typeof parsed.command !== "string") { + setJsonError("'command' field is required and must be a string"); + return null; } - // Validate each server config - const configs: MCPServerWithName[] = []; - for (let i = 0; i < parsed.length; i++) { - const server = parsed[i]; - - if (!server.name || typeof server.name !== "string") { - return { - configs: null, - error: `Server ${i + 1}: 'name' field is required and must be a string`, - }; - } - - if (!server.command || typeof server.command !== "string") { - return { - configs: null, - error: `Server ${i + 1} (${server.name}): 'command' field is required and must be a string`, - }; - } - - configs.push({ - name: server.name, - command: server.command, - args: Array.isArray(server.args) ? server.args : [], - env: typeof server.env === "object" && server.env !== null ? server.env : {}, - transport: server.transport || "stdio", - }); - } - - return { configs, error: null }; - } catch (error) { - return { - configs: null, - error: error instanceof Error ? error.message : "Invalid JSON", + const config: MCPServerConfig = { + command: parsed.command, + args: parsed.args || [], + env: parsed.env || {}, + transport: parsed.transport || "stdio", }; + + setJsonError(null); + return config; + } catch (error) { + setJsonError(error instanceof Error ? error.message : "Invalid JSON"); + return null; } }; @@ -102,13 +77,11 @@ export const MCPConnectForm: FC = ({ onSubmit, isSubmitting }) }; const handleTestConnection = async () => { - const { configs, error } = parseConfigs(); - - if (!configs || error) { - setJsonError(error); + const serverConfig = parseConfig(); + if (!serverConfig) { setTestResult({ status: "error", - message: error || "Invalid configuration", + message: jsonError || "Invalid configuration", tools: [], }); return; @@ -116,47 +89,32 @@ export const MCPConnectForm: FC = ({ onSubmit, isSubmitting }) setIsTesting(true); setTestResult(null); - setJsonError(null); - const allTools: MCPToolDefinition[] = []; - const errors: string[] = []; - - for (const config of configs) { - try { - const result = await connectorsApiService.testMCPConnection(config); - if (result.status === "success") { - allTools.push(...result.tools); - } else { - errors.push(`${config.name}: ${result.message}`); - } - } catch (error) { - errors.push(`${config.name}: ${error instanceof Error ? error.message : "Failed to connect"}`); + try { + const result = await connectorsApiService.testMCPConnection(serverConfig); + + if (result.status === "success") { + setTestResult({ + status: "success", + message: `Successfully connected. Found ${result.tools.length} tool${result.tools.length !== 1 ? 's' : ''}.`, + tools: result.tools, + }); + } else { + setTestResult({ + status: "error", + message: result.message || "Failed to connect", + tools: [], + }); } - } - - if (errors.length === 0) { - setTestResult({ - status: "success", - message: `Successfully connected to ${configs.length} server${configs.length !== 1 ? 's' : ''}. Found ${allTools.length} tool${allTools.length !== 1 ? 's' : ''}.`, - tools: allTools, - }); - } else if (allTools.length > 0) { - setTestResult({ - status: "success", - message: `Partially successful. Connected ${allTools.length} tool${allTools.length !== 1 ? 's' : ''}.`, - tools: allTools, - errors, - }); - } else { + } catch (error) { setTestResult({ status: "error", - message: "Failed to connect to all servers", + message: error instanceof Error ? error.message : "Failed to connect", tools: [], - errors, }); + } finally { + setIsTesting(false); } - - setIsTesting(false); }; const handleSubmit = async (e: React.FormEvent) => { @@ -167,22 +125,28 @@ export const MCPConnectForm: FC = ({ onSubmit, isSubmitting }) return; } - const { configs, error } = parseConfigs(); - - if (!configs || error) { - setJsonError(error); - alert(error || "Invalid JSON configuration"); + const serverConfig = parseConfig(); + if (!serverConfig) { return; } + // Extract server name from config if provided + let serverName = "MCP Server"; + try { + const parsed = JSON.parse(configJson); + if (parsed.name && typeof parsed.name === "string") { + serverName = parsed.name; + } + } catch { + // Use default name + } + isSubmittingRef.current = true; try { - // 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", + name: serverName, connector_type: EnumConnectorName.MCP_CONNECTOR, - config: { server_configs: configs }, + config: { server_config: serverConfig }, is_indexable: false, is_active: true, last_indexed_at: null, @@ -200,9 +164,9 @@ export const MCPConnectForm: FC = ({ onSubmit, isSubmitting })
- MCP Servers + MCP Server - Connect to one or more MCP (Model Context Protocol) servers. Paste a JSON array of server configurations below. + Connect to an MCP (Model Context Protocol) server. Each MCP server is added as a separate connector.
@@ -210,7 +174,7 @@ export const MCPConnectForm: FC = ({ onSubmit, isSubmitting })
- +