Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/plugins/google-discovery/src/api/handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ const popupDocument = (payload: OAuthPopupResult): string => {
<p style="margin:0;font-size:14px;color:#666;line-height:1.5">${escapeHtml(message)}</p>
</main>
<script>
(()=>{const p=${serialized};try{if(window.opener)window.opener.postMessage(p,window.location.origin)}finally{setTimeout(()=>window.close(),150)}})();
(()=>{const p=${serialized};try{if(window.opener)window.opener.postMessage(p,window.location.origin);if("BroadcastChannel"in window){const c=new BroadcastChannel("executor:google-discovery-oauth-result");c.postMessage(p);setTimeout(()=>c.close(),100)}}finally{setTimeout(()=>window.close(),150)}})();
</script>
</body></html>`;
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -363,50 +363,56 @@ type OAuthPopupResult =
error: string;
};

const OAUTH_RESULT_CHANNEL = "executor:google-discovery-oauth-result";

const isOAuthPopupResult = (value: unknown): value is OAuthPopupResult =>
typeof value === "object" &&
value !== null &&
(value as { type?: unknown }).type === "executor:oauth-result";

function openOAuthPopup(
url: string,
onResult: (data: OAuthPopupResult) => void,
onClosed?: () => void,
): void {
onOpenFailed?: () => void,
): () => void {
const w = 640;
const h = 760;
const left = window.screenX + (window.outerWidth - w) / 2;
const top = window.screenY + (window.outerHeight - h) / 2;
const popup = window.open(
url,
"google-discovery-oauth",
`width=${w},height=${h},left=${left},top=${top},popup=1`,
);

let settled = false;
const channel = typeof BroadcastChannel !== "undefined"
? new BroadcastChannel(OAUTH_RESULT_CHANNEL)
: null;
const settle = () => {
if (settled) return;
settled = true;
window.removeEventListener("message", onMessage);
channel?.close();
};

const handleResult = (data: unknown) => {
if (!isOAuthPopupResult(data) || settled) return;
settle();
onResult(data);
};

const onMessage = (event: MessageEvent) => {
if (
event.origin === window.location.origin &&
event.data?.type === "executor:oauth-result" &&
!settled
) {
settle();
onResult(event.data as OAuthPopupResult);
}
if (event.origin === window.location.origin) handleResult(event.data);
};
window.addEventListener("message", onMessage);
if (channel) channel.onmessage = (event) => handleResult(event.data);

if (popup) {
const interval = window.setInterval(() => {
if (popup.closed) {
window.clearInterval(interval);
if (!settled) {
settle();
onClosed?.();
}
}
}, 500);
const popup = window.open(
url,
"google-discovery-oauth",
`width=${w},height=${h},left=${left},top=${top},popup=1`,
);
if (!popup && !settled) {
settle();
queueMicrotask(() => onOpenFailed?.());
}
return settle;
}

export default function AddGoogleDiscoverySource(props: {
Expand Down Expand Up @@ -502,8 +508,12 @@ export default function AddGoogleDiscoverySource(props: {
}
}, []); // eslint-disable-line react-hooks/exhaustive-deps

const oauthCleanup = useRef<(() => void) | null>(null);

const handleStartOAuth = useCallback(async () => {
if (!probe) return;
oauthCleanup.current?.();
oauthCleanup.current = null;
setStartingOAuth(true);
setError(null);
try {
Expand All @@ -519,9 +529,10 @@ export default function AddGoogleDiscoverySource(props: {
},
});

openOAuthPopup(
oauthCleanup.current = openOAuthPopup(
response.authorizationUrl,
(result) => {
oauthCleanup.current = null;
setStartingOAuth(false);
if (result.ok) {
setOauthAuth({
Expand All @@ -541,14 +552,22 @@ export default function AddGoogleDiscoverySource(props: {
}
},
() => {
oauthCleanup.current = null;
setStartingOAuth(false);
setError("OAuth popup was blocked");
},
);
} catch (e) {
setStartingOAuth(false);
setError(e instanceof Error ? e.message : "Failed to start OAuth");
}
}, [probe, doStartOAuth, name, discoveryUrl, clientId, clientSecretSecretId]);
}, [probe, doStartOAuth, scopeId, name, discoveryUrl, clientId, clientSecretSecretId]);

const handleCancelOAuth = useCallback(() => {
oauthCleanup.current?.();
oauthCleanup.current = null;
setStartingOAuth(false);
}, []);

const handleAdd = useCallback(async () => {
if (!probe) return;
Expand Down Expand Up @@ -755,17 +774,29 @@ export default function AddGoogleDiscoverySource(props: {
</CollapsibleTrigger>
)}
</div>
<Button
variant="outline"
onClick={handleStartOAuth}
disabled={!probe || !clientId.trim() || !canUseOAuth || startingOAuth}
>
{startingOAuth
? <><Spinner className="size-3.5" /> Waiting…</>
: oauthAuth
? "Re-authenticate"
: "Connect Google"}
</Button>
<div className="flex shrink-0 items-center gap-2">
<Button
variant="outline"
onClick={handleStartOAuth}
disabled={!probe || !clientId.trim() || !canUseOAuth || startingOAuth}
>
{startingOAuth
? <><Spinner className="size-3.5" /> Waiting…</>
: oauthAuth
? "Re-authenticate"
: "Connect Google"}
</Button>
{startingOAuth && (
<Button
variant="ghost"
size="sm"
onClick={handleCancelOAuth}
className="h-8 px-2 text-xs"
>
Cancel
</Button>
)}
</div>
</div>
<CollapsibleContent>
<div className="rounded-lg border border-border/70 bg-muted/20 px-3 py-2">
Expand Down
2 changes: 1 addition & 1 deletion packages/plugins/mcp/src/api/handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ const popupDocument = (payload: OAuthPopupResult): string => {
<p style="margin:0;font-size:14px;color:#666;line-height:1.5">${escapeHtml(message)}</p>
</main>
<script>
(()=>{const p=${serialized};try{if(window.opener)window.opener.postMessage(p,window.location.origin)}finally{setTimeout(()=>window.close(),150)}})();
(()=>{const p=${serialized};try{if(window.opener)window.opener.postMessage(p,window.location.origin);if("BroadcastChannel"in window){const c=new BroadcastChannel("executor:mcp-oauth-result");c.postMessage(p);setTimeout(()=>c.close(),100)}}finally{setTimeout(()=>window.close(),150)}})();
</script>
</body></html>`;
};
Expand Down
71 changes: 56 additions & 15 deletions packages/plugins/mcp/src/react/AddMcpSource.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -137,32 +137,51 @@ type OAuthPopupResult =
| { type: "executor:oauth-result"; ok: true; sessionId: string } & OAuthTokens
| { type: "executor:oauth-result"; ok: false; sessionId: null; error: string };

const OAUTH_RESULT_CHANNEL = "executor:mcp-oauth-result";

const isOAuthPopupResult = (value: unknown): value is OAuthPopupResult =>
typeof value === "object" &&
value !== null &&
(value as { type?: unknown }).type === "executor:oauth-result";

function openOAuthPopup(
url: string,
onResult: (data: OAuthPopupResult) => void,
onClosed?: () => void,
): void {
onOpenFailed?: () => void,
): () => void {
const w = 600, h = 700;
const left = window.screenX + (window.outerWidth - w) / 2;
const top = window.screenY + (window.outerHeight - h) / 2;
const popup = window.open(url, "mcp-oauth", `width=${w},height=${h},left=${left},top=${top},popup=1`);

let settled = false;
const settle = () => { settled = true; window.removeEventListener("message", onMsg); };
const channel = typeof BroadcastChannel !== "undefined"
? new BroadcastChannel(OAUTH_RESULT_CHANNEL)
: null;
const settle = () => {
if (settled) return;
settled = true;
window.removeEventListener("message", onMsg);
channel?.close();
};

const handleResult = (data: unknown) => {
if (!isOAuthPopupResult(data) || settled) return;
settle();
onResult(data);
};

const onMsg = (e: MessageEvent) => {
if (e.origin === window.location.origin && e.data?.type === "executor:oauth-result" && !settled) {
settle();
onResult(e.data as OAuthPopupResult);
}
if (e.origin === window.location.origin) handleResult(e.data);
};
window.addEventListener("message", onMsg);
if (channel) channel.onmessage = (e) => handleResult(e.data);

if (popup) {
const iv = setInterval(() => {
if (popup.closed) { clearInterval(iv); if (!settled) { settle(); onClosed?.(); } }
}, 500);
const popup = window.open(url, "mcp-oauth", `width=${w},height=${h},left=${left},top=${top},popup=1`);
if (!popup && !settled) {
settle();
queueMicrotask(() => onOpenFailed?.());
}
return settle;
}

// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -245,7 +264,11 @@ export default function AddMcpSource(props: {
}
}, []); // eslint-disable-line react-hooks/exhaustive-deps

const oauthCleanup = useRef<(() => void) | null>(null);

const handleOAuth = useCallback(async () => {
oauthCleanup.current?.();
oauthCleanup.current = null;
dispatch({ type: "oauth-start" });
try {
const redirectUrl = `${window.location.origin}/api/mcp/oauth/callback`;
Expand All @@ -254,9 +277,10 @@ export default function AddMcpSource(props: {
payload: { endpoint: state.url.trim(), redirectUrl },
});
dispatch({ type: "oauth-waiting", sessionId: result.sessionId });
openOAuthPopup(
oauthCleanup.current = openOAuthPopup(
result.authorizationUrl,
(data) => {
oauthCleanup.current = null;
if (data.ok) {
dispatch({
type: "oauth-ok",
Expand All @@ -272,12 +296,21 @@ export default function AddMcpSource(props: {
dispatch({ type: "oauth-fail", error: data.error });
}
},
() => dispatch({ type: "oauth-cancelled" }),
() => {
oauthCleanup.current = null;
dispatch({ type: "oauth-fail", error: "OAuth popup was blocked" });
},
);
} catch (e) {
dispatch({ type: "oauth-fail", error: e instanceof Error ? e.message : "Failed to start OAuth" });
}
}, [state.url, doStartOAuth]);
}, [state.url, scopeId, doStartOAuth]);

const handleCancelOAuth = useCallback(() => {
oauthCleanup.current?.();
oauthCleanup.current = null;
dispatch({ type: "oauth-cancelled" });
}, []);

const handleAddRemote = useCallback(async () => {
if (!probe) return;
Expand Down Expand Up @@ -474,6 +507,14 @@ export default function AddMcpSource(props: {
<div className="flex items-center gap-2 rounded-lg border border-blue-500/30 bg-blue-500/5 px-3 py-2.5">
<Spinner className="size-3.5 text-blue-500" />
<span className="text-xs text-blue-600 dark:text-blue-400">Waiting for authorization in popup…</span>
<Button
variant="ghost"
size="sm"
onClick={handleCancelOAuth}
className="ml-auto h-7 px-2 text-xs"
>
Cancel
</Button>
</div>
)}
</section>
Expand Down