diff --git a/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/IdJagFormFields.tsx b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/IdJagFormFields.tsx index 742683eba74..265150e52f5 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/IdJagFormFields.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/IdJagFormFields.tsx @@ -20,9 +20,11 @@ const FieldLabel: React.FC<{ label: string; tooltip: string }> = ({ label, toolt const IdJagFormFields: React.FC = ({ isEditing = false }) => { const placeholderSuffix = isEditing ? " (leave blank to keep existing)" : ""; const form = Form.useFormInstance(); - const clientAuthMethod = - (Form.useWatch("id_jag_client_auth_method", form) as "client_secret" | "private_key_jwt" | undefined) ?? - "client_secret"; + const watchedMethod = Form.useWatch("id_jag_client_auth_method", form) as + | "client_secret" + | "private_key_jwt" + | undefined; + const clientAuthMethod = watchedMethod ?? (isEditing ? undefined : "client_secret"); return ( <> @@ -72,12 +74,14 @@ const IdJagFormFields: React.FC = ({ isEditing = false }) /> } name="id_jag_client_auth_method" - initialValue="client_secret" + {...(isEditing ? {} : { initialValue: "client_secret" })} preserve={false} > className="rounded-lg" size="large" + allowClear={isEditing} + placeholder={isEditing ? "Keep existing method" : undefined} options={[ { value: "client_secret", label: Client Secret }, { value: "private_key_jwt", label: Private Key JWT }, diff --git a/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/create_mcp_server.tsx b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/create_mcp_server.tsx index ae13cb6b60b..e0ae36e63e4 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/create_mcp_server.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/create_mcp_server.tsx @@ -582,9 +582,9 @@ const CreateMCPServer: React.FC = ({ // The selected ID-JAG client-auth method is authoritative: null the other method's // fields so the backend's credentials merge cannot keep a stale method alive. - if (restValues.auth_type === AUTH_TYPE.OAUTH2_ID_JAG) { + if (restValues.auth_type === AUTH_TYPE.OAUTH2_ID_JAG && idJagClientAuthMethodRaw) { const idJagMethodNulls = - (idJagClientAuthMethodRaw ?? "client_secret") === "private_key_jwt" + idJagClientAuthMethodRaw === "private_key_jwt" ? { client_secret: null } : { client_private_key: null, client_private_key_id: null, client_assertion_signing_alg: null }; payload.credentials = { ...(payload.credentials ?? {}), ...idJagMethodNulls }; diff --git a/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/mcp_server_edit.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/mcp_server_edit.test.tsx index 7206f0b0774..edfa840414e 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/mcp_server_edit.test.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/mcp_server_edit.test.tsx @@ -393,6 +393,48 @@ describe("MCPServerEdit (auth type switch)", () => { vi.clearAllMocks(); }); + it("keeps the stored ID-JAG client-auth method when saved without expressing a choice", async () => { + vi.mocked(networking.updateMCPServer).mockResolvedValue({ + ...interactiveOAuthServer, + auth_type: "oauth2_id_jag", + }); + + render( + , + ); + + await waitFor(() => { + expect(screen.getByPlaceholderText("https://your-org.okta.com/oauth2/v1/token")).toBeInTheDocument(); + }); + + const saveButtons = screen.getAllByRole("button", { name: "Save Changes" }); + await act(async () => { + fireEvent.click(saveButtons[0]); + }); + + await waitFor(() => { + expect(networking.updateMCPServer).toHaveBeenCalledTimes(1); + }); + + const [, payload] = vi.mocked(networking.updateMCPServer).mock.calls[0]; + expect(payload.auth_type).toBe("oauth2_id_jag"); + const credentials = payload.credentials ?? {}; + expect(credentials.client_private_key).toBeUndefined(); + expect(credentials.client_private_key_id).toBeUndefined(); + expect(credentials.client_assertion_signing_alg).toBeUndefined(); + expect(credentials.client_secret).toBeUndefined(); + }); + it("renders the ID-JAG arm on edit and nulls its shared fields when switching away", async () => { vi.mocked(networking.updateMCPServer).mockResolvedValue({ ...interactiveOAuthServer, 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 32cf22f163b..626d585ee22 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 @@ -952,9 +952,9 @@ const MCPServerEdit: React.FC = ({ // The selected ID-JAG client-auth method is authoritative: explicit-null the other // method's stored fields so the backend's credentials merge cannot keep it alive. - if (restValues.auth_type === AUTH_TYPE.OAUTH2_ID_JAG) { + if (restValues.auth_type === AUTH_TYPE.OAUTH2_ID_JAG && idJagClientAuthMethodRaw) { const idJagMethodNulls = - (idJagClientAuthMethodRaw ?? "client_secret") === "private_key_jwt" + idJagClientAuthMethodRaw === "private_key_jwt" ? { client_secret: null } : { client_private_key: null, client_private_key_id: null, client_assertion_signing_alg: null }; payload.credentials = { ...(payload.credentials ?? {}), ...idJagMethodNulls };