History

Polish MCP OAuth completion and connected UI

Rhys Sullivan committed 8 months ago

Commit 12386ed1 parent 008fe733 files · +46 −39Browse files

Unified · Split

3 files
  1. executor/apps/web/src/app/mcp/oauth/complete/page.tsx +38 −35
  2. executor/apps/web/src/components/tools/add-source-auth-panel.tsx +7 −3
  3. executor/apps/web/src/components/tools/add-source-dialog.tsx +1 −1
executor/apps/web/src/app/mcp/oauth/complete/page.tsx+38−35
@@ -1,6 +1,7 @@
1"use client";1"use client";
2 2
3import { useEffect } from "react";3import { useEffect, useRef } from "react";
4import { useQuery as useTanstackQuery } from "@tanstack/react-query";
4 5
5type PopupMessage =6type PopupMessage =
6 | {7 | {
@@ -28,45 +29,47 @@ function fallbackErrorMessage(error: unknown): PopupMessage { · 21 unmodified lines
28 };29 };
29}30}
30 31
32async function fetchPopupResult(): Promise<PopupMessage> {
33 const response = await fetch("/mcp/oauth/result", {
34 method: "GET",
35 cache: "no-store",
36 credentials: "same-origin",
37 });
38
39 const data = await response.json() as PopupMessage;
40 if (!data || data.type !== "executor:mcp-oauth-result") {
41 throw new Error("Invalid OAuth completion payload");
42 }
43 return data;
44}
45
31export default function McpOAuthCompletePage() {46export default function McpOAuthCompletePage() {
32 useEffect(() => {47 const sentRef = useRef(false);
33 let cancelled = false;48 const resultQuery = useTanstackQuery({
49 queryKey: ["mcp-oauth-popup-result"],
50 queryFn: fetchPopupResult,
51 retry: false,
52 staleTime: 0,
53 });
34 54
35 const finish = async () => {55 useEffect(() => {
36 let message: PopupMessage;56 if (sentRef.current || resultQuery.isPending) {
37 try {57 return;
38 const response = await fetch("/mcp/oauth/result", {58 }
39 method: "GET",
40 cache: "no-store",
41 credentials: "same-origin",
42 });
43 const data = await response.json() as PopupMessage;
44 if (!data || data.type !== "executor:mcp-oauth-result") {
45 throw new Error("Invalid OAuth completion payload");
46 }
47 message = data;
48 } catch (error) {
49 message = fallbackErrorMessage(error);
50 }
51 59
52 if (cancelled) {60 sentRef.current = true;
53 return;61 const message = resultQuery.isSuccess
54 }62 ? resultQuery.data
63 : fallbackErrorMessage(resultQuery.error);
55 64
56 try {65 try {
57 if (window.opener) {66 if (window.opener) {
58 window.opener.postMessage(message, window.location.origin);67 window.opener.postMessage(message, window.location.origin);
59 }
60 } finally {
61 window.close();
62 }68 }
63 };69 } finally {
64 70 window.close();
65 void finish();71 }
66 return () => {72 }, [resultQuery.data, resultQuery.error, resultQuery.isPending, resultQuery.isSuccess]);
67 cancelled = true;
68 };
69 }, []);
70 73
71 return (74 return (
72 <main className="min-h-screen flex items-center justify-center bg-background p-6">75 <main className="min-h-screen flex items-center justify-center bg-background p-6">
executor/apps/web/src/components/tools/add-source-auth-panel.tsx+7−3
@@ -98,6 +98,7 @@ export function SourceAuthPanel({ · 97 unmodified lines
98 }98 }
99 99
100 const badge = inferredAuthBadge(inferredSpecAuth);100 const badge = inferredAuthBadge(inferredSpecAuth);
101 const mcpBearerConnected = sourceType === "mcp" && authType === "bearer" && mcpOAuthConnected;
101 return (102 return (
102 <div className="space-y-3">103 <div className="space-y-3">
103 104
@@ -179,7 +180,7 @@ export function SourceAuthPanel({ · 75 unmodified lines
179 <div className="space-y-1.5">180 <div className="space-y-1.5">
180 <div className="flex items-center gap-2">181 <div className="flex items-center gap-2">
181 <Label className="text-xs text-muted-foreground">OAuth</Label>182 <Label className="text-xs text-muted-foreground">OAuth</Label>
182 {mcpOAuthConnected ? (183 {mcpBearerConnected ? (
183 <Badge variant="outline" className="text-[10px] uppercase tracking-wider text-terminal-green">184 <Badge variant="outline" className="text-[10px] uppercase tracking-wider text-terminal-green">
184 Connected185 Connected
185 </Badge>186 </Badge>
@@ -193,8 +194,11 @@ export function SourceAuthPanel({ · 7 unmodified lines
193 disabled={mcpOAuthBusy}194 disabled={mcpOAuthBusy}
194 onClick={onMcpOAuthConnect}195 onClick={onMcpOAuthConnect}
195 >196 >
196 {mcpOAuthBusy ? "Connecting..." : mcpOAuthConnected ? "Reconnect OAuth" : "Connect OAuth in popup"}197 {mcpOAuthBusy ? "Connecting..." : mcpBearerConnected ? "Reconnect OAuth" : "Connect OAuth in popup"}
197 </Button>198 </Button>
199 {mcpBearerConnected ? (
200 <p className="text-[11px] text-muted-foreground">OAuth linked successfully.</p>
201 ) : null}
198 </div>202 </div>
199 ) : null}203 ) : null}
200 204
@@ -210,7 +214,7 @@ export function SourceAuthPanel({ · 9 unmodified lines
210 </div>214 </div>
211 ) : null}215 ) : null}
212 216
213 {authType === "bearer" ? (217 {authType === "bearer" && !mcpBearerConnected ? (
214 <div className="space-y-1.5">218 <div className="space-y-1.5">
215 <Label className="text-xs text-muted-foreground flex items-center gap-1.5">219 <Label className="text-xs text-muted-foreground flex items-center gap-1.5">
216 <LockKeyhole className="h-3 w-3" />220 <LockKeyhole className="h-3 w-3" />
executor/apps/web/src/components/tools/add-source-dialog.tsx+1−1
@@ -149,7 +149,7 @@ export function AddSourceDialog({ · 148 unmodified lines
149 form.handleAuthTypeChange("bearer");149 form.handleAuthTypeChange("bearer");
150 }150 }
151 form.handleAuthFieldChange("tokenValue", result.accessToken);151 form.handleAuthFieldChange("tokenValue", result.accessToken);
152 toast.success("OAuth linked. Bearer token populated.");152 toast.success("OAuth linked successfully.");
153 } catch (error) {153 } catch (error) {
154 toast.error(error instanceof Error ? error.message : "Failed to connect OAuth");154 toast.error(error instanceof Error ? error.message : "Failed to connect OAuth");
155 } finally {155 } finally {