diff --git a/ui/litellm-dashboard/src/components/mcp_tools/create_mcp_server.test.tsx b/ui/litellm-dashboard/src/components/mcp_tools/create_mcp_server.test.tsx
index 1245bcee3fa..672644b685c 100644
--- a/ui/litellm-dashboard/src/components/mcp_tools/create_mcp_server.test.tsx
+++ b/ui/litellm-dashboard/src/components/mcp_tools/create_mcp_server.test.tsx
@@ -937,6 +937,22 @@ describe("CreateMCPServer", () => {
const reopenedUrlInput = screen.getByPlaceholderText("https://your-mcp-server.com") as HTMLInputElement;
expect(reopenedUrlInput.value).toBe("");
});
+
+ it("does not reset an in-flight OAuth resume when mounted with the modal closed (post-redirect restore)", () => {
+ // After the "Authorize & Fetch Token" redirect the page reloads and this
+ // component mounts with isModalVisible=false while useMcpOAuthFlow is still
+ // exchanging the authorization code. Calling reset() during that mount bumps
+ // the hook's reset version and the fetched token is silently discarded, so
+ // the user sees no Connection Status / Tool Configuration and must authorize
+ // again after saving.
+ const { rerender } = render();
+ expect(oauthHook.reset).not.toHaveBeenCalled();
+
+ // A real open -> closed transition must still reset (the #30000 leak fix).
+ rerender();
+ rerender();
+ expect(oauthHook.reset).toHaveBeenCalled();
+ });
});
describe("when stdio transport is selected", () => {
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 05a0696674a..725a3f1534f 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
@@ -603,9 +603,15 @@ const CreateMCPServer: React.FC = ({
// Clear form, tools, and OAuth state when the modal closes so a previous server's
// authorization, credentials, or tool list never bleed into the next "Add New MCP
// Server" session, including when a parent dismisses the modal without routing
- // through handleCancel or handleCreate.
+ // through handleCancel or handleCreate. Only a real open -> closed transition may
+ // trigger this: on the post-OAuth-redirect remount the modal starts closed while
+ // resumeOAuthFlow's token exchange is in flight, and resetting then discards the
+ // fetched token.
+ const wasModalVisibleRef = React.useRef(isModalVisible);
React.useEffect(() => {
- if (!isModalVisible) {
+ const wasVisible = wasModalVisibleRef.current;
+ wasModalVisibleRef.current = isModalVisible;
+ if (!isModalVisible && wasVisible) {
form.resetFields();
setFormValues({});
setOauthAccessToken(null);