From 12099b4f8cd10002810438bd309e208169960107 Mon Sep 17 00:00:00 2001 From: Clawd Date: Sun, 1 Mar 2026 08:45:09 -0800 Subject: feat(settings): add MCP server configuration - Add McpSettings component with add/edit/delete server UI - Support stdio (command + args + env) and sse/http (url + headers) transports - Array builder for args, key-value builder for env vars and headers - Pass mcpServers config to SDK query() calls - Store config as JSON in settings table --- renderer/src/components/settings/McpSettings.tsx | 502 +++++++++++++++++++++++ 1 file changed, 502 insertions(+) create mode 100644 renderer/src/components/settings/McpSettings.tsx (limited to 'renderer/src/components/settings/McpSettings.tsx') diff --git a/renderer/src/components/settings/McpSettings.tsx b/renderer/src/components/settings/McpSettings.tsx new file mode 100644 index 0000000..7d23fe4 --- /dev/null +++ b/renderer/src/components/settings/McpSettings.tsx @@ -0,0 +1,502 @@ +import React, { useState, useEffect } from "react"; + +const api = window.api; + +type McpServerType = "stdio" | "sse" | "http"; + +interface McpServerConfig { + type: McpServerType; + command?: string; + args?: string[]; + env?: Record; + url?: string; + headers?: Record; +} + +type McpServersConfig = Record; + +interface EditingServer { + originalName: string | null; // null = new server + name: string; + config: McpServerConfig; +} + +const emptyConfig = (): McpServerConfig => ({ + type: "stdio", + command: "", + args: [], + env: {}, +}); + +export function McpSettings() { + const [servers, setServers] = useState(null); + const [editing, setEditing] = useState(null); + const [saveStatus, setSaveStatus] = useState<"idle" | "saved" | "error">("idle"); + const [error, setError] = useState(null); + + useEffect(() => { + api.getSettings(["mcpServers"]).then((settings) => { + try { + const parsed = settings["mcpServers"] + ? JSON.parse(settings["mcpServers"]) + : {}; + setServers(parsed); + } catch { + setServers({}); + setError("Failed to parse saved MCP config"); + } + }); + }, []); + + const saveServers = async (newServers: McpServersConfig) => { + try { + if (Object.keys(newServers).length === 0) { + await api.deleteSetting("mcpServers"); + } else { + await api.setSetting("mcpServers", JSON.stringify(newServers)); + } + setServers(newServers); + setSaveStatus("saved"); + setTimeout(() => setSaveStatus("idle"), 1500); + } catch (e) { + setSaveStatus("error"); + setError(String(e)); + } + }; + + const handleDelete = async (name: string) => { + if (!servers) return; + const { [name]: _, ...rest } = servers; + await saveServers(rest); + }; + + const handleSaveEdit = async () => { + if (!editing || !servers) return; + const { originalName, name, config } = editing; + + if (!name.trim()) { + setError("Server name is required"); + return; + } + + // Validate based on type + if (config.type === "stdio" && !config.command?.trim()) { + setError("Command is required for stdio servers"); + return; + } + if ((config.type === "sse" || config.type === "http") && !config.url?.trim()) { + setError("URL is required for SSE/HTTP servers"); + return; + } + + const newServers = { ...servers }; + + // If renaming, remove old entry + if (originalName && originalName !== name) { + delete newServers[originalName]; + } + + // Clean up config based on type + const cleanConfig: McpServerConfig = { type: config.type }; + if (config.type === "stdio") { + cleanConfig.command = config.command; + if (config.args && config.args.length > 0) { + cleanConfig.args = config.args.filter((a) => a.trim()); + } + if (config.env && Object.keys(config.env).length > 0) { + cleanConfig.env = config.env; + } + } else { + cleanConfig.url = config.url; + if (config.headers && Object.keys(config.headers).length > 0) { + cleanConfig.headers = config.headers; + } + } + + newServers[name.trim()] = cleanConfig; + await saveServers(newServers); + setEditing(null); + setError(null); + }; + + const handleCancelEdit = () => { + setEditing(null); + setError(null); + }; + + if (servers === null) { + return ( +
+ Loading... +
+ ); + } + + const serverNames = Object.keys(servers); + + return ( +
+
MCP Servers
+
+ Configure Model Context Protocol servers to give Claude access to + external data sources like Jira, documentation, or databases. Servers + are available in all phases. +
+ + {error && ( +
{error}
+ )} + + {/* Server List */} + {!editing && ( + <> +
+ {serverNames.length === 0 ? ( +
No MCP servers configured
+ ) : ( + serverNames.map((name) => ( +
+
+ {name} + {servers[name].type} + + {servers[name].type === "stdio" + ? servers[name].command + : servers[name].url} + +
+
+ + +
+
+ )) + )} +
+ +
+ + {saveStatus === "saved" && ( + saved ✓ + )} +
+ + )} + + {/* Edit Form */} + {editing && ( + + )} +
+ ); +} + +interface ServerFormProps { + editing: EditingServer; + setEditing: React.Dispatch>; + onSave: () => void; + onCancel: () => void; +} + +function ServerForm({ editing, setEditing, onSave, onCancel }: ServerFormProps) { + const { name, config } = editing; + + const updateName = (newName: string) => { + setEditing({ ...editing, name: newName }); + }; + + const updateConfig = (updates: Partial) => { + setEditing({ ...editing, config: { ...config, ...updates } }); + }; + + const updateArg = (index: number, value: string) => { + const newArgs = [...(config.args || [])]; + newArgs[index] = value; + updateConfig({ args: newArgs }); + }; + + const addArg = () => { + updateConfig({ args: [...(config.args || []), ""] }); + }; + + const removeArg = (index: number) => { + const newArgs = [...(config.args || [])]; + newArgs.splice(index, 1); + updateConfig({ args: newArgs }); + }; + + const updateEnvKey = (oldKey: string, newKey: string) => { + const env = { ...config.env }; + const value = env[oldKey] || ""; + delete env[oldKey]; + if (newKey) env[newKey] = value; + updateConfig({ env }); + }; + + const updateEnvValue = (key: string, value: string) => { + updateConfig({ env: { ...config.env, [key]: value } }); + }; + + const addEnvVar = () => { + const env = { ...config.env, "": "" }; + updateConfig({ env }); + }; + + const removeEnvVar = (key: string) => { + const env = { ...config.env }; + delete env[key]; + updateConfig({ env }); + }; + + const updateHeaderKey = (oldKey: string, newKey: string) => { + const headers = { ...config.headers }; + const value = headers[oldKey] || ""; + delete headers[oldKey]; + if (newKey) headers[newKey] = value; + updateConfig({ headers }); + }; + + const updateHeaderValue = (key: string, value: string) => { + updateConfig({ headers: { ...config.headers, [key]: value } }); + }; + + const addHeader = () => { + const headers = { ...config.headers, "": "" }; + updateConfig({ headers }); + }; + + const removeHeader = (key: string) => { + const headers = { ...config.headers }; + delete headers[key]; + updateConfig({ headers }); + }; + + return ( +
+
+ {editing.originalName ? `Edit "${editing.originalName}"` : "Add Server"} +
+ + {/* Name */} +
+ + updateName(e.target.value)} + placeholder="e.g., jira, godoc" + spellCheck={false} + /> +
+ + {/* Type */} +
+ + +
+ + {/* stdio fields */} + {config.type === "stdio" && ( + <> +
+ + updateConfig({ command: e.target.value })} + placeholder="e.g., npx, node, python" + spellCheck={false} + /> +
+ +
+ +
+ {(config.args || []).map((arg, i) => ( +
+ updateArg(i, e.target.value)} + placeholder={`Argument ${i + 1}`} + spellCheck={false} + /> + +
+ ))} + +
+
+ +
+ +
+ {Object.entries(config.env || {}).map(([key, value]) => ( +
+ updateEnvKey(key, e.target.value)} + placeholder="KEY" + spellCheck={false} + /> + = + updateEnvValue(key, e.target.value)} + placeholder="value" + spellCheck={false} + /> + +
+ ))} + +
+
+ + )} + + {/* sse/http fields */} + {(config.type === "sse" || config.type === "http") && ( + <> +
+ + updateConfig({ url: e.target.value })} + placeholder="https://..." + spellCheck={false} + /> +
+ +
+ +
+ {Object.entries(config.headers || {}).map(([key, value]) => ( +
+ updateHeaderKey(key, e.target.value)} + placeholder="Header-Name" + spellCheck={false} + /> + : + updateHeaderValue(key, e.target.value)} + placeholder="value" + spellCheck={false} + /> + +
+ ))} + +
+
+ + )} + + {/* Form Actions */} +
+ + +
+
+ ); +} -- cgit v1.2.3