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 +++++++++++++++++++++++ renderer/src/styles/globals.css | 212 ++++++++++ 3 files changed, 725 insertions(+), 1 deletion(-) create mode 100644 renderer/src/components/settings/McpSettings.tsx (limited to 'renderer') 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 */} +
+ + +
+
+ ); +} diff --git a/renderer/src/styles/globals.css b/renderer/src/styles/globals.css index 0db62c2..6cf3dc7 100644 --- a/renderer/src/styles/globals.css +++ b/renderer/src/styles/globals.css @@ -1136,3 +1136,215 @@ html[data-theme="light"] .settings-textarea:focus { .cm-scroller::-webkit-scrollbar-thumb:hover { background: var(--border); } + +/* ── MCP Settings ────────────────────────────────────────────── */ +.mcp-server-list { + display: flex; + flex-direction: column; + gap: 8px; + margin-top: 16px; +} + +.mcp-empty { + color: var(--text-secondary); + font-size: 12px; + font-style: italic; + padding: 12px; + text-align: center; + border: 1px dashed var(--border); + border-radius: 4px; +} + +.mcp-server-card { + display: flex; + align-items: center; + justify-content: space-between; + padding: 10px 12px; + background: var(--bg-secondary); + border: 1px solid var(--border); + border-radius: 4px; +} + +.mcp-server-info { + display: flex; + align-items: center; + gap: 10px; + min-width: 0; + flex: 1; +} + +.mcp-server-name { + font-weight: 600; + font-size: 13px; + color: var(--text-primary); +} + +.mcp-server-type { + font-size: 10px; + text-transform: uppercase; + letter-spacing: 0.05em; + padding: 2px 6px; + background: var(--bg-tertiary); + border-radius: 3px; + color: var(--text-secondary); +} + +.mcp-server-detail { + font-size: 11px; + color: var(--text-secondary); + font-family: var(--font-mono, monospace); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + flex: 1; + min-width: 0; +} + +.mcp-server-actions { + display: flex; + gap: 6px; + flex-shrink: 0; +} + +.mcp-error { + padding: 8px 12px; + background: rgba(239, 68, 68, 0.1); + border: 1px solid rgba(239, 68, 68, 0.3); + border-radius: 4px; + color: #ef4444; + font-size: 12px; + margin-top: 12px; +} + +.mcp-server-form { + margin-top: 16px; + padding: 16px; + background: var(--bg-secondary); + border: 1px solid var(--border); + border-radius: 6px; +} + +.mcp-form-title { + font-size: 14px; + font-weight: 600; + color: var(--text-primary); + margin-bottom: 16px; +} + +.mcp-form-field { + margin-bottom: 14px; +} + +.mcp-form-field label { + display: block; + font-size: 11px; + font-weight: 500; + text-transform: uppercase; + letter-spacing: 0.05em; + color: var(--text-secondary); + margin-bottom: 6px; +} + +.mcp-form-field input, +.mcp-form-field select { + width: 100%; + padding: 8px 10px; + background: var(--bg-primary); + border: 1px solid var(--border); + border-radius: 4px; + color: var(--text-primary); + font-size: 13px; + font-family: var(--font-mono, monospace); + box-sizing: border-box; +} + +.mcp-form-field input:focus, +.mcp-form-field select:focus { + outline: none; + border-color: var(--accent); +} + +.mcp-form-field select { + cursor: pointer; +} + +.mcp-array-builder, +.mcp-kv-builder { + display: flex; + flex-direction: column; + gap: 6px; +} + +.mcp-array-row { + display: flex; + gap: 6px; +} + +.mcp-array-row input { + flex: 1; +} + +.mcp-kv-row { + display: flex; + align-items: center; + gap: 6px; +} + +.mcp-kv-row input:first-child { + flex: 0 0 35%; +} + +.mcp-kv-row input:nth-child(3) { + flex: 1; +} + +.mcp-kv-sep { + color: var(--text-secondary); + font-family: var(--font-mono, monospace); + font-size: 13px; +} + +.btn-icon { + width: 28px; + height: 28px; + padding: 0; + display: flex; + align-items: center; + justify-content: center; + background: transparent; + border: 1px solid var(--border); + border-radius: 4px; + color: var(--text-secondary); + cursor: pointer; + font-size: 16px; + flex-shrink: 0; +} + +.btn-icon:hover { + background: var(--bg-tertiary); + color: var(--text-primary); +} + +.btn-small { + padding: 4px 8px; + font-size: 11px; +} + +.btn-danger { + color: #ef4444; + border-color: rgba(239, 68, 68, 0.3); +} + +.btn-danger:hover { + background: rgba(239, 68, 68, 0.1); + color: #ef4444; +} + +.mcp-form-actions { + display: flex; + justify-content: flex-end; + gap: 8px; + margin-top: 20px; + padding-top: 16px; + border-top: 1px solid var(--border); +} -- cgit v1.2.3