import React, { useState, useEffect } from "react"; const api = window.api; type McpServerType = "stdio" | "sse" | "http"; interface McpTool { name: string; description?: string; } interface McpServerConfig { type: McpServerType; command?: string; args?: string[]; env?: Record; url?: string; headers?: Record; // Tool management discoveredTools?: McpTool[]; enabledTools?: string[]; // If undefined/empty after discovery, all tools enabled } 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); const [discovering, setDiscovering] = useState(null); // server name being discovered 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 handleDiscoverTools = async (name: string) => { if (!servers) return; const config = servers[name]; setDiscovering(name); setError(null); try { const result = await api.discoverMcpTools({ type: config.type, command: config.command, args: config.args, env: config.env, url: config.url, }); if (result.error) { setError(`Discovery failed: ${result.error}`); } else { // Save discovered tools, enable all by default const newServers = { ...servers, [name]: { ...config, discoveredTools: result.tools, enabledTools: result.tools.map((t) => t.name), }, }; await saveServers(newServers); } } catch (e) { setError(`Discovery failed: ${String(e)}`); } finally { setDiscovering(null); } }; const handleToggleTool = async (serverName: string, toolName: string, enabled: boolean) => { if (!servers) return; const config = servers[serverName]; const currentEnabled = config.enabledTools || config.discoveredTools?.map((t) => t.name) || []; const newEnabled = enabled ? [...currentEnabled, toolName] : currentEnabled.filter((t) => t !== toolName); const newServers = { ...servers, [serverName]: { ...config, enabledTools: newEnabled, }, }; await saveServers(newServers); }; 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; } } // Preserve discovered tools if config didn't change substantially const oldConfig = originalName ? servers[originalName] : null; if (oldConfig?.discoveredTools) { const configChanged = config.type !== oldConfig.type || config.command !== oldConfig.command || JSON.stringify(config.args) !== JSON.stringify(oldConfig.args) || config.url !== oldConfig.url; if (!configChanged) { cleanConfig.discoveredTools = oldConfig.discoveredTools; cleanConfig.enabledTools = oldConfig.enabledTools; } } 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. Click "Discover Tools" to fetch available tools and select which ones to enable.
{error &&
{error}
} {/* Server List */} {!editing && ( <>
{serverNames.length === 0 ? (
No MCP servers configured
) : ( serverNames.map((name) => { const config = servers[name]; const hasTools = config.discoveredTools && config.discoveredTools.length > 0; const enabledTools = config.enabledTools || config.discoveredTools?.map((t) => t.name) || []; return (
{name} {config.type} {config.type === "stdio" ? config.command : config.url}
{/* Tools Section */} {hasTools && (
Tools ({enabledTools.length}/{config.discoveredTools!.length} enabled)
{config.discoveredTools!.map((tool) => { const isEnabled = enabledTools.includes(tool.name); return ( ); })}
)} {!hasTools && (
Click "Discover Tools" to see available tools
)}
); }) )}
{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 */}
); }