diff --git a/electron/main/index.ts b/electron/main/index.ts index dd0394ed0..9d87c76ac 100644 --- a/electron/main/index.ts +++ b/electron/main/index.ts @@ -534,6 +534,15 @@ function registerIpcHandlers() { // ==================== MCP manage handler ==================== ipcMain.handle('mcp-install', async (event, name, mcp) => { + // Convert args from JSON string to array if needed + if (mcp.args && typeof mcp.args === 'string') { + try { + mcp.args = JSON.parse(mcp.args); + } catch (e) { + // If parsing fails, split by comma as fallback + mcp.args = mcp.args.split(',').map((arg: string) => arg.trim()).filter((arg: string) => arg !== ''); + } + } addMcp(name, mcp); return { success: true }; }); @@ -544,6 +553,15 @@ function registerIpcHandlers() { }); ipcMain.handle('mcp-update', async (event, name, mcp) => { + // Convert args from JSON string to array if needed + if (mcp.args && typeof mcp.args === 'string') { + try { + mcp.args = JSON.parse(mcp.args); + } catch (e) { + // If parsing fails, split by comma as fallback + mcp.args = mcp.args.split(',').map((arg: string) => arg.trim()).filter((arg: string) => arg !== ''); + } + } updateMcp(name, mcp); return { success: true }; }); diff --git a/electron/main/utils/mcpConfig.ts b/electron/main/utils/mcpConfig.ts index ca351f5c0..b3400cae9 100644 --- a/electron/main/utils/mcpConfig.ts +++ b/electron/main/utils/mcpConfig.ts @@ -8,6 +8,7 @@ const MCP_CONFIG_PATH = path.join(MCP_CONFIG_DIR, 'mcp.json'); type McpServerConfig = { command: string; args: string[]; + description?: string; env?: Record; } | { url: string; @@ -17,7 +18,7 @@ type McpServersConfig = { [name: string]: McpServerConfig; }; -type ConfigFile = { +export type ConfigFile = { mcpServers: McpServersConfig; }; @@ -42,6 +43,28 @@ export function readMcpConfig(): ConfigFile { if (!parsed.mcpServers || typeof parsed.mcpServers !== 'object') { return getDefaultConfig(); } + + // Normalize args field - ensure it's always an array + Object.keys(parsed.mcpServers).forEach(serverName => { + const server = parsed.mcpServers[serverName]; + if (server.args) { + const args = server.args as any; + if (typeof args === 'string') { + try { + // Try to parse as JSON string first + server.args = JSON.parse(args); + } catch (e) { + // If parsing fails, split by comma as fallback + server.args = args.split(',').map((arg: string) => arg.trim()).filter((arg: string) => arg !== ''); + } + } + // Ensure it's always an array of strings + if (Array.isArray(server.args)) { + server.args = server.args.map((arg: any) => String(arg)); + } + } + }); + return parsed; } catch (e) { return getDefaultConfig(); @@ -58,7 +81,22 @@ export function writeMcpConfig(config: ConfigFile): void { export function addMcp(name: string, mcp: McpServerConfig): void { const config = readMcpConfig(); if (!config.mcpServers[name]) { - config.mcpServers[name] = mcp; + // Ensure args is an array before adding + const normalizedMcp = { ...mcp }; + if ('args' in normalizedMcp && normalizedMcp.args) { + const args = normalizedMcp.args as any; + if (typeof args === 'string') { + try { + normalizedMcp.args = JSON.parse(args); + } catch (e) { + normalizedMcp.args = args.split(',').map((arg: string) => arg.trim()).filter((arg: string) => arg !== ''); + } + } + if (Array.isArray(normalizedMcp.args)) { + normalizedMcp.args = normalizedMcp.args.map((arg: any) => String(arg)); + } + } + config.mcpServers[name] = normalizedMcp; writeMcpConfig(config); } } @@ -74,6 +112,21 @@ export function removeMcp(name: string): void { export function updateMcp(name: string, mcp: McpServerConfig): void { const config = readMcpConfig(); - config.mcpServers[name] = mcp; + // Ensure args is an array before updating + const normalizedMcp = { ...mcp }; + if ('args' in normalizedMcp && normalizedMcp.args) { + const args = normalizedMcp.args as any; + if (typeof args === 'string') { + try { + normalizedMcp.args = JSON.parse(args); + } catch (e) { + normalizedMcp.args = args.split(',').map((arg: string) => arg.trim()).filter((arg: string) => arg !== ''); + } + } + if (Array.isArray(normalizedMcp.args)) { + normalizedMcp.args = normalizedMcp.args.map((arg: any) => String(arg)); + } + } + config.mcpServers[name] = normalizedMcp; writeMcpConfig(config); } \ No newline at end of file diff --git a/src/pages/Setting/MCP.tsx b/src/pages/Setting/MCP.tsx index 23bf13732..ef1eabe14 100644 --- a/src/pages/Setting/MCP.tsx +++ b/src/pages/Setting/MCP.tsx @@ -20,6 +20,7 @@ import { getProxyBaseURL } from "@/lib"; import { useAuthStore } from "@/store/authStore"; import { toast } from "sonner"; +import { ConfigFile } from "electron/main/utils/mcpConfig"; export default function SettingMCP() { const navigate = useNavigate(); @@ -195,13 +196,28 @@ export default function SettingMCP() { setSaving(true); setErrorMsg(null); try { - await proxyFetchPut(`/api/mcp/users/${showConfig.id}`, { + const mcpData = { mcp_name: configForm.mcp_name, mcp_desc: configForm.mcp_desc, command: configForm.command, args: arrayToArgsJson(configForm.argsArr), env: configForm.env, - }); + } + await proxyFetchPut(`/api/mcp/users/${showConfig.id}`, mcpData); + + if (window.ipcRenderer) { + //Partial payload to empty env {} + const payload: any = { + description: configForm.mcp_desc, + command: configForm.command, + args: arrayToArgsJson(configForm.argsArr), + }; + if (configForm.env && Object.keys(configForm.env).length > 0) { + payload.env = configForm.env; + } + window.ipcRenderer.invoke("mcp-update", mcpData.mcp_name, payload); + } + setShowConfig(null); fetchList(); } catch (err: any) { @@ -236,9 +252,27 @@ export default function SettingMCP() { setInstalling(true); try { if (addType === "local") { - let data; + let data:ConfigFile; try { data = JSON.parse(localJson); + + // validate mcpServers structure + if (!data.mcpServers || typeof data.mcpServers !== "object") { + throw new Error("Invalid mcpServers"); + } + + // check for name conflicts with existing items + const serverNames = Object.keys(data.mcpServers); + const conflict = serverNames.find((name) => + items.some((d) => d.mcp_name === name) + ); + if (conflict) { + toast.error(`MCP server "${conflict}" already exists`, { + closeButton: true, + }); + setInstalling(false); + return; + } } catch (e) { toast.error("Invalid JSON", { closeButton: true }); setInstalling(false); @@ -252,19 +286,14 @@ export default function SettingMCP() { } if (window.ipcRenderer) { const mcpServers = data["mcpServers"]; - Object.entries(mcpServers).forEach(async ([key, value]) => { + for (const [key, value] of Object.entries(mcpServers)) { await window.ipcRenderer.invoke("mcp-install", key, value); - }); + } } } setShowAdd(false); setLocalJson(`{ - "mcp_id": 0, - "mcp_name": "", - "mcp_desc": "", - "command": "", - "args": "", - "env": {} + "mcpServers": {} }`); setRemoteName(""); setRemoteUrl(""); @@ -335,13 +364,13 @@ export default function SettingMCP() { {!isLoading && !error && items.length === 0 && (
No MCP servers
)} - + />}
- onChange({ ...form, mcp_name: e.target.value })} disabled={loading} /> + onChange({ ...form, mcp_name: e.target.value })} disabled readOnly />
@@ -45,7 +45,7 @@ export default function MCPConfigDialog({ open, form, mcp, onChange, onSave, onC onChange({ ...form, command: e.target.value })} disabled={loading} />
- +