fix(frontend/mcp): Robust OAuth callback with localStorage fallback and popup close detection

BroadcastChannel can silently fail in some browser scenarios. Added:
- localStorage as third communication method in callback page
- storage event listener in dialog
- Popup close detection that checks localStorage directly
- Cleaned up auth-required box styling (gray instead of amber)
This commit is contained in:
Zamil Majdy
2026-02-09 17:52:02 +04:00
parent 7decc20a32
commit 6c2791b00b
2 changed files with 83 additions and 9 deletions

View File

@@ -66,6 +66,12 @@ export async function GET(request: Request) {
}
} catch(e) { console.warn("postMessage failed:", e); }
// Method 3: localStorage (most reliable cross-tab fallback)
try {
localStorage.setItem("mcp_oauth_result", JSON.stringify(msg));
sent = true;
} catch(e) { console.warn("localStorage failed:", e); }
var statusEl = document.getElementById("status");
var spinnerEl = document.getElementById("spinner");
spinnerEl.style.display = "none";

View File

@@ -66,6 +66,10 @@ export function MCPToolDialog({
const messageHandlerRef = useRef<((event: MessageEvent) => void) | null>(
null,
);
const storageHandlerRef = useRef<((event: StorageEvent) => void) | null>(
null,
);
const popupCheckRef = useRef<ReturnType<typeof setInterval> | null>(null);
const oauthHandledRef = useRef(false);
const autoConnectAttemptedRef = useRef(false);
@@ -91,9 +95,15 @@ export function MCPToolDialog({
if (messageHandlerRef.current) {
window.removeEventListener("message", messageHandlerRef.current);
}
if (storageHandlerRef.current) {
window.removeEventListener("storage", storageHandlerRef.current);
}
if (broadcastChannelRef.current) {
broadcastChannelRef.current.close();
}
if (popupCheckRef.current) {
clearInterval(popupCheckRef.current);
}
};
}, []);
@@ -102,10 +112,22 @@ export function MCPToolDialog({
window.removeEventListener("message", messageHandlerRef.current);
messageHandlerRef.current = null;
}
if (storageHandlerRef.current) {
window.removeEventListener("storage", storageHandlerRef.current);
storageHandlerRef.current = null;
}
if (broadcastChannelRef.current) {
broadcastChannelRef.current.close();
broadcastChannelRef.current = null;
}
if (popupCheckRef.current) {
clearInterval(popupCheckRef.current);
popupCheckRef.current = null;
}
// Clean up any stale localStorage entry
try {
localStorage.removeItem("mcp_oauth_result");
} catch {}
setOauthLoading(false);
oauthLoadingRef.current = false;
oauthHandledRef.current = false;
@@ -244,14 +266,23 @@ export function MCPToolDialog({
window.open(login_url, "_blank");
}
// Clear any stale localStorage entry before starting
try {
localStorage.removeItem("mcp_oauth_result");
} catch {}
// Listener 1: BroadcastChannel (works even when window.opener is null)
const bc = new BroadcastChannel("mcp_oauth");
bc.onmessage = (event) => {
if (event.data?.type === "mcp_oauth_result") {
handleOAuthResult(event.data);
}
};
broadcastChannelRef.current = bc;
try {
const bc = new BroadcastChannel("mcp_oauth");
bc.onmessage = (event) => {
if (event.data?.type === "mcp_oauth_result") {
handleOAuthResult(event.data);
}
};
broadcastChannelRef.current = bc;
} catch (e) {
console.warn("BroadcastChannel not available:", e);
}
// Listener 2: window.postMessage (fallback)
const handleMessage = (event: MessageEvent) => {
@@ -263,6 +294,43 @@ export function MCPToolDialog({
messageHandlerRef.current = handleMessage;
window.addEventListener("message", handleMessage);
// Listener 3: localStorage (most reliable cross-tab fallback)
const handleStorage = (event: StorageEvent) => {
if (event.key === "mcp_oauth_result" && event.newValue) {
try {
const data = JSON.parse(event.newValue);
localStorage.removeItem("mcp_oauth_result");
handleOAuthResult(data);
} catch {}
}
};
storageHandlerRef.current = handleStorage;
window.addEventListener("storage", handleStorage);
// Fallback: detect popup close and check localStorage directly
const popupRef = popup;
popupCheckRef.current = setInterval(() => {
if (!oauthLoadingRef.current || oauthHandledRef.current) {
if (popupCheckRef.current) clearInterval(popupCheckRef.current);
return;
}
// Check if popup closed
if (popupRef && popupRef.closed) {
// Check localStorage for the result (storage event may not fire in same window)
try {
const stored = localStorage.getItem("mcp_oauth_result");
if (stored) {
const data = JSON.parse(stored);
localStorage.removeItem("mcp_oauth_result");
handleOAuthResult(data);
return;
}
} catch {}
// Popup closed without result after a short grace period
if (popupCheckRef.current) clearInterval(popupCheckRef.current);
}
}, 500);
// Timeout
setTimeout(() => {
if (oauthLoadingRef.current) {
@@ -344,8 +412,8 @@ export function MCPToolDialog({
{/* Auth required: show sign-in panel */}
{authRequired && (
<div className="flex flex-col items-center gap-3 rounded-lg border border-amber-200 bg-amber-50 p-4 dark:border-amber-800 dark:bg-amber-950">
<p className="text-sm font-medium text-amber-700 dark:text-amber-300">
<div className="flex flex-col items-center gap-3 rounded-lg border border-gray-200 bg-gray-50 p-4 dark:border-slate-700 dark:bg-slate-800">
<p className="text-sm font-medium text-gray-600 dark:text-gray-300">
This server requires authentication
</p>
<Button