diff --git a/surfsense_web/components/assistant-ui/connector-popup.tsx b/surfsense_web/components/assistant-ui/connector-popup.tsx index a366bd37f..5ad520b05 100644 --- a/surfsense_web/components/assistant-ui/connector-popup.tsx +++ b/surfsense_web/components/assistant-ui/connector-popup.tsx @@ -101,7 +101,8 @@ export const ConnectorIndicator: FC = () => { // Fallback to API if Electric is not available or fails // Use Electric data if: 1) we have data, or 2) still loading without error // Use API data if: Electric failed (has error) or finished loading with no data - const useElectricData = connectorsFromElectric.length > 0 || (connectorsLoading && !connectorsError); + const useElectricData = + connectorsFromElectric.length > 0 || (connectorsLoading && !connectorsError); const connectors = useElectricData ? connectorsFromElectric : allConnectors || []; // Manual refresh function that works with both Electric and API @@ -129,7 +130,7 @@ export const ConnectorIndicator: FC = () => { const hasConnectors = connectors.length > 0; const hasSources = hasConnectors || activeDocumentTypes.length > 0; const totalSourceCount = connectors.length + activeDocumentTypes.length; - + const activeConnectorsCount = connectors.length; // Check which connectors are already connected @@ -226,7 +227,6 @@ export const ConnectorIndicator: FC = () => { isDisconnecting={isDisconnecting} isIndexing={indexingConnectorIds.has(editingConnector.id)} searchSpaceId={searchSpaceId?.toString()} - onStartDateChange={setStartDate} onEndDateChange={setEndDate} onPeriodicEnabledChange={setPeriodicEnabled} 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 d67b9b49c..92b87f124 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 @@ -49,12 +49,12 @@ export const MCPConnectForm: FC = ({ onSubmit, isSubmitting }) const handleConfigChange = (value: string) => { setConfigJson(value); - + // Clear previous error if (jsonError) { setJsonError(null); } - + // Validate immediately to show errors as user types (with debouncing via parseMCPConfig cache) if (value.trim()) { const result = parseMCPConfig(value); @@ -120,13 +120,14 @@ export const MCPConnectForm: FC = ({ onSubmit, isSubmitting }) return (
-
- - - Connect to an MCP (Model Context Protocol) server. Each MCP server is added as a separate connector. - -
-
+
+ + + Connect to an MCP (Model Context Protocol) server. Each MCP server is added as a + separate connector. + +
+
@@ -140,11 +141,10 @@ export const MCPConnectForm: FC = ({ onSubmit, isSubmitting }) rows={16} className={`font-mono text-xs ${jsonError ? "border-red-500" : ""}`} /> - {jsonError && ( -

{jsonError}

- )} + {jsonError &&

{jsonError}

}

- Paste a single MCP server configuration. Must include: name, command, args (optional), env (optional), transport (optional). + Paste a single MCP server configuration. Must include: name, command, args (optional), + env (optional), transport (optional).

@@ -178,7 +178,9 @@ export const MCPConnectForm: FC = ({ onSubmit, isSubmitting })
- {testResult.status === "success" ? "Connection Successful" : "Connection Failed"} + {testResult.status === "success" + ? "Connection Successful" + : "Connection Failed"} {testResult.tools.length > 0 && (
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 80d364f27..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.name} + {connector.name}

Manage your connector settings and sync configuration @@ -200,7 +200,6 @@ export const ConnectorEditView: FC = ({ onConfigChange={onConfigChange} onNameChange={onNameChange} searchSpaceId={searchSpaceId} - /> )} 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 b7fb9880a..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 @@ -540,18 +540,18 @@ export const useConnectorDialog = () => { data: { ...connectorData, connector_type: connectorData.connector_type as EnumConnectorName, - is_active: true, - next_scheduled_at: connectorData.next_scheduled_at as string | null, - }, - queryParams: { - search_space_id: searchSpaceId, - }, - }); + is_active: true, + next_scheduled_at: connectorData.next_scheduled_at as string | null, + }, + queryParams: { + search_space_id: searchSpaceId, + }, + }); - // Refetch connectors to get the new one - const result = await refetchAllConnectors(); - if (result.data) { - const connector = result.data.find( + // Refetch connectors to get the new one + const result = await refetchAllConnectors(); + if (result.data) { + const connector = result.data.find( (c: SearchSourceConnector) => c.id === newConnector.id ); if (connector) { @@ -644,34 +644,35 @@ export const useConnectorDialog = () => { }, }); - const successMessage = currentConnectorType === "MCP_CONNECTOR" - ? `${connector.name} added successfully` - : `${connectorTitle} connected and indexing started!`; - toast.success(successMessage, { - description: periodicEnabledForIndexing - ? `Periodic sync enabled every ${getFrequencyLabel(frequencyMinutesForIndexing)}.` - : "You can continue working while we sync your data.", - }); + const successMessage = + currentConnectorType === "MCP_CONNECTOR" + ? `${connector.name} added successfully` + : `${connectorTitle} connected and indexing started!`; + toast.success(successMessage, { + description: periodicEnabledForIndexing + ? `Periodic sync enabled every ${getFrequencyLabel(frequencyMinutesForIndexing)}.` + : "You can continue working while we sync your data.", + }); - const url = new URL(window.location.href); - url.searchParams.delete("modal"); - url.searchParams.delete("tab"); - url.searchParams.delete("view"); - url.searchParams.delete("connectorType"); - router.replace(url.pathname + url.search, { scroll: false }); + const url = new URL(window.location.href); + url.searchParams.delete("modal"); + url.searchParams.delete("tab"); + url.searchParams.delete("view"); + url.searchParams.delete("connectorType"); + router.replace(url.pathname + url.search, { scroll: false }); - // Clear indexing config state since we're not showing the view - setIndexingConfig(null); - setIndexingConnector(null); - setIndexingConnectorConfig(null); + // Clear indexing config state since we're not showing the view + setIndexingConfig(null); + setIndexingConnector(null); + setIndexingConnectorConfig(null); - // Invalidate queries to refresh data - queryClient.invalidateQueries({ - queryKey: cacheKeys.logs.summary(Number(searchSpaceId)), - }); + // Invalidate queries to refresh data + queryClient.invalidateQueries({ + queryKey: cacheKeys.logs.summary(Number(searchSpaceId)), + }); - // Refresh connectors list - await refetchAllConnectors(); + // Refresh connectors list + await refetchAllConnectors(); } else { // Non-indexable connector // For Circleback, transition to edit view to show webhook URL @@ -708,9 +709,10 @@ export const useConnectorDialog = () => { await refetchAllConnectors(); } else { // Other non-indexable connectors - just show success message and close - const successMessage = currentConnectorType === "MCP_CONNECTOR" - ? `${connector.name} added successfully` - : `${connectorTitle} connected successfully!`; + const successMessage = + currentConnectorType === "MCP_CONNECTOR" + ? `${connector.name} added successfully` + : `${connectorTitle} connected successfully!`; toast.success(successMessage); // Refresh connectors list before closing modal @@ -758,7 +760,7 @@ export const useConnectorDialog = () => { const handleBackFromConnect = useCallback(() => { const url = new URL(window.location.href); url.searchParams.set("modal", "connectors"); - + // If we're connecting an MCP and came from list view, go back to list if (connectingConnectorType === "MCP_CONNECTOR" && viewingMCPList) { url.searchParams.set("view", "mcp-list"); @@ -766,7 +768,7 @@ export const useConnectorDialog = () => { url.searchParams.set("tab", "all"); url.searchParams.delete("view"); } - + url.searchParams.delete("connectorType"); router.replace(url.pathname + url.search, { scroll: false }); }, [router, connectingConnectorType, viewingMCPList]); @@ -1252,33 +1254,33 @@ export const useConnectorDialog = () => { ); } - // Generate toast message based on connector type - const toastTitle = `${editingConnector.name} updated successfully`; + // Generate toast message based on connector type + const toastTitle = `${editingConnector.name} updated successfully`; - toast.success(toastTitle, { - description: periodicEnabled - ? `Periodic sync ${frequency ? `enabled every ${getFrequencyLabel(frequencyMinutes)}` : "enabled"}. ${indexingDescription}` - : indexingDescription, - }); + toast.success(toastTitle, { + description: periodicEnabled + ? `Periodic sync ${frequency ? `enabled every ${getFrequencyLabel(frequencyMinutes)}` : "enabled"}. ${indexingDescription}` + : indexingDescription, + }); - // Update URL - the effect will handle closing the modal and clearing state - const url = new URL(window.location.href); - url.searchParams.delete("modal"); - url.searchParams.delete("tab"); - url.searchParams.delete("view"); - url.searchParams.delete("connectorId"); - router.replace(url.pathname + url.search, { scroll: false }); + // Update URL - the effect will handle closing the modal and clearing state + const url = new URL(window.location.href); + url.searchParams.delete("modal"); + url.searchParams.delete("tab"); + url.searchParams.delete("view"); + url.searchParams.delete("connectorId"); + router.replace(url.pathname + url.search, { scroll: false }); - refreshConnectors(); - queryClient.invalidateQueries({ - queryKey: cacheKeys.logs.summary(Number(searchSpaceId)), - }); - } catch (error) { - console.error("Error saving connector:", error); - toast.error("Failed to save connector changes"); - } finally { - setIsSaving(false); - } + refreshConnectors(); + queryClient.invalidateQueries({ + queryKey: cacheKeys.logs.summary(Number(searchSpaceId)), + }); + } catch (error) { + console.error("Error saving connector:", error); + toast.error("Failed to save connector changes"); + } finally { + setIsSaving(false); + } }, [ editingConnector, 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 1e25d24a0..a518d63a6 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 @@ -96,9 +96,7 @@ export const ActiveConnectorsTab: FC = ({ // Separate OAuth and non-OAuth connectors const oauthConnectors = connectors.filter((c) => oauthConnectorTypes.has(c.connector_type)); - const nonOauthConnectors = connectors.filter( - (c) => !oauthConnectorTypes.has(c.connector_type) - ); + const nonOauthConnectors = connectors.filter((c) => !oauthConnectorTypes.has(c.connector_type)); // Group OAuth connectors by type const oauthConnectorsByType = oauthConnectors.reduce( @@ -150,8 +148,7 @@ export const ActiveConnectorsTab: FC = ({ }); const hasActiveConnectors = - filteredOAuthConnectorTypes.length > 0 || - filteredNonOAuthConnectors.length > 0; + filteredOAuthConnectorTypes.length > 0 || filteredNonOAuthConnectors.length > 0; return ( 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 260d6c926..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 @@ -123,7 +123,6 @@ export const AllConnectorsTab: FC = ({ isConnecting={isConnecting} documentCount={documentCount} accountCount={accountCount} - isIndexing={isIndexing} onConnect={() => onConnectOAuth(connector)} onManage={ @@ -165,9 +164,13 @@ export const AllConnectorsTab: FC = ({ // 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 mcpConnectorCount = + isMCP && allConnectors + ? allConnectors.filter( + (c: SearchSourceConnector) => + c.connector_type === EnumConnectorName.MCP_CONNECTOR + ).length + : undefined; const handleConnect = onConnectNonOAuth ? () => onConnectNonOAuth(connector.connectorType) 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 36afc44f0..d1d5bee08 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 @@ -1,14 +1,14 @@ /** * MCP Configuration Validator Utility - * + * * Shared validation and parsing logic for MCP (Model Context Protocol) server configurations. - * + * * Features: * - Zod schema validation for runtime type safety * - Configuration caching to avoid repeated parsing (5-minute TTL) * - Standardized error messages * - Connection testing utilities - * + * * Usage: * ```typescript * // Parse and validate config @@ -18,14 +18,14 @@ * } else { * // Show result.error to user * } - * + * * // Test connection * const testResult = await testMCPConnection(config); * if (testResult.status === "success") { * console.log(`Found ${testResult.tools.length} tools`); * } * ``` - * + * * @module mcp-config-validator */ @@ -36,7 +36,7 @@ import { connectorsApiService } from "@/lib/apis/connectors-api.service"; /** * Zod schema for MCP server configuration * Provides compile-time and runtime type safety - * + * * Exported for advanced use cases (e.g., form builders) */ export const MCPServerConfigSchema = z.object({ @@ -95,11 +95,11 @@ export const parseMCPConfig = (configJson: string): MCPConfigValidationResult => // Check cache first const cached = configCache.get(configJson); if (cached && Date.now() - cached.timestamp < CACHE_TTL) { - console.log('[MCP Validator] ✅ Using cached config'); + console.log("[MCP Validator] ✅ Using cached config"); return { config: cached.config, error: null }; } - console.log('[MCP Validator] 🔍 Parsing new config...'); + console.log("[MCP Validator] 🔍 Parsing new config..."); // Clean up expired cache entries periodically if (configCache.size > 100) { @@ -111,7 +111,7 @@ export const parseMCPConfig = (configJson: string): MCPConfigValidationResult => // Validate that it's an object, not an array if (Array.isArray(parsed)) { - console.error('[MCP Validator] ❌ Error: Config is an array, expected object'); + console.error("[MCP Validator] ❌ Error: Config is an array, expected object"); return { config: null, error: "Please provide a single server configuration object, not an array", @@ -125,27 +125,23 @@ export const parseMCPConfig = (configJson: string): MCPConfigValidationResult => // Format Zod validation errors for user-friendly display const firstError = result.error.issues[0]; const fieldPath = firstError.path.join("."); - + // Clean up error message - remove technical Zod jargon let errorMsg = firstError.message; - + // Replace technical error messages with user-friendly ones if (errorMsg.includes("expected string, received undefined")) { - errorMsg = fieldPath - ? `The '${fieldPath}' field is required` - : "This field is required"; + errorMsg = fieldPath ? `The '${fieldPath}' field is required` : "This field is required"; } else if (errorMsg.includes("Invalid input")) { - errorMsg = fieldPath - ? `The '${fieldPath}' field has an invalid value` - : "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}`; } - - console.error('[MCP Validator] ❌ Validation error:', errorMsg); - console.error('[MCP Validator] Full Zod errors:', result.error.issues); - + + console.error("[MCP Validator] ❌ Validation error:", errorMsg); + console.error("[MCP Validator] Full Zod errors:", result.error.issues); + return { config: null, error: errorMsg, @@ -164,8 +160,8 @@ export const parseMCPConfig = (configJson: string): MCPConfigValidationResult => config, timestamp: Date.now(), }); - - console.log('[MCP Validator] ✅ Config parsed successfully:', config); + + console.log("[MCP Validator] ✅ Config parsed successfully:", config); return { config, @@ -173,7 +169,7 @@ export const parseMCPConfig = (configJson: string): MCPConfigValidationResult => }; } catch (error) { const errorMsg = error instanceof Error ? error.message : "Invalid JSON"; - console.error('[MCP Validator] ❌ JSON parse error:', errorMsg); + console.error("[MCP Validator] ❌ JSON parse error:", errorMsg); return { config: null, error: errorMsg, @@ -222,11 +218,11 @@ export const testMCPConnection = async ( export const extractServerName = (configJson: string): string => { try { const parsed = JSON.parse(configJson); - + // Use Zod to validate and extract name field safely const nameSchema = z.object({ name: z.string().optional() }); const result = nameSchema.safeParse(parsed); - + if (result.success && result.data.name) { return result.data.name; } 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 fc1f76f66..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 @@ -82,11 +82,13 @@ 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 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) { @@ -177,58 +179,58 @@ export const ConnectorAccountsListView: FC = ({ ) : (

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

- {getDisplayName(connector)} -

- {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/lib/apis/connectors-api.service.ts b/surfsense_web/lib/apis/connectors-api.service.ts index 17e2d0b3f..0e4f7f4d5 100644 --- a/surfsense_web/lib/apis/connectors-api.service.ts +++ b/surfsense_web/lib/apis/connectors-api.service.ts @@ -267,9 +267,13 @@ class ConnectorsApiService { search_space_id: String(queryParams.search_space_id), }).toString(); - return baseApiService.post(`/api/v1/connectors/mcp?${queryString}`, undefined, { - body: data, - }); + return baseApiService.post( + `/api/v1/connectors/mcp?${queryString}`, + undefined, + { + body: data, + } + ); }; /**