Skip to content

Commit c68a29f

Browse files
fix(mcp): show success/denied state on consent page after OAuth redirect
Instead of staying in a loading state forever after clicking Approve, the consent page now shows a green checkmark with "Authorization complete" message telling the user to close the window. Same for Deny. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 2e37b36 commit c68a29f

1 file changed

Lines changed: 54 additions & 27 deletions

File tree

app/[locale]/oauth/mcp-authorize/mcp-consent-form.tsx

Lines changed: 54 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"use client"
22

3-
import { useState } from "react"
3+
import { useState, useEffect } from "react"
44
import { Button } from "@/components/ui/button"
55

66
interface McpConsentFormProps {
@@ -11,32 +11,29 @@ interface McpConsentFormProps {
1111
mcpCallbackUrl: string
1212
}
1313

14-
/**
15-
* Client component for MCP OAuth consent approve/deny buttons.
16-
*
17-
* On approve: POSTs to MCP server /auth/callback with user identity.
18-
* The MCP server then generates an auth code and redirects the browser
19-
* back to the MCP client (Claude Desktop).
20-
*
21-
* On deny: Redirects back to MCP client with error=access_denied.
22-
*/
2314
export function McpConsentForm({
2415
sessionId,
2516
userId,
2617
userRole,
2718
tenantId,
2819
mcpCallbackUrl,
2920
}: McpConsentFormProps) {
30-
const [loading, setLoading] = useState<"approve" | "deny" | null>(null)
21+
const [state, setState] = useState<"idle" | "redirecting" | "success" | "denied" | "error">("idle")
3122
const [error, setError] = useState<string | null>(null)
3223

33-
const handleApprove = async () => {
34-
setLoading("approve")
24+
// After redirecting, show success state (the redirect may take a moment)
25+
useEffect(() => {
26+
if (state === "redirecting") {
27+
const timer = setTimeout(() => setState("success"), 1500)
28+
return () => clearTimeout(timer)
29+
}
30+
}, [state])
31+
32+
const handleApprove = () => {
33+
setState("redirecting")
3534
setError(null)
3635

3736
try {
38-
// Redirect to MCP server callback via GET with query params
39-
// The callback will generate an auth code and redirect back to Claude
4037
const url = new URL(mcpCallbackUrl)
4138
url.searchParams.set("session_id", sessionId)
4239
url.searchParams.set("user_id", userId)
@@ -45,19 +42,49 @@ export function McpConsentForm({
4542
window.location.href = url.toString()
4643
} catch (err) {
4744
setError(err instanceof Error ? err.message : "Failed to authorize")
48-
setLoading(null)
45+
setState("error")
4946
}
5047
}
5148

5249
const handleDeny = () => {
53-
setLoading("deny")
54-
// Close the window or show denied message
50+
setState("denied")
5551
window.close()
56-
// If window.close() doesn't work (not opened by script), show message
57-
setTimeout(() => {
58-
setLoading(null)
59-
setError("Authorization denied. You can close this window.")
60-
}, 500)
52+
}
53+
54+
if (state === "success") {
55+
return (
56+
<div className="space-y-4 text-center">
57+
<div className="mx-auto flex h-12 w-12 items-center justify-center rounded-full bg-green-100 dark:bg-green-900/30">
58+
<svg className="h-6 w-6 text-green-600 dark:text-green-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
59+
<path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7" />
60+
</svg>
61+
</div>
62+
<div>
63+
<p className="font-medium">Authorization complete</p>
64+
<p className="mt-1 text-sm text-muted-foreground">
65+
You can close this window and return to Claude Desktop.
66+
</p>
67+
</div>
68+
</div>
69+
)
70+
}
71+
72+
if (state === "denied") {
73+
return (
74+
<div className="space-y-4 text-center">
75+
<div className="mx-auto flex h-12 w-12 items-center justify-center rounded-full bg-muted">
76+
<svg className="h-6 w-6 text-muted-foreground" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
77+
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
78+
</svg>
79+
</div>
80+
<div>
81+
<p className="font-medium">Authorization denied</p>
82+
<p className="mt-1 text-sm text-muted-foreground">
83+
You can close this window.
84+
</p>
85+
</div>
86+
</div>
87+
)
6188
}
6289

6390
return (
@@ -70,19 +97,19 @@ export function McpConsentForm({
7097

7198
<Button
7299
onClick={handleApprove}
73-
disabled={loading !== null}
100+
disabled={state === "redirecting"}
74101
className="w-full"
75102
>
76-
{loading === "approve" ? "Authorizing..." : "Approve"}
103+
{state === "redirecting" ? "Authorizing..." : "Approve"}
77104
</Button>
78105

79106
<Button
80107
onClick={handleDeny}
81-
disabled={loading !== null}
108+
disabled={state === "redirecting"}
82109
variant="outline"
83110
className="w-full"
84111
>
85-
{loading === "deny" ? "Denying..." : "Deny"}
112+
Deny
86113
</Button>
87114
</div>
88115
)

0 commit comments

Comments
 (0)