diff --git a/ui/litellm-dashboard/src/components/mcp_tools/mcp_server_edit.test.tsx b/ui/litellm-dashboard/src/components/mcp_tools/mcp_server_edit.test.tsx
index 758d8a80251..cbd18d145ce 100644
--- a/ui/litellm-dashboard/src/components/mcp_tools/mcp_server_edit.test.tsx
+++ b/ui/litellm-dashboard/src/components/mcp_tools/mcp_server_edit.test.tsx
@@ -1582,6 +1582,55 @@ describe("MCPServerEdit (OAuth token persistence on save)", () => {
expect(document.body.innerHTML).not.toContain("leftover-token");
});
+ it("resets the remove-app checkbox on a server switch so it never deletes the next server's stored app", async () => {
+ vi.mocked(networking.updateMCPServer).mockResolvedValue({
+ ...interactiveOAuthServer,
+ auth_type: "true_passthrough",
+ });
+
+ const { rerender } = render(
+ ,
+ );
+
+ // Check "remove saved app" on server A.
+ fireEvent.click(screen.getByRole("checkbox", { name: /Remove the saved OAuth app on save/ }));
+ expect(
+ (screen.getByRole("checkbox", { name: /Remove the saved OAuth app on save/ }) as HTMLInputElement).checked,
+ ).toBe(true);
+
+ // Switch the panel to server B without unmounting.
+ rerender(
+ ,
+ );
+
+ // The checkbox must have reset, so saving server B does not send the explicit-null delete write.
+ expect(
+ (screen.getByRole("checkbox", { name: /Remove the saved OAuth app on save/ }) as HTMLInputElement).checked,
+ ).toBe(false);
+
+ await act(async () => {
+ fireEvent.click(screen.getAllByRole("button", { name: "Save Changes" })[0]);
+ });
+
+ await waitFor(() => expect(networking.updateMCPServer).toHaveBeenCalledTimes(1));
+ const [, payload] = vi.mocked(networking.updateMCPServer).mock.calls[0];
+ expect(payload.credentials).not.toEqual({ client_id: null, client_secret: null });
+ });
+
it("forwards a newly authorized browser-held token for tool loading before the form is saved", async () => {
// Regression: fetchTools keyed the browser-held decision off the saved mcpServer.auth_type, so
// after switching the form to true_passthrough and authorizing, the fresh token was not sent as
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 e8af0b3be37..4ff9723f65a 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
@@ -309,7 +309,11 @@ const MCPServerEdit: React.FC = ({
}
syncedServerIdRef.current = mcpServer.server_id;
form.setFieldsValue(initialValues);
+ // Reset per-server OAuth UI state so it never carries across a server switch without an unmount: a
+ // stale removeStoredApp would send an explicit-null credential write that deletes the new server's
+ // stored app, and a stale warning would show on a server whose upstream did not change.
setAppMayNotMatchUpstream(false);
+ setRemoveStoredApp(false);
}, [mcpServer.server_id, initialValues, form]);
// Initialize cost config from existing server data