diff --git a/client/src/components/ServersTab.tsx b/client/src/components/ServersTab.tsx index 0e3a4526c..830900e6f 100644 --- a/client/src/components/ServersTab.tsx +++ b/client/src/components/ServersTab.tsx @@ -1,7 +1,7 @@ import { useState } from "react"; import { Card } from "./ui/card"; import { Button } from "./ui/button"; -import { Plus, Database,FileText } from "lucide-react"; +import { Plus, Database, FileText } from "lucide-react"; import { ServerWithName } from "@/hooks/use-app-state"; import { ServerConnectionCard } from "./connection/ServerConnectionCard"; import { ServerModal } from "./connection/ServerModal"; diff --git a/client/src/components/connection/JsonImportModal.tsx b/client/src/components/connection/JsonImportModal.tsx index dba075e6e..dbbb94801 100644 --- a/client/src/components/connection/JsonImportModal.tsx +++ b/client/src/components/connection/JsonImportModal.tsx @@ -22,7 +22,10 @@ export function JsonImportModal({ onImport, }: JsonImportModalProps) { const [jsonContent, setJsonContent] = useState(""); - const [validationResult, setValidationResult] = useState<{ success: boolean; error?: string } | null>(null); + const [validationResult, setValidationResult] = useState<{ + success: boolean; + error?: string; + } | null>(null); const [isImporting, setIsImporting] = useState(false); const fileInputRef = useRef(null); @@ -74,10 +77,13 @@ export function JsonImportModal({ } onImport(servers); - toast.success(`Successfully imported ${servers.length} server${servers.length === 1 ? '' : 's'}`); + toast.success( + `Successfully imported ${servers.length} server${servers.length === 1 ? "" : "s"}`, + ); handleClose(); } catch (error) { - const errorMessage = error instanceof Error ? error.message : "Unknown error"; + const errorMessage = + error instanceof Error ? error.message : "Unknown error"; toast.error(`Failed to import servers: ${errorMessage}`); } finally { setIsImporting(false); @@ -91,7 +97,6 @@ export function JsonImportModal({ onClose(); }; - const getValidationIcon = () => { if (!validationResult) return null; return validationResult.success ? ( @@ -104,9 +109,13 @@ export function JsonImportModal({ const getValidationMessage = () => { if (!validationResult) return null; return validationResult.success ? ( - Valid JSON config + + Valid JSON config + ) : ( - {validationResult.error} + + {validationResult.error} + ); }; @@ -115,7 +124,8 @@ export function JsonImportModal({ - MCP Import Servers from JSON + MCP Import Servers + from JSON @@ -167,7 +177,7 @@ export function JsonImportModal({

Example JSON Format:

-{`{
+                {`{
   "mcpServers": {
     "weather": {
       "command": "/path/to/node",
@@ -187,9 +197,7 @@ export function JsonImportModal({
           {validationResult && !validationResult.success && (
             
               
-              
-                {validationResult.error}
-              
+              {validationResult.error}
             
           )}
         
diff --git a/client/src/hooks/use-app-state.ts b/client/src/hooks/use-app-state.ts index 9a389db57..dd2680cdf 100644 --- a/client/src/hooks/use-app-state.ts +++ b/client/src/hooks/use-app-state.ts @@ -314,26 +314,96 @@ export function useAppState() { [appState.servers, logger], ); - // Auto-connect to CLI-provided MCP server on mount + // CLI config processing guard + const cliConfigProcessedRef = useRef(false); + + // Auto-connect to CLI-provided MCP server(s) on mount useEffect(() => { - if (!isLoading) { - const windowCliConfig = (window as any).MCP_CLI_CONFIG; - if (windowCliConfig && windowCliConfig.command) { - logger.info( - "Auto-connecting to CLI-provided MCP server (from window)", - { cliConfig: windowCliConfig }, - ); - const formData: ServerFormData = { - name: windowCliConfig.name || "CLI Server", - type: "stdio" as const, - command: windowCliConfig.command, - args: windowCliConfig.args || [], - }; - handleConnect(formData); - return; - } + if (!isLoading && !cliConfigProcessedRef.current) { + cliConfigProcessedRef.current = true; + // Fetch CLI config from API (both dev and production) + fetch("/api/mcp-cli-config") + .then((response) => response.json()) + .then((data) => { + const cliConfig = data.config; + if (cliConfig) { + // Handle multiple servers from config file + if (cliConfig.servers && Array.isArray(cliConfig.servers)) { + const autoConnectServer = cliConfig.autoConnectServer; + + logger.info( + "Processing CLI-provided MCP servers (from config file)", + { + serverCount: cliConfig.servers.length, + autoConnectServer: autoConnectServer || "all", + cliConfig: cliConfig, + }, + ); + + // Add all servers to the UI, but only auto-connect to filtered ones + cliConfig.servers.forEach((server: any) => { + const formData: ServerFormData = { + name: server.name || "CLI Server", + type: (server.type === "sse" + ? "http" + : server.type || "stdio") as "stdio" | "http", + command: server.command, + args: server.args || [], + url: server.url, + env: server.env || {}, + }; + + // Always add/update server from CLI config + const mcpConfig = toMCPConfig(formData); + dispatch({ + type: "UPSERT_SERVER", + name: formData.name, + server: { + name: formData.name, + config: mcpConfig, + lastConnectionTime: new Date(), + connectionStatus: "disconnected" as const, + retryCount: 0, + enabled: false, // Start disabled, will enable on successful connection + }, + }); + + // Only auto-connect if matches filter (or no filter) + if (!autoConnectServer || server.name === autoConnectServer) { + logger.info("Auto-connecting to server", { + serverName: server.name, + }); + handleConnect(formData); + } else { + logger.info("Skipping auto-connect for server", { + serverName: server.name, + reason: "filtered out", + }); + } + }); + return; + } + // Handle legacy single server mode + if (cliConfig.command) { + logger.info("Auto-connecting to CLI-provided MCP server", { + cliConfig, + }); + const formData: ServerFormData = { + name: cliConfig.name || "CLI Server", + type: "stdio" as const, + command: cliConfig.command, + args: cliConfig.args || [], + env: cliConfig.env || {}, + }; + handleConnect(formData); + } + } + }) + .catch((error) => { + logger.debug("Could not fetch CLI config from API", { error }); + }); } - }, [isLoading, handleConnect, logger]); + }, [isLoading, handleConnect, logger, appState.servers]); const getValidAccessToken = useCallback( async (serverName: string): Promise => { diff --git a/client/src/lib/json-config-parser.ts b/client/src/lib/json-config-parser.ts index 20f7bdf1d..93ffc3015 100644 --- a/client/src/lib/json-config-parser.ts +++ b/client/src/lib/json-config-parser.ts @@ -20,21 +20,25 @@ export interface JsonConfig { export function parseJsonConfig(jsonContent: string): ServerFormData[] { try { const config: JsonConfig = JSON.parse(jsonContent); - - if (!config.mcpServers || typeof config.mcpServers !== 'object') { - throw new Error('Invalid JSON config: missing or invalid "mcpServers" property'); + + if (!config.mcpServers || typeof config.mcpServers !== "object") { + throw new Error( + 'Invalid JSON config: missing or invalid "mcpServers" property', + ); } const servers: ServerFormData[] = []; - for (const [serverName, serverConfig] of Object.entries(config.mcpServers)) { - if (!serverConfig || typeof serverConfig !== 'object') { + for (const [serverName, serverConfig] of Object.entries( + config.mcpServers, + )) { + if (!serverConfig || typeof serverConfig !== "object") { console.warn(`Skipping invalid server config for "${serverName}"`); continue; } // Determine server type based on config - if (serverConfig.type === 'sse' || serverConfig.url) { + if (serverConfig.type === "sse" || serverConfig.url) { // HTTP/SSE server servers.push({ name: serverName, @@ -54,7 +58,9 @@ export function parseJsonConfig(jsonContent: string): ServerFormData[] { env: serverConfig.env || {}, }); } else { - console.warn(`Skipping server "${serverName}": missing required command`); + console.warn( + `Skipping server "${serverName}": missing required command`, + ); continue; } } @@ -62,7 +68,7 @@ export function parseJsonConfig(jsonContent: string): ServerFormData[] { return servers; } catch (error) { if (error instanceof SyntaxError) { - throw new Error('Invalid JSON format: ' + error.message); + throw new Error("Invalid JSON format: " + error.message); } throw error; } @@ -73,41 +79,56 @@ export function parseJsonConfig(jsonContent: string): ServerFormData[] { * @param jsonContent - The JSON string content * @returns Validation result with success status and error message */ -export function validateJsonConfig(jsonContent: string): { success: boolean; error?: string } { +export function validateJsonConfig(jsonContent: string): { + success: boolean; + error?: string; +} { try { const config = JSON.parse(jsonContent); - - if (!config.mcpServers || typeof config.mcpServers !== 'object') { - return { success: false, error: 'Missing or invalid "mcpServers" property' }; + + if (!config.mcpServers || typeof config.mcpServers !== "object") { + return { + success: false, + error: 'Missing or invalid "mcpServers" property', + }; } const serverNames = Object.keys(config.mcpServers); if (serverNames.length === 0) { - return { success: false, error: 'No servers found in "mcpServers" object' }; + return { + success: false, + error: 'No servers found in "mcpServers" object', + }; } // Validate each server config - for (const [serverName, serverConfig] of Object.entries(config.mcpServers)) { - if (!serverConfig || typeof serverConfig !== 'object') { - return { success: false, error: `Invalid server config for "${serverName}"` }; + for (const [serverName, serverConfig] of Object.entries( + config.mcpServers, + )) { + if (!serverConfig || typeof serverConfig !== "object") { + return { + success: false, + error: `Invalid server config for "${serverName}"`, + }; } const configObj = serverConfig as JsonServerConfig; - const hasCommand = configObj.command && typeof configObj.command === 'string'; - const hasUrl = configObj.url && typeof configObj.url === 'string'; - const isSse = configObj.type === 'sse'; + const hasCommand = + configObj.command && typeof configObj.command === "string"; + const hasUrl = configObj.url && typeof configObj.url === "string"; + const isSse = configObj.type === "sse"; if (!hasCommand && !hasUrl && !isSse) { - return { - success: false, - error: `Server "${serverName}" must have either "command" or "url" property` + return { + success: false, + error: `Server "${serverName}" must have either "command" or "url" property`, }; } if (hasCommand && hasUrl) { - return { - success: false, - error: `Server "${serverName}" cannot have both "command" and "url" properties` + return { + success: false, + error: `Server "${serverName}" cannot have both "command" and "url" properties`, }; } } @@ -115,9 +136,11 @@ export function validateJsonConfig(jsonContent: string): { success: boolean; err return { success: true }; } catch (error) { if (error instanceof SyntaxError) { - return { success: false, error: 'Invalid JSON format: ' + error.message }; + return { success: false, error: "Invalid JSON format: " + error.message }; } - return { success: false, error: 'Unknown error: ' + (error as Error).message }; + return { + success: false, + error: "Unknown error: " + (error as Error).message, + }; } } - diff --git a/server/index.ts b/server/index.ts index f04bdd0f3..82b87794b 100644 --- a/server/index.ts +++ b/server/index.ts @@ -66,9 +66,11 @@ function getMCPConfigFromEnv() { const servers = Object.entries(config.mcpServers).map( ([name, serverConfig]: [string, any]) => ({ name, + type: serverConfig.type || "stdio", // Default to stdio if not specified command: serverConfig.command, args: serverConfig.args || [], env: serverConfig.env || {}, + url: serverConfig.url, // For SSE/HTTP connections }), ); console.log("Transformed servers:", servers);