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 dcd20cefd..4d141d4ea 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
@@ -14,12 +14,10 @@ import type { ConnectorConfigProps } from "../index";
interface MCPConfigProps extends ConnectorConfigProps {
onNameChange?: (name: string) => void;
- searchSpaceId?: string;
- onOtherMCPConnectorsLoaded?: (connectorIds: number[]) => void;
}
-export const MCPConfig: FC = ({ connector, onConfigChange, onNameChange, searchSpaceId, onOtherMCPConnectorsLoaded }) => {
- const [name, setName] = useState("MCPs");
+export const MCPConfig: FC = ({ connector, onConfigChange, onNameChange }) => {
+ const [name, setName] = useState("");
const [configJson, setConfigJson] = useState("");
const [jsonError, setJsonError] = useState(null);
const [isTesting, setIsTesting] = useState(false);
@@ -29,55 +27,26 @@ export const MCPConfig: FC = ({ connector, onConfigChange, onNam
message: string;
tools: MCPToolDefinition[];
} | null>(null);
- const [allMCPConnectors, setAllMCPConnectors] = useState([]);
- // Load all MCP connectors for this search space
+ // Initialize form from connector config (only on mount)
useEffect(() => {
- const loadAllMCPConnectors = async () => {
- if (!searchSpaceId) return;
-
- try {
- const connectors = await connectorsApiService.getConnectors({
- queryParams: { search_space_id: parseInt(searchSpaceId, 10) }
- });
- const mcpConnectors = connectors.filter((c: any) => c.connector_type === "MCP_CONNECTOR");
- setAllMCPConnectors(mcpConnectors);
-
- // Notify parent about other MCP connectors that should be deleted on save
- const otherConnectorIds = mcpConnectors
- .filter((c: any) => c.id !== connector.id)
- .map((c: any) => c.id);
- if (onOtherMCPConnectorsLoaded && otherConnectorIds.length > 0) {
- onOtherMCPConnectorsLoaded(otherConnectorIds);
- }
-
- // Collect all server configs from all MCP connectors
- const allServerConfigs: MCPServerConfig[] = [];
- for (const mcpConn of mcpConnectors) {
- const serverConfigs = mcpConn.config?.server_configs as MCPServerConfig[] | undefined;
- if (serverConfigs && Array.isArray(serverConfigs)) {
- allServerConfigs.push(...serverConfigs);
- }
- }
-
- if (allServerConfigs.length > 0) {
- setConfigJson(JSON.stringify(allServerConfigs, null, 2));
- } else {
- setConfigJson(JSON.stringify([{
- command: "",
- args: [],
- env: {},
- transport: "stdio",
- }], null, 2));
- }
- } catch (error) {
- console.error("Failed to load MCP connectors:", error);
- }
- };
+ if (connector.name) {
+ setName(connector.name);
+ }
- loadAllMCPConnectors();
- }, [searchSpaceId]);
-
+ const serverConfig = connector.config?.server_config as MCPServerConfig | undefined;
+ if (serverConfig) {
+ // Convert server config to JSON string for editing (name is in separate field)
+ const configObj = {
+ command: serverConfig.command || "",
+ args: serverConfig.args || [],
+ env: serverConfig.env || {},
+ transport: serverConfig.transport || "stdio",
+ };
+ setConfigJson(JSON.stringify(configObj, null, 2));
+ }
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []); // Only run on mount to preserve user edits
const handleNameChange = (value: string) => {
setName(value);
@@ -86,31 +55,31 @@ export const MCPConfig: FC = ({ connector, onConfigChange, onNam
}
};
- const parseConfig = (): MCPServerConfig[] | null => {
+ const parseConfig = (): MCPServerConfig | null => {
try {
const parsed = JSON.parse(configJson);
-
- // Handle both single object and array
- const configs = Array.isArray(parsed) ? parsed : [parsed];
-
- // Validate each config
- const validConfigs: MCPServerConfig[] = [];
- for (let i = 0; i < configs.length; i++) {
- const cfg = configs[i];
- if (!cfg.command || typeof cfg.command !== "string") {
- setJsonError(`Config ${i + 1}: 'command' field is required and must be a string`);
- return null;
- }
- validConfigs.push({
- command: cfg.command,
- args: cfg.args || [],
- env: cfg.env || {},
- transport: cfg.transport || "stdio",
- });
+
+ // 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;
}
-
+
+ // Validate required fields
+ if (!parsed.command || typeof parsed.command !== "string") {
+ setJsonError("'command' field is required and must be a string");
+ return null;
+ }
+
+ const config: MCPServerConfig = {
+ command: parsed.command,
+ args: parsed.args || [],
+ env: parsed.env || {},
+ transport: parsed.transport || "stdio",
+ };
+
setJsonError(null);
- return validConfigs;
+ return config;
} catch (error) {
setJsonError(error instanceof Error ? error.message : "Invalid JSON");
return null;
@@ -119,51 +88,32 @@ export const MCPConfig: FC = ({ connector, onConfigChange, onNam
const handleConfigChange = (value: string) => {
setConfigJson(value);
- // Clear error when user starts typing
if (jsonError) {
setJsonError(null);
}
- // Treat empty/whitespace-only input as empty array (user wants to remove all servers)
- const trimmedValue = value.trim();
- if (trimmedValue === "") {
- if (onConfigChange) {
- onConfigChange({ server_configs: [] });
- }
- return;
- }
-
// Try to parse and update parent if valid
try {
const parsed = JSON.parse(value);
- const configs = Array.isArray(parsed) ? parsed : [parsed];
-
- // Validate each config
- const validConfigs: MCPServerConfig[] = [];
- for (const cfg of configs) {
- if (cfg.command && typeof cfg.command === "string") {
- validConfigs.push({
- command: cfg.command,
- args: cfg.args || [],
- env: cfg.env || {},
- transport: cfg.transport || "stdio",
- });
+ if (!Array.isArray(parsed) && parsed.command) {
+ const config: MCPServerConfig = {
+ command: parsed.command,
+ args: parsed.args || [],
+ env: parsed.env || {},
+ transport: parsed.transport || "stdio",
+ };
+ if (onConfigChange) {
+ onConfigChange({ server_config: config });
}
}
-
- // Always update parent with configs (including empty array)
- // Empty array signals that all servers should be removed
- if (onConfigChange) {
- onConfigChange({ server_configs: validConfigs });
- }
} catch {
- // Ignore parse errors while typing - don't update parent with invalid config
+ // Ignore parse errors while typing
}
};
const handleTestConnection = async () => {
- const serverConfigs = parseConfig();
- if (!serverConfigs || serverConfigs.length === 0) {
+ const serverConfig = parseConfig();
+ if (!serverConfig) {
setTestResult({
status: "error",
message: jsonError || "Invalid configuration",
@@ -172,55 +122,34 @@ export const MCPConfig: FC = ({ connector, onConfigChange, onNam
return;
}
- // Update parent with the config array
+ // Update parent with the config
if (onConfigChange) {
- onConfigChange({ server_configs: serverConfigs });
+ onConfigChange({ server_config: serverConfig });
}
setIsTesting(true);
setTestResult(null);
try {
- // Test all servers and collect results
- const allTools: MCPToolDefinition[] = [];
- const errors: string[] = [];
+ const result = await connectorsApiService.testMCPConnection(serverConfig);
- for (const serverConfig of serverConfigs) {
- try {
- const result = await connectorsApiService.testMCPConnection(serverConfig);
- if (result.status === "success") {
- allTools.push(...result.tools);
- } else {
- errors.push(`${serverConfig.command}: ${result.message}`);
- }
- } catch (error) {
- errors.push(`${serverConfig.command}: ${error instanceof Error ? error.message : "Failed to connect"}`);
- }
- }
-
- if (errors.length === 0) {
+ if (result.status === "success") {
setTestResult({
status: "success",
- message: `Successfully connected to ${serverConfigs.length} server(s)`,
- tools: allTools,
- });
- } else if (allTools.length > 0) {
- setTestResult({
- status: "success",
- message: `Partially successful. Errors: ${errors.join("; ")}`,
- tools: allTools,
+ message: `Connected successfully! Found ${result.tools.length} tool(s).`,
+ tools: result.tools,
});
} else {
setTestResult({
status: "error",
- message: errors.join("; "),
+ message: result.message || "Failed to connect",
tools: [],
});
}
} catch (error) {
setTestResult({
status: "error",
- message: error instanceof Error ? error.message : "Failed to connect to MCP servers",
+ message: error instanceof Error ? error.message : "Failed to connect",
tools: [],
});
} finally {
@@ -230,6 +159,18 @@ export const MCPConfig: FC = ({ connector, onConfigChange, onNam
return (
+ {/* Server Name */}
+
+ Server Name *
+ handleNameChange(e.target.value)}
+ placeholder="e.g., Filesystem Server"
+ required
+ />
+
+
{/* Server Configuration */}
@@ -239,21 +180,19 @@ export const MCPConfig: FC = ({ connector, onConfigChange, onNam
+
MCP Server Configuration (JSON)
@@ -266,7 +205,7 @@ export const MCPConfig: FC
= ({ connector, onConfigChange, onNam
variant="outline"
className="w-full"
>
- {isTesting ? "Testing..." : "Test Connection"}
+ {isTesting ? "Testing Connection..." : "Test Connection"}
@@ -280,9 +219,9 @@ export const MCPConfig: FC = ({ connector, onConfigChange, onNam
}
>
{testResult.status === "success" ? (
-
+
) : (
-
+
)}
@@ -317,10 +256,10 @@ export const MCPConfig: FC = ({ connector, onConfigChange, onNam
{testResult.message}
- {showDetails && testResult.status === "success" && testResult.tools.length > 0 && (
+ {showDetails && testResult.tools.length > 0 && (
- Found {testResult.tools.length} tool{testResult.tools.length !== 1 ? 's' : ''}:
+ Available tools:
{testResult.tools.map((tool, i) => (
diff --git a/surfsense_web/components/assistant-ui/connector-popup/constants/connector-popup.schemas.ts b/surfsense_web/components/assistant-ui/connector-popup/constants/connector-popup.schemas.ts
index a1b303163..d74d66203 100644
--- a/surfsense_web/components/assistant-ui/connector-popup/constants/connector-popup.schemas.ts
+++ b/surfsense_web/components/assistant-ui/connector-popup/constants/connector-popup.schemas.ts
@@ -7,7 +7,7 @@ import { searchSourceConnectorTypeEnum } from "@/contracts/types/connector.types
export const connectorPopupQueryParamsSchema = z.object({
modal: z.enum(["connectors"]).optional(),
tab: z.enum(["all", "active"]).optional(),
- view: z.enum(["configure", "edit", "connect", "youtube", "accounts"]).optional(),
+ view: z.enum(["configure", "edit", "connect", "youtube", "accounts", "mcp-list"]).optional(),
connector: z.string().optional(),
connectorId: z.string().optional(),
connectorType: z.string().optional(),
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 6cfc279ac..8583da7bf 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
@@ -68,7 +68,6 @@ export const useConnectorDialog = () => {
const [isDisconnecting, setIsDisconnecting] = useState(false);
const [connectorConfig, setConnectorConfig] = useState | null>(null);
const [connectorName, setConnectorName] = useState(null);
- const [otherMCPConnectorIds, setOtherMCPConnectorIds] = useState([]);
// Connect mode state (for non-OAuth connectors)
const [connectingConnectorType, setConnectingConnectorType] = useState(null);
@@ -81,12 +80,18 @@ export const useConnectorDialog = () => {
connectorTitle: string;
} | null>(null);
+ // MCP list view state (for managing multiple MCP connectors)
+ const [viewingMCPList, setViewingMCPList] = useState(false);
+
// Track if we came from accounts list when entering edit mode
const [cameFromAccountsList, setCameFromAccountsList] = useState<{
connectorType: string;
connectorTitle: string;
} | null>(null);
+ // Track if we came from MCP list view when entering edit mode
+ const [cameFromMCPList, setCameFromMCPList] = useState(false);
+
// Helper function to get frequency label
const getFrequencyLabel = useCallback((minutes: string): string => {
switch (minutes) {
@@ -140,6 +145,16 @@ export const useConnectorDialog = () => {
setViewingAccountsType(null);
}
+ // Clear MCP list view if view is not "mcp-list" anymore
+ if (params.view !== "mcp-list" && viewingMCPList) {
+ setViewingMCPList(false);
+ }
+
+ // Handle MCP list view
+ if (params.view === "mcp-list" && !viewingMCPList) {
+ setViewingMCPList(true);
+ }
+
// Handle connect view
if (params.view === "connect" && params.connectorType && !connectingConnectorType) {
setConnectingConnectorType(params.connectorType);
@@ -623,32 +638,34 @@ export const useConnectorDialog = () => {
},
});
- toast.success(`${connectorTitle} connected and indexing started!`, {
- description: periodicEnabledForIndexing
- ? `Periodic sync enabled every ${getFrequencyLabel(frequencyMinutesForIndexing)}.`
- : "You can continue working while we sync your data.",
- });
+ const successMessage = currentConnectorType === "MCP_CONNECTOR"
+ ? `${connector.name} MCP server 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.",
+ });
- // Close modal and return to main view
- 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
@@ -685,7 +702,10 @@ export const useConnectorDialog = () => {
await refetchAllConnectors();
} else {
// Other non-indexable connectors - just show success message and close
- toast.success(`${connectorTitle} connected successfully!`);
+ const successMessage = currentConnectorType === "MCP_CONNECTOR"
+ ? `${connector.name} MCP server added successfully`
+ : `${connectorTitle} connected successfully!`;
+ toast.success(successMessage);
// Close modal and return to main view
const url = new URL(window.location.href);
@@ -729,11 +749,18 @@ export const useConnectorDialog = () => {
const handleBackFromConnect = useCallback(() => {
const url = new URL(window.location.href);
url.searchParams.set("modal", "connectors");
- url.searchParams.set("tab", "all");
- url.searchParams.delete("view");
+
+ // 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");
+ } else {
+ url.searchParams.set("tab", "all");
+ url.searchParams.delete("view");
+ }
+
url.searchParams.delete("connectorType");
router.replace(url.pathname + url.search, { scroll: false });
- }, [router]);
+ }, [router, connectingConnectorType, viewingMCPList]);
// Handle going back from YouTube view
const handleBackFromYouTube = useCallback(() => {
@@ -776,6 +803,38 @@ export const useConnectorDialog = () => {
router.replace(url.pathname + url.search, { scroll: false });
}, [router]);
+ // Handle viewing MCP list
+ const handleViewMCPList = useCallback(() => {
+ if (!searchSpaceId) return;
+
+ setViewingMCPList(true);
+
+ // Update URL to show MCP list view
+ const url = new URL(window.location.href);
+ url.searchParams.set("modal", "connectors");
+ url.searchParams.set("view", "mcp-list");
+ window.history.pushState({ modal: true }, "", url.toString());
+ }, [searchSpaceId]);
+
+ // Handle going back from MCP list view
+ const handleBackFromMCPList = useCallback(() => {
+ setViewingMCPList(false);
+ const url = new URL(window.location.href);
+ url.searchParams.set("modal", "connectors");
+ url.searchParams.delete("view");
+ router.replace(url.pathname + url.search, { scroll: false });
+ }, [router]);
+
+ // Handle adding new MCP from list view
+ const handleAddNewMCPFromList = useCallback(() => {
+ setConnectingConnectorType("MCP_CONNECTOR");
+ const url = new URL(window.location.href);
+ url.searchParams.set("modal", "connectors");
+ url.searchParams.set("view", "connect");
+ url.searchParams.set("connectorType", "MCP_CONNECTOR");
+ router.replace(url.pathname + url.search, { scroll: false });
+ }, [router]);
+
// Handle starting indexing
const handleStartIndexing = useCallback(
async (refreshConnectors: () => void) => {
@@ -961,6 +1020,13 @@ export const useConnectorDialog = () => {
(connector: SearchSourceConnector) => {
if (!searchSpaceId) return;
+ // For MCP connectors from "All Connectors" tab, show the list view instead of directly editing
+ // (unless we're already in the MCP list view or on the Active tab where individual MCPs are shown)
+ if (connector.connector_type === "MCP_CONNECTOR" && !viewingMCPList && activeTab === "all") {
+ handleViewMCPList();
+ return;
+ }
+
// All connector types should be handled in the popup edit view
// Validate connector data
const connectorValidation = searchSourceConnector.safeParse(connector);
@@ -977,6 +1043,13 @@ export const useConnectorDialog = () => {
setCameFromAccountsList(null);
}
+ // Track if we came from MCP list view
+ if (viewingMCPList && connector.connector_type === "MCP_CONNECTOR") {
+ setCameFromMCPList(true);
+ } else {
+ setCameFromMCPList(false);
+ }
+
// Track index with date range opened event
if (connector.is_indexable) {
trackIndexWithDateRangeOpened(
@@ -1006,7 +1079,7 @@ export const useConnectorDialog = () => {
url.searchParams.set("connectorId", connector.id.toString());
window.history.pushState({ modal: true }, "", url.toString());
},
- [searchSpaceId, viewingAccountsType]
+ [searchSpaceId, viewingAccountsType, viewingMCPList, handleViewMCPList, activeTab]
);
// Handle saving connector changes
@@ -1047,82 +1120,6 @@ export const useConnectorDialog = () => {
const startDateStr = startDate ? format(startDate, "yyyy-MM-dd") : undefined;
const endDateStr = endDate ? format(endDate, "yyyy-MM-dd") : undefined;
- // For MCP connectors, track original server count for toast messages
- let originalServerCount = 0;
- let newServerCount = 0;
- if (editingConnector.connector_type === "MCP_CONNECTOR") {
- const originalServerConfigs = editingConnector.config?.server_configs;
- originalServerCount = Array.isArray(originalServerConfigs) ? originalServerConfigs.length : 0;
- const newServerConfigs = connectorConfig?.server_configs;
- newServerCount = Array.isArray(newServerConfigs) ? newServerConfigs.length : 0;
- }
-
- // For MCP connectors, check if all servers were removed (empty array)
- if (editingConnector.connector_type === "MCP_CONNECTOR") {
- const serverConfigs = connectorConfig?.server_configs;
- if (!serverConfigs || (Array.isArray(serverConfigs) && serverConfigs.length === 0)) {
- // All servers removed - delete the entire connector
- await deleteConnector({
- id: editingConnector.id,
- });
-
- // Also delete other MCP connectors that were consolidated
- if (otherMCPConnectorIds.length > 0) {
- await Promise.all(
- otherMCPConnectorIds.map((id) =>
- deleteConnector({
- id,
- }).catch(() => {
- // Silently ignore errors for individual deletions
- })
- )
- );
- setOtherMCPConnectorIds([]);
- }
-
- toast.success("MCPs disconnected successfully", {
- description: "All MCP servers have been removed.",
- });
-
- // Update URL to close modal
- 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 });
-
- // Refresh connectors and reset state
- refreshConnectors();
- setEditingConnector(null);
- setConnectorName("");
- setConnectorConfig(null);
- setPeriodicEnabled(false);
- setFrequencyMinutes("1440");
- setStartDate(undefined);
- setEndDate(undefined);
- setOtherMCPConnectorIds([]);
-
- setIsSaving(false);
- return;
- }
- }
-
- // For MCP connectors, delete other MCP connectors first (consolidate all into one)
- if (editingConnector.connector_type === "MCP_CONNECTOR" && otherMCPConnectorIds.length > 0) {
- // Silently delete other MCP connectors without showing toasts
- await Promise.all(
- otherMCPConnectorIds.map((id) =>
- deleteConnector({
- id,
- }).catch(() => {
- // Silently ignore errors for individual deletions
- })
- )
- );
- setOtherMCPConnectorIds([]);
- }
-
// Update connector with periodic sync settings, config changes, and name
// Note: Periodic sync is disabled for Google Drive connectors and non-indexable connectors
const frequency =
@@ -1228,17 +1225,7 @@ export const useConnectorDialog = () => {
}
// Generate toast message based on connector type
- let toastTitle = `${editingConnector.name} updated successfully`;
- if (editingConnector.connector_type === "MCP_CONNECTOR") {
- const serverDiff = newServerCount - originalServerCount;
- if (serverDiff > 0) {
- toastTitle = `${serverDiff} MCP ${serverDiff === 1 ? "server" : "servers"} added`;
- } else if (serverDiff < 0) {
- toastTitle = `${Math.abs(serverDiff)} MCP ${Math.abs(serverDiff) === 1 ? "server" : "servers"} removed`;
- } else {
- toastTitle = "MCPs updated successfully";
- }
- }
+ const toastTitle = `${editingConnector.name} updated successfully`;
toast.success(toastTitle, {
description: periodicEnabled
@@ -1279,8 +1266,6 @@ export const useConnectorDialog = () => {
router,
connectorConfig,
connectorName,
- otherMCPConnectorIds,
- deleteConnector,
]
);
@@ -1304,16 +1289,24 @@ export const useConnectorDialog = () => {
toast.success(
editingConnector.connector_type === "MCP_CONNECTOR"
- ? "MCPs disconnected successfully"
+ ? `${editingConnector.name} MCP server removed successfully`
: `${editingConnector.name} disconnected successfully`
);
- // Update URL - the effect will handle closing the modal and clearing state
+ // Update URL - for MCP, go back to list; for others, close modal
const url = new URL(window.location.href);
- url.searchParams.delete("modal");
- url.searchParams.delete("tab");
- url.searchParams.delete("view");
- url.searchParams.delete("connectorId");
+ if (editingConnector.connector_type === "MCP_CONNECTOR") {
+ // Go back to MCP list view
+ setViewingMCPList(true);
+ url.searchParams.set("modal", "connectors");
+ url.searchParams.set("view", "mcp-list");
+ url.searchParams.delete("connectorId");
+ } else {
+ 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();
@@ -1365,6 +1358,21 @@ export const useConnectorDialog = () => {
// Handle going back from edit view
const handleBackFromEdit = useCallback(() => {
+ // If editing an MCP connector and came from MCP list, go back to MCP list view
+ if (editingConnector?.connector_type === "MCP_CONNECTOR" && cameFromMCPList) {
+ setViewingMCPList(true);
+ setCameFromMCPList(false);
+ const url = new URL(window.location.href);
+ url.searchParams.set("modal", "connectors");
+ url.searchParams.set("view", "mcp-list");
+ url.searchParams.delete("connectorId");
+ router.replace(url.pathname + url.search, { scroll: false });
+ setEditingConnector(null);
+ setConnectorName(null);
+ setConnectorConfig(null);
+ return;
+ }
+
// If we came from accounts list view, go back there
if (cameFromAccountsList && editingConnector) {
// Restore accounts list view
@@ -1377,10 +1385,10 @@ export const useConnectorDialog = () => {
url.searchParams.delete("connectorId");
router.replace(url.pathname + url.search, { scroll: false });
} else {
- // Otherwise, go back to main connector popup
+ // Otherwise, go back to main connector popup (preserve the tab the user was on)
const url = new URL(window.location.href);
url.searchParams.set("modal", "connectors");
- url.searchParams.set("tab", "all");
+ url.searchParams.set("tab", activeTab); // Use current tab instead of always "all"
url.searchParams.delete("view");
url.searchParams.delete("connectorId");
router.replace(url.pathname + url.search, { scroll: false });
@@ -1388,7 +1396,7 @@ export const useConnectorDialog = () => {
setEditingConnector(null);
setConnectorName(null);
setConnectorConfig(null);
- }, [router, cameFromAccountsList, editingConnector]);
+ }, [router, cameFromAccountsList, editingConnector, cameFromMCPList, activeTab]);
// Handle dialog open/close
const handleOpenChange = useCallback(
@@ -1466,6 +1474,7 @@ export const useConnectorDialog = () => {
searchSpaceId,
allConnectors,
viewingAccountsType,
+ viewingMCPList,
// Setters
setSearchQuery,
@@ -1474,7 +1483,6 @@ export const useConnectorDialog = () => {
setPeriodicEnabled,
setFrequencyMinutes,
setConnectorName,
- setOtherMCPConnectorIds,
// Handlers
handleOpenChange,
@@ -1495,6 +1503,9 @@ export const useConnectorDialog = () => {
handleBackFromYouTube,
handleViewAccountsList,
handleBackFromAccountsList,
+ handleViewMCPList,
+ handleBackFromMCPList,
+ handleAddNewMCPFromList,
handleQuickIndexConnector,
connectorConfig,
setConnectorConfig,
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 ba4a03084..b7edf3643 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
@@ -123,19 +123,12 @@ export const ActiveConnectorsTab: FC = ({
// Get OAuth connector types set for quick lookup
const oauthConnectorTypes = new Set(OAUTH_CONNECTORS.map((c) => c.connectorType));
- // Separate OAuth, MCP, and other non-OAuth connectors
+ // Separate OAuth and non-OAuth connectors
const oauthConnectors = connectors.filter((c) => oauthConnectorTypes.has(c.connector_type));
- const mcpConnectors = connectors.filter((c) => c.connector_type === "MCP_CONNECTOR");
const nonOauthConnectors = connectors.filter(
- (c) => !oauthConnectorTypes.has(c.connector_type) && c.connector_type !== "MCP_CONNECTOR"
+ (c) => !oauthConnectorTypes.has(c.connector_type)
);
- // 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) => {
@@ -185,17 +178,9 @@ export const ActiveConnectorsTab: FC = ({
);
});
- // Check if MCPs match search query
- const showMCPs =
- mcpConnectors.length > 0 &&
- (!searchQuery ||
- "mcps".includes(searchQuery.toLowerCase()) ||
- "model context protocol".includes(searchQuery.toLowerCase()));
-
const hasActiveConnectors =
filteredOAuthConnectorTypes.length > 0 ||
- filteredNonOAuthConnectors.length > 0 ||
- showMCPs;
+ filteredNonOAuthConnectors.length > 0;
return (
@@ -217,16 +202,8 @@ export const ActiveConnectorsTab: FC = ({
const documentCount = getDocumentCountForConnector(
connectorType,
documentTypeCounts
- );
- // Calculate account count - for MCP, count servers; for others, count connectors
- const accountCount =
- connectorType === "MCP_CONNECTOR"
- ? typeConnectors.reduce((total, c) => {
- const serverConfigs = c.config?.server_configs;
- return total + (Array.isArray(serverConfigs) ? serverConfigs.length : 0);
- }, 0)
- : typeConnectors.length;
- const mostRecentLastIndexed = getMostRecentLastIndexed(typeConnectors);
+ );
+ const accountCount = typeConnectors.length;
const handleManageClick = () => {
if (onViewAccountsList) {
@@ -298,41 +275,6 @@ export const ActiveConnectorsTab: FC = ({
);
})}
- {/* MCP Connectors - Single Grouped Card */}
- {showMCPs && (
-
-
- {getConnectorIcon("MCP_CONNECTOR", "size-6")}
-
-
-
MCPs
-
- {totalMCPServers} {totalMCPServers === 1 ? "Server" : "Servers"}
-
-
-
onManage(mcpConnectors[0]) : undefined
- }
- >
- Manage
-
-
- )}
-
{/* Non-OAuth Connectors - Individual Cards */}
{filteredNonOAuthConnectors.map((connector) => {
const isIndexing = indexingConnectorIds.has(connector.id);
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 a51b7d26a..0653a1cbe 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
@@ -103,14 +103,7 @@ export const AllConnectorsTab: FC = ({
)
: [];
- // Calculate account count - for MCP, count servers; for others, count connectors
- const accountCount =
- connector.connectorType === "MCP_CONNECTOR"
- ? typeConnectors.reduce((total, c) => {
- const serverConfigs = c.config?.server_configs;
- return total + (Array.isArray(serverConfigs) ? serverConfigs.length : 0);
- }, 0)
- : typeConnectors.length;
+ const accountCount = typeConnectors.length;
// Get the most recent last_indexed_at across all accounts
const mostRecentLastIndexed = typeConnectors.reduce(
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
new file mode 100644
index 000000000..2da6352a9
--- /dev/null
+++ b/surfsense_web/components/assistant-ui/connector-popup/views/mcp-connector-list-view.tsx
@@ -0,0 +1,122 @@
+"use client";
+
+import { Plus, Server } from "lucide-react";
+import type { FC } from "react";
+import { Button } from "@/components/ui/button";
+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,
+}) => {
+ return (
+
+ {/* Header */}
+
+
+
+
+
+
+
+
+
MCP Connectors
+
+ Manage your Model Context Protocol servers
+
+
+
+
+
+ {/* Add New Button */}
+
+
+
+ Add New MCP Server
+
+
+
+ {/* 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")}
+
+
+
onManageConnector(connector)}
+ >
+ Manage
+
+
+ );
+ })
+ )}
+
+
+ );
+};
diff --git a/surfsense_web/contracts/types/mcp.types.ts b/surfsense_web/contracts/types/mcp.types.ts
index eaed344df..e25ffe3c5 100644
--- a/surfsense_web/contracts/types/mcp.types.ts
+++ b/surfsense_web/contracts/types/mcp.types.ts
@@ -15,19 +15,19 @@ export const mcpServerConfig = z.object({
*/
export const mcpConnectorCreate = z.object({
name: z.string().min(1, "Connector name is required"),
- server_configs: z.array(mcpServerConfig).min(1, "At least one server configuration is required"),
+ server_config: mcpServerConfig,
});
export const mcpConnectorUpdate = z.object({
name: z.string().min(1).optional(),
- server_configs: z.array(mcpServerConfig).optional(),
+ server_config: mcpServerConfig.optional(),
});
export const mcpConnectorRead = z.object({
id: z.number(),
name: z.string(),
connector_type: z.literal("MCP_CONNECTOR"),
- server_configs: z.array(mcpServerConfig),
+ server_config: mcpServerConfig,
search_space_id: z.number(),
user_id: z.string(),
created_at: z.string(),