diff --git a/surfsense_web/components/assistant-ui/connector-popup.tsx b/surfsense_web/components/assistant-ui/connector-popup.tsx index 1e6dd09ae..28fe5b5b0 100644 --- a/surfsense_web/components/assistant-ui/connector-popup.tsx +++ b/surfsense_web/components/assistant-ui/connector-popup.tsx @@ -68,6 +68,7 @@ export const ConnectorIndicator: FC = () => { setEndDate, setPeriodicEnabled, setFrequencyMinutes, + setOtherMCPConnectorIds, handleOpenChange, handleTabChange, handleScroll, @@ -239,6 +240,8 @@ export const ConnectorIndicator: FC = () => { isSaving={isSaving} isDisconnecting={isDisconnecting} isIndexing={indexingConnectorIds.has(editingConnector.id)} + searchSpaceId={searchSpaceId?.toString()} + onOtherMCPConnectorsLoaded={setOtherMCPConnectorIds} onStartDateChange={setStartDate} onEndDateChange={setEndDate} onPeriodicEnabledChange={setPeriodicEnabled} diff --git a/surfsense_web/components/assistant-ui/connector-popup/components/connector-card.tsx b/surfsense_web/components/assistant-ui/connector-popup/components/connector-card.tsx index fa4b8feb6..bb35ee8a3 100644 --- a/surfsense_web/components/assistant-ui/connector-popup/components/connector-card.tsx +++ b/surfsense_web/components/assistant-ui/connector-popup/components/connector-card.tsx @@ -27,6 +27,16 @@ interface ConnectorCardProps { onManage?: () => void; } +/** + * Check if a connector type is indexable (has documents) + * MCP connectors are tools only and don't have indexable content + */ +function isIndexableConnector(connectorType?: string): boolean { + if (!connectorType) return true; // Default to true for unknown types + const nonIndexableTypes = ["MCP_CONNECTOR"]; + return !nonIndexableTypes.includes(connectorType); +} + /** * Extract a number from the active task message for display * Looks for patterns like "45 indexed", "Processing 123", etc. @@ -135,7 +145,12 @@ export const ConnectorCard: FC = ({ } if (isConnected) { - // Show last indexed date for connected connectors + // For non-indexable connectors (like MCP), show description instead of index status + if (!isIndexableConnector(connectorType)) { + return description; + } + + // Show last indexed date for connected indexable connectors if (lastIndexedAt) { return ( 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 new file mode 100644 index 000000000..3ef43b3db --- /dev/null +++ b/surfsense_web/components/assistant-ui/connector-popup/connect-forms/components/mcp-connect-form.tsx @@ -0,0 +1,276 @@ +"use client"; + +import { CheckCircle2, Server, XCircle } from "lucide-react"; +import { type FC, useRef, useState } from "react"; +import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; +import { Button } from "@/components/ui/button"; +import { Label } from "@/components/ui/label"; +import { Textarea } from "@/components/ui/textarea"; +import { EnumConnectorName } from "@/contracts/enums/connector"; +import type { MCPServerConfig, MCPToolDefinition } from "@/contracts/types/mcp.types"; +import { connectorsApiService } from "@/lib/apis/connectors-api.service"; +import type { ConnectFormProps } from ".."; + +const DEFAULT_CONFIG = `[ + { + "name": "MCP Server 1", + "command": "npx", + "args": ["-y", "@modelcontextprotocol/server-filesystem", "/path/to/allowed/directory"], + "env": {}, + "transport": "stdio" + } +]`; + +interface MCPServerWithName extends MCPServerConfig { + name: string; +} + +export const MCPConnectForm: FC = ({ onSubmit, isSubmitting }) => { + const isSubmittingRef = useRef(false); + const [configJson, setConfigJson] = useState(DEFAULT_CONFIG); + const [jsonError, setJsonError] = useState(null); + const [isTesting, setIsTesting] = useState(false); + const [testResults, setTestResults] = useState | null>(null); + + const parseConfigs = (): { configs: MCPServerWithName[] | null; error: string | 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", + }; + } + + if (parsed.length === 0) { + return { + configs: null, + error: "Array must contain at least one MCP server configuration", + }; + } + + // 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 handleConfigChange = (value: string) => { + setConfigJson(value); + if (jsonError) { + setJsonError(null); + } + }; + + const handleTestConnection = async () => { + const { configs, error } = parseConfigs(); + + if (!configs || error) { + setJsonError(error); + setTestResults([{ + name: "Parse Error", + status: "error", + message: error || "Invalid configuration", + tools: [], + }]); + return; + } + + setIsTesting(true); + setTestResults(null); + setJsonError(null); + + const results: Array<{ + name: string; + status: "success" | "error"; + message: string; + tools: MCPToolDefinition[]; + }> = []; + + for (const config of configs) { + try { + const result = await connectorsApiService.testMCPConnection(config); + results.push({ + name: config.name, + ...result, + }); + } catch (error) { + results.push({ + name: config.name, + status: "error", + message: error instanceof Error ? error.message : "Failed to connect to MCP server", + tools: [], + }); + } + } + + setTestResults(results); + setIsTesting(false); + }; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + + // Prevent multiple submissions + if (isSubmittingRef.current || isSubmitting) { + return; + } + + const { configs, error } = parseConfigs(); + + if (!configs || error) { + setJsonError(error); + alert(error || "Invalid JSON configuration"); + return; + } + + 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, + }); + } + } finally { + isSubmittingRef.current = false; + } + }; + + return ( +
+ + +
+ MCP Servers + + Connect to one or more MCP (Model Context Protocol) servers. Paste a JSON array of server configurations below. + +
+
+ +
+
+
+ +