mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-06-08 20:25:19 +02:00
change the flow of mcp connectors
This commit is contained in:
parent
ab0582cb1d
commit
4c07c0fe4f
12 changed files with 492 additions and 522 deletions
|
|
@ -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}")
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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) => (
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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>(
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
@ -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(),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue