change the flow of mcp connectors

This commit is contained in:
Manoj Aggarwal 2026-01-16 14:39:44 -08:00
parent ab0582cb1d
commit 4c07c0fe4f
12 changed files with 492 additions and 522 deletions

View file

@ -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}")

View file

@ -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)

View file

@ -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,

View file

@ -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 ? (
<YouTubeCrawlerView searchSpaceId={searchSpaceId} onBack={handleBackFromYouTube} />
) : viewingMCPList ? (
<div className="p-6 sm:p-12 h-full overflow-hidden">
<MCPConnectorListView
mcpConnectors={
(allConnectors || []).filter(
(c: SearchSourceConnector) => c.connector_type === "MCP_CONNECTOR"
) as SearchSourceConnector[]
}
onAddNew={handleAddNewMCPFromList}
onManageConnector={handleStartEdit}
onBack={handleBackFromMCPList}
/>
</div>
) : viewingAccountsType ? (
<ConnectorAccountsListView
connectorType={viewingAccountsType.connectorType}

View file

@ -4,6 +4,7 @@ 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";
@ -11,23 +12,9 @@ import type { MCPServerConfig, MCPToolDefinition } from "@/contracts/types/mcp.t
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<ConnectFormProps> = ({ onSubmit, isSubmitting }) => {
const isSubmittingRef = useRef(false);
const [configJson, setConfigJson] = useState(DEFAULT_CONFIG);
const [configJson, setConfigJson] = useState("");
const [jsonError, setJsonError] = useState<string | null>(null);
const [isTesting, setIsTesting] = useState(false);
const [showDetails, setShowDetails] = useState(false);
@ -35,62 +22,50 @@ export const MCPConnectForm: FC<ConnectFormProps> = ({ 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<ConnectFormProps> = ({ 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<ConnectFormProps> = ({ 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<ConnectFormProps> = ({ 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<ConnectFormProps> = ({ onSubmit, isSubmitting })
<Alert className="bg-slate-400/5 dark:bg-white/5 border-slate-400/20 p-2 sm:p-3 flex items-start [&>svg]:relative [&>svg]:left-0 [&>svg]:top-1">
<Server className="h-4 w-4 shrink-0 ml-1" />
<div className="-ml-1">
<AlertTitle className="text-xs sm:text-sm">MCP Servers</AlertTitle>
<AlertTitle className="text-xs sm:text-sm">MCP Server</AlertTitle>
<AlertDescription className="text-[10px] sm:text-xs pl-0!">
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.
</AlertDescription>
</div>
</Alert>
@ -210,7 +174,7 @@ export const MCPConnectForm: FC<ConnectFormProps> = ({ onSubmit, isSubmitting })
<form id="mcp-connect-form" onSubmit={handleSubmit} className="space-y-6">
<div className="rounded-xl border border-border bg-slate-400/5 dark:bg-white/5 p-4 sm:p-6 space-y-4">
<div className="space-y-2">
<Label htmlFor="config">MCP Servers Configuration (JSON Array)</Label>
<Label htmlFor="config">MCP Server Configuration (JSON)</Label>
<Textarea
id="config"
value={configJson}
@ -223,7 +187,7 @@ export const MCPConnectForm: FC<ConnectFormProps> = ({ onSubmit, isSubmitting })
<p className="text-xs text-red-500">JSON Error: {jsonError}</p>
)}
<p className="text-[10px] sm:text-xs text-muted-foreground">
Paste an array of MCP server configurations. Each object must have: name, command, args (optional), env (optional), transport (optional).
Paste a single MCP server configuration. Must include: name, command, args (optional), env (optional), transport (optional).
</p>
</div>
@ -235,7 +199,7 @@ export const MCPConnectForm: FC<ConnectFormProps> = ({ onSubmit, isSubmitting })
variant="outline"
className="w-full"
>
{isTesting ? "Testing All Servers..." : "Test All Connections"}
{isTesting ? "Testing Connection..." : "Test Connection"}
</Button>
</div>
@ -248,9 +212,9 @@ export const MCPConnectForm: FC<ConnectFormProps> = ({ onSubmit, isSubmitting })
}
>
{testResult.status === "success" ? (
<CheckCircle2 className="h-4 w-4 text-green-500" />
<CheckCircle2 className="h-4 w-4 text-green-600" />
) : (
<XCircle className="h-4 w-4 text-red-500" />
<XCircle className="h-4 w-4 text-red-600" />
)}
<div className="flex-1">
<div className="flex items-center justify-between">
@ -285,14 +249,6 @@ export const MCPConnectForm: FC<ConnectFormProps> = ({ onSubmit, isSubmitting })
</div>
<AlertDescription className="text-xs mt-1">
{testResult.message}
{testResult.errors && testResult.errors.length > 0 && (
<div className="mt-2 text-red-600">
<p className="font-semibold">Errors:</p>
{testResult.errors.map((err, i) => (
<div key={i}> {err}</div>
))}
</div>
)}
{showDetails && testResult.tools.length > 0 && (
<div className="mt-3 pt-3 border-t border-green-500/20">
<p className="font-semibold mb-2">

View file

@ -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<MCPConfigProps> = ({ connector, onConfigChange, onNameChange, searchSpaceId, onOtherMCPConnectorsLoaded }) => {
const [name, setName] = useState<string>("MCPs");
export const MCPConfig: FC<MCPConfigProps> = ({ connector, onConfigChange, onNameChange }) => {
const [name, setName] = useState<string>("");
const [configJson, setConfigJson] = useState("");
const [jsonError, setJsonError] = useState<string | null>(null);
const [isTesting, setIsTesting] = useState(false);
@ -29,55 +27,26 @@ export const MCPConfig: FC<MCPConfigProps> = ({ connector, onConfigChange, onNam
message: string;
tools: MCPToolDefinition[];
} | null>(null);
const [allMCPConnectors, setAllMCPConnectors] = useState<any[]>([]);
// 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<MCPConfigProps> = ({ 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<MCPConfigProps> = ({ 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<MCPConfigProps> = ({ 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<MCPConfigProps> = ({ connector, onConfigChange, onNam
return (
<div className="space-y-6">
{/* Server Name */}
<div className="space-y-2">
<Label htmlFor="name">Server Name *</Label>
<Input
id="name"
value={name}
onChange={(e) => handleNameChange(e.target.value)}
placeholder="e.g., Filesystem Server"
required
/>
</div>
{/* Server Configuration */}
<div className="space-y-4">
<h3 className="font-medium text-sm sm:text-base flex items-center gap-2">
@ -239,21 +180,19 @@ export const MCPConfig: FC<MCPConfigProps> = ({ connector, onConfigChange, onNam
<div className="rounded-xl border border-border bg-slate-400/5 dark:bg-white/5 p-3 sm:p-6 space-y-4">
<div className="space-y-2">
<Label htmlFor="config">MCP Server Configuration (JSON)</Label>
<Textarea
id="config"
value={configJson}
onChange={(e) => handleConfigChange(e.target.value)}
rows={12}
className={`font-mono text-xs border-slate-400/20 focus-visible:border-slate-400/40 ${
jsonError ? "border-red-500" : ""
}`}
rows={16}
className={`font-mono text-xs ${jsonError ? "border-red-500" : ""}`}
/>
{jsonError && (
<p className="text-xs text-red-500">
JSON Error: {jsonError}
</p>
<p className="text-xs text-red-500">JSON Error: {jsonError}</p>
)}
<p className="text-[10px] sm:text-xs text-muted-foreground">
Edit your MCP server configurations (array format). Each server requires: command, args, env, transport.
Edit your MCP server configuration. Must include: name, command, args (optional), env (optional), transport (optional).
</p>
</div>
@ -266,7 +205,7 @@ export const MCPConfig: FC<MCPConfigProps> = ({ connector, onConfigChange, onNam
variant="outline"
className="w-full"
>
{isTesting ? "Testing..." : "Test Connection"}
{isTesting ? "Testing Connection..." : "Test Connection"}
</Button>
</div>
@ -280,9 +219,9 @@ export const MCPConfig: FC<MCPConfigProps> = ({ connector, onConfigChange, onNam
}
>
{testResult.status === "success" ? (
<CheckCircle2 className="h-4 w-4 text-green-500" />
<CheckCircle2 className="h-4 w-4 text-green-600" />
) : (
<XCircle className="h-4 w-4 text-red-500" />
<XCircle className="h-4 w-4 text-red-600" />
)}
<div className="flex-1">
<div className="flex items-center justify-between">
@ -317,10 +256,10 @@ export const MCPConfig: FC<MCPConfigProps> = ({ connector, onConfigChange, onNam
</div>
<AlertDescription className="text-xs mt-1">
{testResult.message}
{showDetails && testResult.status === "success" && testResult.tools.length > 0 && (
{showDetails && testResult.tools.length > 0 && (
<div className="mt-3 pt-3 border-t border-green-500/20">
<p className="font-semibold mb-2">
Found {testResult.tools.length} tool{testResult.tools.length !== 1 ? 's' : ''}:
Available tools:
</p>
<ul className="list-disc list-inside text-xs space-y-0.5">
{testResult.tools.map((tool, i) => (

View file

@ -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(),

View file

@ -68,7 +68,6 @@ export const useConnectorDialog = () => {
const [isDisconnecting, setIsDisconnecting] = useState(false);
const [connectorConfig, setConnectorConfig] = useState<Record<string, unknown> | null>(null);
const [connectorName, setConnectorName] = useState<string | null>(null);
const [otherMCPConnectorIds, setOtherMCPConnectorIds] = useState<number[]>([]);
// Connect mode state (for non-OAuth connectors)
const [connectingConnectorType, setConnectingConnectorType] = useState<string | null>(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,

View file

@ -123,19 +123,12 @@ export const ActiveConnectorsTab: FC<ActiveConnectorsTabProps> = ({
// Get OAuth connector types set for quick lookup
const oauthConnectorTypes = new Set<string>(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<ActiveConnectorsTabProps> = ({
);
});
// 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 (
<TabsContent value="active" className="m-0">
@ -217,16 +202,8 @@ export const ActiveConnectorsTab: FC<ActiveConnectorsTabProps> = ({
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<ActiveConnectorsTabProps> = ({
);
})}
{/* MCP Connectors - Single Grouped Card */}
{showMCPs && (
<div
className={cn(
"flex items-center gap-4 p-4 rounded-xl border border-border transition-all",
"hover:bg-slate-400/10 dark:hover:bg-white/10"
)}
>
<div
className={cn(
"flex h-12 w-12 items-center justify-center rounded-lg border shrink-0",
"bg-slate-400/5 dark:bg-white/5 border-slate-400/5 dark:border-white/5"
)}
>
{getConnectorIcon("MCP_CONNECTOR", "size-6")}
</div>
<div className="flex-1 min-w-0">
<p className="text-[14px] font-semibold leading-tight truncate">MCPs</p>
<p className="text-[10px] text-muted-foreground mt-1">
{totalMCPServers} {totalMCPServers === 1 ? "Server" : "Servers"}
</p>
</div>
<Button
variant="secondary"
size="sm"
className="h-8 text-[11px] 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 shrink-0"
onClick={
onManage && mcpConnectors[0] ? () => onManage(mcpConnectors[0]) : undefined
}
>
Manage
</Button>
</div>
)}
{/* Non-OAuth Connectors - Individual Cards */}
{filteredNonOAuthConnectors.map((connector) => {
const isIndexing = indexingConnectorIds.has(connector.id);

View file

@ -103,14 +103,7 @@ export const AllConnectorsTab: FC<AllConnectorsTabProps> = ({
)
: [];
// 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<string | undefined>(

View file

@ -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<MCPConnectorListViewProps> = ({
mcpConnectors,
onAddNew,
onManageConnector,
onBack,
}) => {
return (
<div className="flex flex-col h-full">
{/* Header */}
<div className="flex items-center justify-between mb-6 shrink-0">
<div className="flex items-center gap-3">
<Button
variant="ghost"
size="icon"
onClick={onBack}
className="h-8 w-8"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="m15 18-6-6 6-6" />
</svg>
</Button>
<div>
<h2 className="text-lg sm:text-xl font-semibold">MCP Connectors</h2>
<p className="text-xs sm:text-sm text-muted-foreground">
Manage your Model Context Protocol servers
</p>
</div>
</div>
</div>
{/* Add New Button */}
<div className="mb-4 shrink-0">
<Button
onClick={onAddNew}
className="w-full"
variant="outline"
>
<Plus className="h-4 w-4 mr-2" />
Add New MCP Server
</Button>
</div>
{/* MCP Connectors List */}
<div className="space-y-3 flex-1 overflow-y-auto">
{mcpConnectors.length === 0 ? (
<div className="flex flex-col items-center justify-center py-12 text-center">
<div className="h-16 w-16 rounded-full bg-slate-400/5 dark:bg-white/5 flex items-center justify-center mb-4">
<Server className="h-8 w-8 text-muted-foreground" />
</div>
<h3 className="text-sm font-medium mb-1">No MCP Servers</h3>
<p className="text-xs text-muted-foreground max-w-[280px]">
Get started by adding your first Model Context Protocol server
</p>
</div>
) : (
mcpConnectors.map((connector) => {
// Extract server name from config
const serverName = connector.config?.server_config?.name || connector.name;
return (
<div
key={connector.id}
className={cn(
"flex items-center gap-4 p-4 rounded-xl border border-border transition-all",
"bg-slate-400/5 dark:bg-white/5 hover:bg-slate-400/10 dark:hover:bg-white/10"
)}
>
<div
className={cn(
"flex h-12 w-12 items-center justify-center rounded-lg border shrink-0",
"bg-slate-400/5 dark:bg-white/5 border-slate-400/5 dark:border-white/5"
)}
>
{getConnectorIcon("MCP_CONNECTOR", "size-6")}
</div>
<div className="flex-1 min-w-0">
<p className="text-[14px] font-semibold leading-tight truncate">
{serverName}
</p>
</div>
<Button
variant="secondary"
size="sm"
className="h-8 text-[11px] 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 shrink-0"
onClick={() => onManageConnector(connector)}
>
Manage
</Button>
</div>
);
})
)}
</div>
</div>
);
};

View file

@ -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(),