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/SettingsPage.tsx | 12 +- renderer/src/components/settings/McpSettings.tsx | 502 +++++++++++++++++++++++ 2 files changed, 513 insertions(+), 1 deletion(-) create mode 100644 renderer/src/components/settings/McpSettings.tsx (limited to 'renderer/src/components') diff --git a/renderer/src/components/SettingsPage.tsx b/renderer/src/components/SettingsPage.tsx index d3ff4bf..7d06547 100644 --- a/renderer/src/components/SettingsPage.tsx +++ b/renderer/src/components/SettingsPage.tsx @@ -2,8 +2,9 @@ import React, { useState } from "react"; import { SystemPromptsSettings } from "./settings/SystemPromptsSettings"; import { GitSettings } from "./settings/GitSettings"; import { ModelSettings } from "./settings/ModelSettings"; +import { McpSettings } from "./settings/McpSettings"; -type SettingsSection = "model" | "system-prompts" | "git"; +type SettingsSection = "model" | "mcp" | "system-prompts" | "git"; interface SettingsPageProps { onClose: () => void; @@ -40,6 +41,14 @@ export function SettingsPage({ onClose }: SettingsPageProps) { > Model + + + + + )) + )} + + +
+ + {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