From c9cc963101307bfd123b941c5b05e31489a65499 Mon Sep 17 00:00:00 2001 From: mateo-berri <277851410+mateo-berri@users.noreply.github.com> Date: Thu, 4 Jun 2026 05:01:23 +0000 Subject: [PATCH] refactor(mcp/ui): share normalizeEnvVars helper between create and edit forms --- .../mcp_tools/create_mcp_server.tsx | 27 +------------------ .../components/mcp_tools/mcp_server_edit.tsx | 25 ++--------------- .../src/components/mcp_tools/utils.tsx | 26 ++++++++++++++++++ 3 files changed, 29 insertions(+), 49 deletions(-) diff --git a/ui/litellm-dashboard/src/components/mcp_tools/create_mcp_server.tsx b/ui/litellm-dashboard/src/components/mcp_tools/create_mcp_server.tsx index 17470331778..083a759814a 100644 --- a/ui/litellm-dashboard/src/components/mcp_tools/create_mcp_server.tsx +++ b/ui/litellm-dashboard/src/components/mcp_tools/create_mcp_server.tsx @@ -15,7 +15,7 @@ import OpenAPIFormSection, { OpenAPIKeyTool } from "./OpenAPIFormSection"; import MCPLogoSelector from "./MCPLogoSelector"; import EnvVarsSection from "./EnvVarsSection"; import { isAdminRole } from "@/utils/roles"; -import { validateMCPServerUrl, validateMCPServerName } from "./utils"; +import { validateMCPServerUrl, validateMCPServerName, normalizeEnvVars } from "./utils"; import NotificationsManager from "../molecules/notifications_manager"; import { useMcpOAuthFlow } from "@/hooks/useMcpOAuthFlow"; import { useTestMCPConnection } from "@/hooks/useTestMCPConnection"; @@ -49,31 +49,6 @@ const reduceStaticHeaders = (list: unknown): Record => { }, {}); }; -type EnvVarEntry = { name: string; value: string; scope: "global" | "user"; description?: string }; - -/** Normalize the env_vars form list into the payload shape the backend expects. - * Drops empty rows and any with invalid identifiers. */ -const normalizeEnvVars = (list: unknown): EnvVarEntry[] => { - if (!Array.isArray(list)) return []; - const seen = new Set(); - const out: EnvVarEntry[] = []; - for (const entry of list) { - if (!entry || typeof entry !== "object") continue; - const name = String((entry as Record).name ?? "").trim(); - if (!name || seen.has(name)) continue; - if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) continue; - const scope = (entry as Record).scope === "user" ? "user" : "global"; - out.push({ - name, - value: scope === "user" ? "" : String((entry as Record).value ?? ""), - scope, - description: ((entry as Record).description as string | undefined) || undefined, - }); - seen.add(name); - } - return out; -}; - const CreateMCPServer: React.FC = ({ userID, userRole, diff --git a/ui/litellm-dashboard/src/components/mcp_tools/mcp_server_edit.tsx b/ui/litellm-dashboard/src/components/mcp_tools/mcp_server_edit.tsx index ad6b804cfcc..68cba54987c 100644 --- a/ui/litellm-dashboard/src/components/mcp_tools/mcp_server_edit.tsx +++ b/ui/litellm-dashboard/src/components/mcp_tools/mcp_server_edit.tsx @@ -10,7 +10,7 @@ import MCPToolConfiguration from "./mcp_tool_configuration"; import StdioConfiguration from "./StdioConfiguration"; import MCPLogoSelector from "./MCPLogoSelector"; import EnvVarsSection from "./EnvVarsSection"; -import { validateMCPServerUrl, validateMCPServerName } from "./utils"; +import { validateMCPServerUrl, validateMCPServerName, normalizeEnvVars } from "./utils"; import NotificationsManager from "../molecules/notifications_manager"; import { useMcpOAuthFlow } from "@/hooks/useMcpOAuthFlow"; import { getSecureItem, setSecureItem } from "@/utils/secureStorage"; @@ -431,28 +431,7 @@ const MCPServerEdit: React.FC = ({ }, {}) : ({} as Record); - const envVars = Array.isArray(envVarsList) - ? envVarsList.reduce( - (acc: Array<{ name: string; value: string; scope: "global" | "user"; description?: string }>, entry: Record) => { - const name = String(entry?.name ?? "").trim(); - if (!name || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) { - return acc; - } - if (acc.some((existing) => existing.name === name)) { - return acc; - } - const scope = entry?.scope === "user" ? "user" : "global"; - acc.push({ - name, - value: scope === "user" ? "" : String(entry?.value ?? ""), - scope, - description: (entry?.description as string | undefined) || undefined, - }); - return acc; - }, - [], - ) - : []; + const envVars = normalizeEnvVars(envVarsList); const credentialsPayload = credentialValues && typeof credentialValues === "object" diff --git a/ui/litellm-dashboard/src/components/mcp_tools/utils.tsx b/ui/litellm-dashboard/src/components/mcp_tools/utils.tsx index 44a06405615..6d9479a13c3 100644 --- a/ui/litellm-dashboard/src/components/mcp_tools/utils.tsx +++ b/ui/litellm-dashboard/src/components/mcp_tools/utils.tsx @@ -1,3 +1,5 @@ +import { MCPEnvVar, MCPEnvVarScope } from "./types"; + export const extractMCPToken = (url: string): { token: string | null; baseUrl: string } => { try { const mcpIndex = url.indexOf("/mcp/"); @@ -51,3 +53,27 @@ export const validateMCPServerName = (value: string) => { ? Promise.reject("Cannot contain '-' (hyphen) or spaces. Please use '_' (underscore) instead.") : Promise.resolve(); }; + +// Normalize the env_vars form list into the payload shape the backend expects. +// Drops empty rows, invalid identifiers, and duplicate names; user-scoped entries never carry a value. +export const normalizeEnvVars = (list: unknown): MCPEnvVar[] => { + if (!Array.isArray(list)) return []; + const seen = new Set(); + const out: MCPEnvVar[] = []; + for (const entry of list) { + if (!entry || typeof entry !== "object") continue; + const record = entry as Record; + const name = String(record.name ?? "").trim(); + if (!name || seen.has(name)) continue; + if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) continue; + const scope: MCPEnvVarScope = record.scope === "user" ? "user" : "global"; + out.push({ + name, + value: scope === "user" ? "" : String(record.value ?? ""), + scope, + description: (record.description as string | undefined) || undefined, + }); + seen.add(name); + } + return out; +};