From 356f73cc01dee1a1002b26cf038f6688e76d55f5 Mon Sep 17 00:00:00 2001 From: Yuneng Jiang Date: Tue, 18 Aug 2026 22:43:02 -0700 Subject: [PATCH] refactor(ui): type the MCP form store snapshot instead of Record The ported call sites read the form store through `Record`, which reproduced antd's untyped `getFieldsValue(true)` boundary and let a misspelled key through unchecked. Replaces it with a named snapshot type covering the keys those call sites actually read. --- .../_components/CreateMCPServer.tsx | 10 +++---- .../_components/mcp_server_edit.tsx | 14 ++++++--- .../mcp-servers/_components/utils.tsx | 29 +++++++++++++++++++ 3 files changed, 44 insertions(+), 9 deletions(-) diff --git a/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/CreateMCPServer.tsx b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/CreateMCPServer.tsx index ac7960c1884..bfa546351c7 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/CreateMCPServer.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/CreateMCPServer.tsx @@ -57,7 +57,7 @@ import OpenAPIFormSection, { OpenAPIKeyTool } from "./OpenAPIFormSection"; import MCPLogoSelector from "./MCPLogoSelector"; import EnvVarsSection from "./EnvVarsSection"; import { isAdminRole } from "@/utils/roles"; -import { antdValidator, validateMCPServerUrl, validateMCPServerName } from "./utils"; +import { antdValidator, asFormSnapshot, validateMCPServerUrl, validateMCPServerName } from "./utils"; import { toast } from "@/lib/toast"; import { useMcpOAuthFlow } from "@/hooks/useMcpOAuthFlow"; import { useTestMCPConnection } from "@/hooks/useTestMCPConnection"; @@ -170,7 +170,7 @@ const CreateMCPServer: React.FC = ({ const persistCreateUiState = () => { writeCreateUiSnapshot({ modalVisible: isModalVisible, - formValues: form.getValues() as Record, + formValues: form.getValues(), transportType, costConfig, allowedTools, @@ -197,7 +197,7 @@ const CreateMCPServer: React.FC = ({ ...(dcrClientRef.current ?? {}), }), getTemporaryPayload: () => { - const values: Record = form.getValues(); + const values = asFormSnapshot(form.getValues()); const transport = values.transport || transportType; // For OpenAPI transport the form has spec_path instead of url. // We pass the spec_path as url so the temp-session endpoint has something @@ -518,7 +518,7 @@ const CreateMCPServer: React.FC = ({ if (isHeldOAuthTokenStale(form.getValues(), authorizedIdentity)) { clearHeldOAuthToken(); } - setFormValues(form.getValues() as Record); + setFormValues(form.getValues()); }; // Generate options with existing groups and potential new group @@ -616,7 +616,7 @@ const CreateMCPServer: React.FC = ({ } if (isHeldOAuthTokenStale(form.getValues(), authorizedIdentity)) { clearHeldOAuthToken(changedValues); - setFormValues(form.getValues() as Record); + setFormValues(form.getValues()); return; } setFormValues(allValues); diff --git a/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/mcp_server_edit.tsx b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/mcp_server_edit.tsx index ed708cde26c..87f64ffd59d 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/mcp_server_edit.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/mcp_server_edit.tsx @@ -53,7 +53,13 @@ import IdJagFormFields from "./IdJagFormFields"; import OAuthFormFields from "./OAuthFormFields"; import MCPLogoSelector from "./MCPLogoSelector"; import EnvVarsSection from "./EnvVarsSection"; -import { antdValidator, validateMCPServerUrl, validateMCPServerName, normalizeToolOverrideMap } from "./utils"; +import { + antdValidator, + asFormSnapshot, + validateMCPServerUrl, + validateMCPServerName, + normalizeToolOverrideMap, +} from "./utils"; import { buildEditServerPayload, editPayloadErrorMessage } from "./editServerPayload"; import { toast } from "@/lib/toast"; import { useMcpOAuthFlow } from "@/hooks/useMcpOAuthFlow"; @@ -222,7 +228,7 @@ const MCPServerEdit: React.FC = ({ return; } try { - const values: Record = form.getValues(); + const values = asFormSnapshot(form.getValues()); setSecureItem( EDIT_OAUTH_UI_STATE_KEY, JSON.stringify({ @@ -261,7 +267,7 @@ const MCPServerEdit: React.FC = ({ accessToken, getCredentials: () => form.getValues("credentials") as Record | undefined, getTemporaryPayload: () => { - const values: Record = form.getValues(); + const values = asFormSnapshot(form.getValues()); const url = values.url || mcpServer.url; const transport = values.transport || mcpServer.transport; if (!url || !transport) { @@ -535,7 +541,7 @@ const MCPServerEdit: React.FC = ({ setIsLoadingTools(true); setToolsError(null); try { - const values: Record = form.getValues(); + const values = asFormSnapshot(form.getValues()); const rawTransport = values.transport || mcpServer.transport; // oauth2_flow must be explicit: the preview endpoint infers client_credentials from the // inherited client_id/client_secret/token_url (common once DCR or discovery filled them) and diff --git a/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/utils.tsx b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/utils.tsx index 98bd7c6821f..1efcecf980a 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/utils.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/utils.tsx @@ -116,3 +116,32 @@ export const normalizeToolOverrideMap = ( } return value; }; + +export interface StaticHeaderEntry { + readonly header?: string; + readonly value?: string; +} + +export interface ServerFormSnapshot { + readonly alias?: string; + readonly args?: string; + readonly auth_type?: string; + readonly authorization_url?: string; + readonly command?: string; + readonly credentials?: Record; + readonly delegate_auth_to_upstream?: boolean; + readonly description?: string; + readonly env?: Record; + readonly issuer?: string; + readonly mcp_access_groups?: readonly string[]; + readonly oauth_flow_type?: string; + readonly registration_url?: string; + readonly server_name?: string; + readonly spec_path?: string; + readonly static_headers?: readonly StaticHeaderEntry[]; + readonly token_url?: string; + readonly transport?: string; + readonly url?: string; +} + +export const asFormSnapshot = (values: Record): ServerFormSnapshot => values as ServerFormSnapshot;