diff --git a/src/lib/tauriCommands.ts b/src/lib/tauriCommands.ts index 6c03d976..cdac92d9 100644 --- a/src/lib/tauriCommands.ts +++ b/src/lib/tauriCommands.ts @@ -562,6 +562,7 @@ export interface CreateMcpServerRequest { auth_type: "none" | "api_key" | "bearer" | "oauth2"; auth_value?: string; enabled: boolean; + env_config?: string; } export interface UpdateMcpServerRequest { @@ -572,6 +573,7 @@ export interface UpdateMcpServerRequest { auth_type?: "none" | "api_key" | "bearer" | "oauth2"; auth_value?: string; enabled?: boolean; + env_config?: string; } // ─── MCP Commands ───────────────────────────────────────────────────────────── diff --git a/src/pages/Settings/MCPServers.tsx b/src/pages/Settings/MCPServers.tsx index 19d3c24a..36a9f0c3 100644 --- a/src/pages/Settings/MCPServers.tsx +++ b/src/pages/Settings/MCPServers.tsx @@ -54,6 +54,42 @@ function parseTransportConfig(config: string): { command: string; args: string[] } } +function parseEnvVars(input: string): Record { + const result: Record = {}; + const pairs = input.trim().split(/\s+/).filter(Boolean); + for (const pair of pairs) { + const [key, ...valueParts] = pair.split("="); + if (key) { + result[key] = valueParts.join("=") || ""; + } + } + return result; +} + +function formatEnvVars(obj: Record): string { + return Object.entries(obj) + .map(([k, v]) => `${k}=${v}`) + .join(" "); +} + +function parseHeaders(input: string): Record { + const result: Record = {}; + const pairs = input.trim().split(/\s+/).filter(Boolean); + for (const pair of pairs) { + const [key, ...valueParts] = pair.split(":"); + if (key) { + result[key] = valueParts.join(":") || ""; + } + } + return result; +} + +function formatHeaders(obj: Record): string { + return Object.entries(obj) + .map(([k, v]) => `${k}:${v}`) + .join(" "); +} + type StatusKey = McpServerStatus["status"]; const statusColors: Record = { @@ -72,6 +108,9 @@ interface ServerForm { auth_type: "none" | "api_key" | "bearer" | "oauth2"; auth_value: string; enabled: boolean; + plaintext_env: string; + encrypted_env: string; + http_headers: string; } const emptyForm: ServerForm = { @@ -83,6 +122,9 @@ const emptyForm: ServerForm = { auth_type: "none", auth_value: "", enabled: true, + plaintext_env: "", + encrypted_env: "", + http_headers: "", }; export default function MCPServers() { @@ -155,6 +197,21 @@ export default function MCPServers() { const startEdit = (server: McpServer) => { const parsed = parseTransportConfig(server.transport_config); + + // Parse plaintext env from transport_config.env + let plaintextEnv = ""; + let httpHeaders = ""; + try { + const config = JSON.parse(server.transport_config); + if (server.transport_type === "stdio" && config.env) { + plaintextEnv = formatEnvVars(config.env); + } else if (server.transport_type === "http" && config.headers) { + httpHeaders = formatHeaders(config.headers); + } + } catch { + // Invalid JSON, ignore + } + setForm({ name: server.name, url: server.url, @@ -164,6 +221,9 @@ export default function MCPServers() { auth_type: server.auth_type, auth_value: "", enabled: server.enabled, + plaintext_env: plaintextEnv, + encrypted_env: "", // Never populate (security: don't show encrypted values) + http_headers: httpHeaders, }); setEditServer(server); setIsAdding(true); @@ -180,10 +240,25 @@ export default function MCPServers() { if (form.transport_type === "http" && !form.url) return; if (form.transport_type === "stdio" && !form.command) return; + // Build transport_config with env vars or headers + const plaintextEnvObj = parseEnvVars(form.plaintext_env); + const httpHeadersObj = parseHeaders(form.http_headers); + const transportConfig = form.transport_type === "stdio" - ? JSON.stringify({ command: form.command, args: form.args.split(/\s+/).filter(Boolean) }) - : "{}"; + ? JSON.stringify({ + command: form.command, + args: form.args.split(/\s+/).filter(Boolean), + env: plaintextEnvObj, + }) + : JSON.stringify({ + headers: httpHeadersObj, + }); + + // Build env_config (encrypted env) as JSON string + const encryptedEnvObj = parseEnvVars(form.encrypted_env); + const envConfig = + Object.keys(encryptedEnvObj).length > 0 ? JSON.stringify(encryptedEnvObj) : undefined; const url = form.transport_type === "http" ? form.url : ""; @@ -196,6 +271,7 @@ export default function MCPServers() { transport_config: transportConfig, auth_type: form.auth_type, enabled: form.enabled, + env_config: envConfig, }; if (form.auth_value) { request.auth_value = form.auth_value; @@ -210,6 +286,7 @@ export default function MCPServers() { auth_type: form.auth_type, auth_value: form.auth_value || undefined, enabled: form.enabled, + env_config: envConfig, }; await createMcpServerCmd(request); } @@ -475,6 +552,62 @@ export default function MCPServers() { )} + {form.transport_type === "stdio" && ( + <> + +
+
+ +

+ Space-separated KEY=value pairs for non-sensitive values (e.g., DEBUG=1 LOG_LEVEL=info) +

+ setForm({ ...form, plaintext_env: e.target.value })} + placeholder="KEY1=value1 KEY2=value2" + /> +
+ +
+ +

+ For sensitive values like API keys. Space-separated KEY=value pairs. +

+ setForm({ ...form, encrypted_env: e.target.value })} + placeholder="API_KEY=secret TOKEN=xyz" + /> + {editServer && ( +

+ Leave blank to keep existing encrypted values +

+ )} +
+
+ + )} + + {form.transport_type === "http" && ( + <> + +
+ +

+ Space-separated KEY:value pairs for custom HTTP headers (e.g., X-API-Key:secret X-Custom:value) +

+ setForm({ ...form, http_headers: e.target.value })} + placeholder="X-API-Key:secret X-Custom-Header:value" + /> +
+ + )} +