diff --git a/surfsense_web/components/assistant-ui/connector-popup.tsx b/surfsense_web/components/assistant-ui/connector-popup.tsx index b946c153f..5ad520b05 100644 --- a/surfsense_web/components/assistant-ui/connector-popup.tsx +++ b/surfsense_web/components/assistant-ui/connector-popup.tsx @@ -22,7 +22,6 @@ import { useIndexingConnectors } from "./connector-popup/hooks/use-indexing-conn 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 = () => { @@ -178,18 +177,16 @@ export const ConnectorIndicator: FC = () => { {isYouTubeView && searchSpaceId ? ( ) : viewingMCPList ? ( -
- c.connector_type === "MCP_CONNECTOR" - ) as SearchSourceConnector[] - } - onAddNew={handleAddNewMCPFromList} - onManageConnector={handleStartEdit} - onBack={handleBackFromMCPList} - /> -
+ ) : viewingAccountsType ? ( void; onManage?: () => void; @@ -46,10 +48,12 @@ export const ConnectorCard: FC = ({ isConnecting = false, documentCount, accountCount, + connectorCount, isIndexing = false, onConnect, onManage, }) => { + const isMCP = connectorType === EnumConnectorName.MCP_CONNECTOR; // Get connector status const { getConnectorStatus, isConnectorEnabled, getConnectorStatusMessage, shouldShowWarnings } = useConnectorStatus(); @@ -112,13 +116,21 @@ export const ConnectorCard: FC = ({

) : isConnected ? (

- {formatDocumentCount(documentCount)} - {accountCount !== undefined && accountCount > 0 && ( + {isMCP && connectorCount !== undefined ? ( + + {connectorCount} {connectorCount === 1 ? "server" : "servers"} + + ) : ( <> - - - {accountCount} {accountCount === 1 ? "Account" : "Accounts"} - + {formatDocumentCount(documentCount)} + {accountCount !== undefined && accountCount > 0 && ( + <> + + + {accountCount} {accountCount === 1 ? "Account" : "Accounts"} + + + )} )}

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 b0f6c8078..c1a1af5a1 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 @@ -4,11 +4,9 @@ import { CheckCircle2, ChevronDown, ChevronUp, Server, XCircle } from "lucide-re import { type FC, useRef, useState } from "react"; import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; import { Button } from "@/components/ui/button"; -import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Textarea } from "@/components/ui/textarea"; import { EnumConnectorName } from "@/contracts/enums/connector"; -import type { MCPToolDefinition } from "@/contracts/types/mcp.types"; import type { ConnectFormProps } from ".."; import { extractServerName, @@ -46,7 +44,7 @@ export const MCPConnectForm: FC = ({ onSubmit, isSubmitting }) name: "My Remote MCP Server", url: "https://your-mcp-server.com/mcp", headers: { - "API_KEY": "your_api_key_here", + API_KEY: "your_api_key_here", }, transport: "streamable-http", }, @@ -178,29 +176,47 @@ export const MCPConnectForm: FC = ({ onSubmit, isSubmitting }) id="config" value={configJson} onChange={(e) => handleConfigChange(e.target.value)} + onKeyDown={(e) => { + if (e.key === "Tab") { + e.preventDefault(); + const target = e.target as HTMLTextAreaElement; + const start = target.selectionStart; + const end = target.selectionEnd; + const indent = " "; // 2 spaces for JSON + const newValue = + configJson.substring(0, start) + indent + configJson.substring(end); + handleConfigChange(newValue); + // Set cursor position after the inserted tab + requestAnimationFrame(() => { + target.selectionStart = target.selectionEnd = start + indent.length; + }); + } + }} placeholder={DEFAULT_CONFIG} rows={16} className={`font-mono text-xs ${jsonError ? "border-red-500" : ""}`} /> {jsonError &&

JSON Error: {jsonError}

}

- Local (stdio): command, args, env, transport: "stdio"
- Remote (HTTP): url, headers, transport: "streamable-http" + Paste a single MCP server configuration. Must include: name, command, args (optional), + env (optional), transport (optional).

+ {/* Test Connection */}
+ {/* Test Result */} {testResult && ( = ({ onSubmit, isSubmitting }) type="button" variant="ghost" size="sm" - className="h-6 px-2" + className="h-6 px-2 self-start sm:self-auto text-xs" onClick={(e) => { e.preventDefault(); e.stopPropagation(); @@ -236,18 +252,20 @@ export const MCPConnectForm: FC = ({ onSubmit, isSubmitting }) {showDetails ? ( <> - Hide Details + Hide Details + Hide ) : ( <> - Show Details + Show Details + Show )} )} - + {testResult.message} {showDetails && testResult.tools.length > 0 && (
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 54ba3ed3a..1de6300f0 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 @@ -47,10 +47,10 @@ export const MCPConfig: FC = ({ connector, onConfigChange, onNam const serverConfig = connector.config?.server_config as MCPServerConfig | undefined; if (serverConfig) { const transport = serverConfig.transport || "stdio"; - + // Build config object based on transport type let configObj: Record; - + if (transport === "streamable-http" || transport === "http" || transport === "sse") { // HTTP transport - use url and headers configObj = { @@ -67,7 +67,7 @@ export const MCPConfig: FC = ({ connector, onConfigChange, onNam transport: transport, }; } - + setConfigJson(JSON.stringify(configObj, null, 2)); } }, [isValidConnector, connector.name, connector.config?.server_config]); @@ -148,15 +148,23 @@ export const MCPConfig: FC = ({ connector, onConfigChange, onNam return (
{/* Server Name */} -
- - handleNameChange(e.target.value)} - placeholder="e.g., Filesystem Server" - required - /> +
+
+ + handleNameChange(e.target.value)} + placeholder="e.g., Filesystem Server" + className="border-slate-400/20 focus-visible:border-slate-400/40" + required + /> +

+ A friendly name to identify this connector. +

+
{/* Server Configuration */} @@ -173,12 +181,29 @@ export const MCPConfig: FC = ({ connector, onConfigChange, onNam id="config" value={configJson} onChange={(e) => handleConfigChange(e.target.value)} + onKeyDown={(e) => { + if (e.key === "Tab") { + e.preventDefault(); + const target = e.target as HTMLTextAreaElement; + const start = target.selectionStart; + const end = target.selectionEnd; + const indent = " "; // 2 spaces for JSON + const newValue = + configJson.substring(0, start) + indent + configJson.substring(end); + handleConfigChange(newValue); + // Set cursor position after the inserted tab + requestAnimationFrame(() => { + target.selectionStart = target.selectionEnd = start + indent.length; + }); + } + }} rows={16} className={`font-mono text-xs ${jsonError ? "border-red-500" : ""}`} /> {jsonError &&

JSON Error: {jsonError}

}

- Local (stdio): command, args, env, transport: "stdio"
+ Local (stdio): command, args, env, transport: "stdio" +
Remote (HTTP): url, headers, transport: "streamable-http"

@@ -189,10 +214,10 @@ export const MCPConfig: FC = ({ connector, onConfigChange, onNam type="button" onClick={handleTestConnection} disabled={isTesting} - variant="outline" - className="w-full" + variant="secondary" + className="w-full h-8 text-[13px] px-3 rounded-lg font-medium bg-white text-slate-700 hover:bg-slate-50 border-0 shadow-xs dark:bg-secondary dark:text-secondary-foreground dark:hover:bg-secondary/80" > - {isTesting ? "Testing Connection..." : "Test Connection"} + {isTesting ? "Testing Connection" : "Test Connection"}
@@ -211,7 +236,7 @@ export const MCPConfig: FC = ({ connector, onConfigChange, onNam )}
-
+
{testResult.status === "success" ? "Connection Successful" @@ -222,7 +247,7 @@ export const MCPConfig: FC = ({ connector, onConfigChange, onNam type="button" variant="ghost" size="sm" - className="h-6 px-2" + className="h-6 px-2 self-start sm:self-auto text-xs" onClick={(e) => { e.preventDefault(); e.stopPropagation(); @@ -232,12 +257,14 @@ export const MCPConfig: FC = ({ connector, onConfigChange, onNam {showDetails ? ( <> - Hide Details + Hide Details + Hide ) : ( <> - Show Details + Show Details + Show )} diff --git a/surfsense_web/components/assistant-ui/connector-popup/connector-configs/views/connector-edit-view.tsx b/surfsense_web/components/assistant-ui/connector-popup/connector-configs/views/connector-edit-view.tsx index 89c36ffc2..5433acbf7 100644 --- a/surfsense_web/components/assistant-ui/connector-popup/connector-configs/views/connector-edit-view.tsx +++ b/surfsense_web/components/assistant-ui/connector-popup/connector-configs/views/connector-edit-view.tsx @@ -151,7 +151,7 @@ export const ConnectorEditView: FC = ({

- {connector.connector_type === "MCP_CONNECTOR" ? "MCP Server" : connector.name} + {connector.name}

Manage your connector settings and sync configuration diff --git a/surfsense_web/components/assistant-ui/connector-popup/hooks/use-connector-dialog.ts b/surfsense_web/components/assistant-ui/connector-popup/hooks/use-connector-dialog.ts index 7a2243705..4f56f588d 100644 --- a/surfsense_web/components/assistant-ui/connector-popup/hooks/use-connector-dialog.ts +++ b/surfsense_web/components/assistant-ui/connector-popup/hooks/use-connector-dialog.ts @@ -646,7 +646,7 @@ export const useConnectorDialog = () => { const successMessage = currentConnectorType === "MCP_CONNECTOR" - ? `${connector.name} MCP server added successfully` + ? `${connector.name} added successfully` : `${connectorTitle} connected and indexing started!`; toast.success(successMessage, { description: periodicEnabledForIndexing @@ -711,7 +711,7 @@ export const useConnectorDialog = () => { // Other non-indexable connectors - just show success message and close const successMessage = currentConnectorType === "MCP_CONNECTOR" - ? `${connector.name} MCP server added successfully` + ? `${connector.name} added successfully` : `${connectorTitle} connected successfully!`; toast.success(successMessage); diff --git a/surfsense_web/components/assistant-ui/connector-popup/tabs/all-connectors-tab.tsx b/surfsense_web/components/assistant-ui/connector-popup/tabs/all-connectors-tab.tsx index 6152504fc..2487b7276 100644 --- a/surfsense_web/components/assistant-ui/connector-popup/tabs/all-connectors-tab.tsx +++ b/surfsense_web/components/assistant-ui/connector-popup/tabs/all-connectors-tab.tsx @@ -1,6 +1,7 @@ "use client"; import type { FC } from "react"; +import { EnumConnectorName } from "@/contracts/enums/connector"; import type { SearchSourceConnector } from "@/contracts/types/connector.types"; import { ConnectorCard } from "../components/connector-card"; import { CRAWLERS, OAUTH_CONNECTORS, OTHER_CONNECTORS } from "../constants/connector-constants"; @@ -161,6 +162,16 @@ export const AllConnectorsTab: FC = ({ ); const isIndexing = actualConnector && indexingConnectorIds?.has(actualConnector.id); + // For MCP connectors, count total MCP connectors instead of document count + const isMCP = connector.connectorType === EnumConnectorName.MCP_CONNECTOR; + const mcpConnectorCount = + isMCP && allConnectors + ? allConnectors.filter( + (c: SearchSourceConnector) => + c.connector_type === EnumConnectorName.MCP_CONNECTOR + ).length + : undefined; + const handleConnect = onConnectNonOAuth ? () => onConnectNonOAuth(connector.connectorType) : () => {}; // Fallback - connector popup should handle all connector types @@ -175,6 +186,7 @@ export const AllConnectorsTab: FC = ({ isConnected={isConnected} isConnecting={isConnecting} documentCount={documentCount} + connectorCount={mcpConnectorCount} isIndexing={isIndexing} onConnect={handleConnect} onManage={ diff --git a/surfsense_web/components/assistant-ui/connector-popup/utils/mcp-config-validator.ts b/surfsense_web/components/assistant-ui/connector-popup/utils/mcp-config-validator.ts index e03d76445..650a95e3d 100644 --- a/surfsense_web/components/assistant-ui/connector-popup/utils/mcp-config-validator.ts +++ b/surfsense_web/components/assistant-ui/connector-popup/utils/mcp-config-validator.ts @@ -138,35 +138,37 @@ export const parseMCPConfig = (configJson: string): MCPConfigValidationResult => // Replace technical error messages with user-friendly ones if (errorMsg.includes("expected string, received undefined")) { - errorMsg = "This field is required"; + errorMsg = fieldPath ? `The '${fieldPath}' field is required` : "This field is required"; } else if (errorMsg.includes("Invalid input")) { - errorMsg = "Invalid value"; + errorMsg = fieldPath ? `The '${fieldPath}' field has an invalid value` : "Invalid value"; + } else if (fieldPath && !errorMsg.toLowerCase().includes(fieldPath.toLowerCase())) { + // If error message doesn't mention the field name, prepend it + errorMsg = `The '${fieldPath}' field: ${errorMsg}`; } - const formattedError = fieldPath ? `${fieldPath}: ${errorMsg}` : errorMsg; - - console.error("[MCP Validator] ❌ Validation error:", formattedError); + console.error("[MCP Validator] ❌ Validation error:", errorMsg); console.error("[MCP Validator] Full Zod errors:", result.error.issues); return { config: null, - error: formattedError, + error: errorMsg, }; } // Build config based on transport type - const config: MCPServerConfig = result.data.transport === "stdio" || !result.data.transport - ? { - command: (result.data as z.infer).command, - args: (result.data as z.infer).args, - env: (result.data as z.infer).env, - transport: "stdio" as const, - } - : { - url: (result.data as z.infer).url, - headers: (result.data as z.infer).headers, - transport: result.data.transport as "streamable-http" | "http" | "sse", - }; + const config: MCPServerConfig = + result.data.transport === "stdio" || !result.data.transport + ? { + command: (result.data as z.infer).command, + args: (result.data as z.infer).args, + env: (result.data as z.infer).env, + transport: "stdio" as const, + } + : { + url: (result.data as z.infer).url, + headers: (result.data as z.infer).headers, + transport: result.data.transport as "streamable-http" | "http" | "sse", + }; // Cache the successfully parsed config configCache.set(configJson, { diff --git a/surfsense_web/components/assistant-ui/connector-popup/views/connector-accounts-list-view.tsx b/surfsense_web/components/assistant-ui/connector-popup/views/connector-accounts-list-view.tsx index 5f8c1f3ed..a48ca02e6 100644 --- a/surfsense_web/components/assistant-ui/connector-popup/views/connector-accounts-list-view.tsx +++ b/surfsense_web/components/assistant-ui/connector-popup/views/connector-accounts-list-view.tsx @@ -1,9 +1,10 @@ "use client"; import { differenceInDays, differenceInMinutes, format, isToday, isYesterday } from "date-fns"; -import { ArrowLeft, Loader2, Plus } from "lucide-react"; +import { ArrowLeft, Loader2, Plus, Server } from "lucide-react"; import type { FC } from "react"; import { Button } from "@/components/ui/button"; +import { EnumConnectorName } from "@/contracts/enums/connector"; import { getConnectorIcon } from "@/contracts/enums/connectorIcons"; import type { SearchSourceConnector } from "@/contracts/types/connector.types"; import { cn } from "@/lib/utils"; @@ -19,6 +20,7 @@ interface ConnectorAccountsListViewProps { onManage: (connector: SearchSourceConnector) => void; onAddAccount: () => void; isConnecting?: boolean; + addButtonText?: string; } /** @@ -70,6 +72,7 @@ export const ConnectorAccountsListView: FC = ({ onManage, onAddAccount, isConnecting = false, + addButtonText, }) => { // Get connector status const { isConnectorEnabled, getConnectorStatusMessage } = useConnectorStatus(); @@ -80,6 +83,22 @@ export const ConnectorAccountsListView: FC = ({ // Filter connectors to only show those of this type const typeConnectors = connectors.filter((c) => c.connector_type === connectorType); + // Determine button text - default to "Add Account" unless specified + const buttonText = + addButtonText || + (connectorType === EnumConnectorName.MCP_CONNECTOR ? "Add New MCP Server" : "Add Account"); + const isMCP = connectorType === EnumConnectorName.MCP_CONNECTOR; + + // Helper to get display name for connector (handles MCP server name extraction) + const getDisplayName = (connector: SearchSourceConnector): string => { + if (isMCP) { + // For MCP, extract server name from config if available + const serverName = connector.config?.server_config?.name || connector.name; + return serverName; + } + return getConnectorDisplayName(connector.name); + }; + return (

{/* Header */} @@ -115,22 +134,22 @@ export const ConnectorAccountsListView: FC = ({ onClick={onAddAccount} disabled={isConnecting || !isEnabled} className={cn( - "flex items-center gap-1.5 sm:gap-2 px-2 sm:px-3 py-1.5 sm:py-2 rounded-lg border-2 border-dashed text-left transition-all duration-200 shrink-0 self-center sm:self-auto sm:w-auto", + "flex items-center justify-center gap-1.5 h-8 px-3 rounded-md border-2 border-dashed text-xs sm:text-sm transition-all duration-200 shrink-0 w-full sm:w-auto", !isEnabled ? "border-border/30 opacity-50 cursor-not-allowed" - : "border-primary/50 hover:bg-primary/5", + : "border-slate-400/20 dark:border-white/20 hover:bg-primary/5", isConnecting && "opacity-50 cursor-not-allowed" )} > -
+
{isConnecting ? ( - + ) : ( - + )}
- - {isConnecting ? "Connecting" : "Add Account"} + + {isConnecting ? "Connecting" : buttonText}
@@ -139,61 +158,81 @@ export const ConnectorAccountsListView: FC = ({ {/* Content */}
{/* Connected Accounts Grid */} -
- {typeConnectors.map((connector) => { - const isIndexing = indexingConnectorIds.has(connector.id); + {typeConnectors.length === 0 ? ( +
+
+ {isMCP ? ( + + ) : ( + getConnectorIcon(connectorType, "size-8") + )} +
+

+ {isMCP ? "No MCP Servers" : `No ${connectorTitle} Accounts`} +

+

+ {isMCP + ? "Get started by adding your first Model Context Protocol server" + : `Get started by connecting your first ${connectorTitle} account`} +

+
+ ) : ( +
+ {typeConnectors.map((connector) => { + const isIndexing = indexingConnectorIds.has(connector.id); - return ( -
+ return (
- {getConnectorIcon(connector.connector_type, "size-6")} -
-
-

- {getConnectorDisplayName(connector.name)} -

- {isIndexing ? ( -

- - Syncing +

+ {getConnectorIcon(connector.connector_type, "size-6")} +
+
+

+ {getDisplayName(connector)}

- ) : ( -

- {isIndexableConnector(connector.connector_type) - ? connector.last_indexed_at - ? `Last indexed: ${formatLastIndexedDate(connector.last_indexed_at)}` - : "Never indexed" - : "Active"} -

- )} + {isIndexing ? ( +

+ + Syncing +

+ ) : ( +

+ {isIndexableConnector(connector.connector_type) + ? connector.last_indexed_at + ? `Last indexed: ${formatLastIndexedDate(connector.last_indexed_at)}` + : "Never indexed" + : "Active"} +

+ )} +
+
- -
- ); - })} -
+ ); + })} +
+ )}
); diff --git a/surfsense_web/components/assistant-ui/connector-popup/views/mcp-connector-list-view.tsx b/surfsense_web/components/assistant-ui/connector-popup/views/mcp-connector-list-view.tsx deleted file mode 100644 index 78a0b0b0c..000000000 --- a/surfsense_web/components/assistant-ui/connector-popup/views/mcp-connector-list-view.tsx +++ /dev/null @@ -1,134 +0,0 @@ -"use client"; - -import { Plus, Server, XCircle } from "lucide-react"; -import type { FC } from "react"; -import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; -import { Button } from "@/components/ui/button"; -import { EnumConnectorName } from "@/contracts/enums/connector"; -import { getConnectorIcon } from "@/contracts/enums/connectorIcons"; -import type { SearchSourceConnector } from "@/contracts/types/connector.types"; -import { cn } from "@/lib/utils"; - -interface MCPConnectorListViewProps { - mcpConnectors: SearchSourceConnector[]; - onAddNew: () => void; - onManageConnector: (connector: SearchSourceConnector) => void; - onBack: () => void; -} - -export const MCPConnectorListView: FC = ({ - mcpConnectors, - onAddNew, - onManageConnector, - onBack, -}) => { - // Validate that all connectors are MCP connectors - const invalidConnectors = mcpConnectors.filter( - (c) => c.connector_type !== EnumConnectorName.MCP_CONNECTOR - ); - - if (invalidConnectors.length > 0) { - console.error( - "MCPConnectorListView received non-MCP connectors:", - invalidConnectors.map((c) => c.connector_type) - ); - return ( - - - Invalid Connector Type - - This view can only display MCP connectors. Found {invalidConnectors.length} invalid - connector(s). - - - ); - } - return ( -
- {/* Header */} -
-
- -
-

MCP Connectors

-

- Manage your Model Context Protocol servers -

-
-
-
- - {/* Add New Button */} -
- -
- - {/* MCP Connectors List */} -
- {mcpConnectors.length === 0 ? ( -
-
- -
-

No MCP Servers

-

- Get started by adding your first Model Context Protocol server -

-
- ) : ( - mcpConnectors.map((connector) => { - // Extract server name from config - const serverName = connector.config?.server_config?.name || connector.name; - - return ( -
-
- {getConnectorIcon("MCP_CONNECTOR", "size-6")} -
-
-

{serverName}

-
- -
- ); - }) - )} -
-
- ); -}; diff --git a/surfsense_web/contracts/enums/connectorIcons.tsx b/surfsense_web/contracts/enums/connectorIcons.tsx index 5f147b63b..9350b6a1e 100644 --- a/surfsense_web/contracts/enums/connectorIcons.tsx +++ b/surfsense_web/contracts/enums/connectorIcons.tsx @@ -65,7 +65,7 @@ export const getConnectorIcon = (connectorType: EnumConnectorName | string, clas case EnumConnectorName.CIRCLEBACK_CONNECTOR: return ; case EnumConnectorName.MCP_CONNECTOR: - return ; + return MCP; // Additional cases for non-enum connector types case "YOUTUBE_CONNECTOR": return YouTube; diff --git a/surfsense_web/public/connectors/modelcontextprotocol.svg b/surfsense_web/public/connectors/modelcontextprotocol.svg new file mode 100644 index 000000000..e9c3fa46e --- /dev/null +++ b/surfsense_web/public/connectors/modelcontextprotocol.svg @@ -0,0 +1 @@ + \ No newline at end of file