Unified · Split
| @@ -4,26 +4,34 @@ import { auth } from "@modelcontextprotocol/sdk/client/auth.js"; · 3 unmodified lines | ||
| 4 | 4 | import { |
| 5 | 5 | buildPendingCookieName, |
| 6 | 6 | decodePendingCookieValue, |
| 7 | + encodePopupResultCookieValue, | |
| 8 | + MCP_OAUTH_RESULT_COOKIE, | |
| 7 | 9 | McpPopupOAuthProvider, |
| 8 | − oauthPopupResultHtml, | |
| 10 | + type McpOAuthPopupResult, | |
| 9 | 11 | } from "@/lib/mcp-oauth-provider"; |
| 10 | 12 | |
| 11 | −function popupHtmlResponse(payload: { | |
| 12 | − ok: boolean; | |
| 13 | − sourceUrl?: string; | |
| 14 | − accessToken?: string; | |
| 15 | − refreshToken?: string; | |
| 16 | − scope?: string; | |
| 17 | − expiresIn?: number; | |
| 18 | − error?: string; | |
| 19 | −}): NextResponse { | |
| 20 | − return new NextResponse(oauthPopupResultHtml(payload), { | |
| 21 | − status: payload.ok ? 200 : 400, | |
| 22 | − headers: { | |
| 23 | − "content-type": "text/html; charset=utf-8", | |
| 24 | − "cache-control": "no-store", | |
| 25 | − }, | |
| 13 | +function popupResultRedirect( | |
| 14 | + request: NextRequest, | |
| 15 | + pendingCookieName: string | null, | |
| 16 | + payload: McpOAuthPopupResult, | |
| 17 | +): NextResponse { | |
| 18 | + const host = request.headers.get("x-forwarded-host") ?? request.headers.get("host"); | |
| 19 | + const proto = request.headers.get("x-forwarded-proto") ?? request.nextUrl.protocol.replace(":", ""); | |
| 20 | + const origin = host && proto ? `${proto}://${host}` : request.nextUrl.origin; | |
| 21 | + const response = NextResponse.redirect(`${origin}/mcp/oauth/complete`); | |
| 22 | + response.cookies.set({ | |
| 23 | + name: MCP_OAUTH_RESULT_COOKIE, | |
| 24 | + value: encodePopupResultCookieValue(payload), | |
| 25 | + httpOnly: true, | |
| 26 | + secure: request.nextUrl.protocol === "https:", | |
| 27 | + sameSite: "lax", | |
| 28 | + maxAge: 2 * 60, | |
| 29 | + path: "/", | |
| 26 | 30 | }); |
| 31 | + if (pendingCookieName) { | |
| 32 | + response.cookies.delete(pendingCookieName); | |
| 33 | + } | |
| 34 | + return response; | |
| 27 | 35 | } |
| 28 | 36 | |
| 29 | 37 | export async function GET(request: NextRequest) { |
| @@ -32,7 +40,7 @@ export async function GET(request: NextRequest) { · 2 unmodified lines | ||
| 32 | 40 | const error = request.nextUrl.searchParams.get("error")?.trim(); |
| 33 | 41 | |
| 34 | 42 | if (!state) { |
| 35 | − return popupHtmlResponse({ ok: false, error: "Missing OAuth state" }); | |
| 43 | + return popupResultRedirect(request, null, { ok: false, error: "Missing OAuth state" }); | |
| 36 | 44 | } |
| 37 | 45 | |
| 38 | 46 | const cookieName = buildPendingCookieName(state); |
| @@ -40,30 +48,28 @@ export async function GET(request: NextRequest) { · 1 unmodified line | ||
| 40 | 48 | const pending = rawPending ? decodePendingCookieValue(rawPending) : null; |
| 41 | 49 | |
| 42 | 50 | if (!pending) { |
| 43 | − const response = popupHtmlResponse({ ok: false, error: "OAuth session expired. Try connecting again." }); | |
| 44 | − response.cookies.delete(cookieName); | |
| 45 | − return response; | |
| 51 | + return popupResultRedirect(request, cookieName, { | |
| 52 | + ok: false, | |
| 53 | + error: "OAuth session expired. Try connecting again.", | |
| 54 | + }); | |
| 46 | 55 | } |
| 47 | 56 | |
| 48 | 57 | if (error) { |
| 49 | − const response = popupHtmlResponse({ ok: false, error: `OAuth error: ${error}` }); | |
| 50 | − response.cookies.delete(cookieName); | |
| 51 | − return response; | |
| 58 | + return popupResultRedirect(request, cookieName, { ok: false, error: `OAuth error: ${error}` }); | |
| 52 | 59 | } |
| 53 | 60 | |
| 54 | 61 | if (!code) { |
| 55 | − const response = popupHtmlResponse({ ok: false, error: "Missing OAuth authorization code" }); | |
| 56 | − response.cookies.delete(cookieName); | |
| 57 | − return response; | |
| 62 | + return popupResultRedirect(request, cookieName, { | |
| 63 | + ok: false, | |
| 64 | + error: "Missing OAuth authorization code", | |
| 65 | + }); | |
| 58 | 66 | } |
| 59 | 67 | |
| 60 | 68 | let sourceUrl: URL; |
| 61 | 69 | try { |
| 62 | 70 | sourceUrl = new URL(pending.sourceUrl); |
| 63 | 71 | } catch { |
| 64 | − const response = popupHtmlResponse({ ok: false, error: "Invalid MCP source URL" }); | |
| 65 | − response.cookies.delete(cookieName); | |
| 66 | − return response; | |
| 72 | + return popupResultRedirect(request, cookieName, { ok: false, error: "Invalid MCP source URL" }); | |
| 67 | 73 | } |
| 68 | 74 | |
| 69 | 75 | const provider = new McpPopupOAuthProvider({ |
| @@ -79,23 +85,22 @@ export async function GET(request: NextRequest) { · 9 unmodified lines | ||
| 79 | 85 | authorizationCode: code, |
| 80 | 86 | }); |
| 81 | 87 | } catch (finishError) { |
| 82 | − const response = popupHtmlResponse({ | |
| 88 | + return popupResultRedirect(request, cookieName, { | |
| 83 | 89 | ok: false, |
| 84 | 90 | error: finishError instanceof Error ? finishError.message : "Failed to finish OAuth", |
| 85 | 91 | }); |
| 86 | − response.cookies.delete(cookieName); | |
| 87 | − return response; | |
| 88 | 92 | } |
| 89 | 93 | |
| 90 | 94 | const tokens = provider.getTokens(); |
| 91 | 95 | const accessToken = tokens?.access_token?.trim() ?? ""; |
| 92 | 96 | if (!accessToken) { |
| 93 | − const response = popupHtmlResponse({ ok: false, error: "OAuth completed without an access token" }); | |
| 94 | − response.cookies.delete(cookieName); | |
| 95 | − return response; | |
| 97 | + return popupResultRedirect(request, cookieName, { | |
| 98 | + ok: false, | |
| 99 | + error: "OAuth completed without an access token", | |
| 100 | + }); | |
| 96 | 101 | } |
| 97 | 102 | |
| 98 | − const response = popupHtmlResponse({ | |
| 103 | + return popupResultRedirect(request, cookieName, { | |
| 99 | 104 | ok: true, |
| 100 | 105 | sourceUrl: pending.sourceUrl, |
| 101 | 106 | accessToken, |
| @@ -103,6 +108,4 @@ export async function GET(request: NextRequest) { · 1 unmodified line | ||
| 103 | 108 | scope: tokens?.scope, |
| 104 | 109 | expiresIn: typeof tokens?.expires_in === "number" ? tokens.expires_in : undefined, |
| 105 | 110 | }); |
| 106 | − response.cookies.delete(cookieName); | |
| 107 | − return response; | |
| 108 | 111 | } |
| @@ -0,0 +1,79 @@ | ||
| 1 | +"use client"; | |
| 2 | + | |
| 3 | +import { useEffect } from "react"; | |
| 4 | + | |
| 5 | +type PopupMessage = | |
| 6 | + | { | |
| 7 | + type: "executor:mcp-oauth-result"; | |
| 8 | + ok: true; | |
| 9 | + sourceUrl: string; | |
| 10 | + payload: { | |
| 11 | + accessToken: string; | |
| 12 | + refreshToken?: string; | |
| 13 | + scope?: string; | |
| 14 | + expiresIn?: number; | |
| 15 | + }; | |
| 16 | + } | |
| 17 | + | { | |
| 18 | + type: "executor:mcp-oauth-result"; | |
| 19 | + ok: false; | |
| 20 | + error: string; | |
| 21 | + }; | |
| 22 | + | |
| 23 | +function fallbackErrorMessage(error: unknown): PopupMessage { | |
| 24 | + return { | |
| 25 | + type: "executor:mcp-oauth-result", | |
| 26 | + ok: false, | |
| 27 | + error: error instanceof Error ? error.message : "Failed to finalize OAuth", | |
| 28 | + }; | |
| 29 | +} | |
| 30 | + | |
| 31 | +export default function McpOAuthCompletePage() { | |
| 32 | + useEffect(() => { | |
| 33 | + let cancelled = false; | |
| 34 | + | |
| 35 | + const finish = async () => { | |
| 36 | + let message: PopupMessage; | |
| 37 | + try { | |
| 38 | + const response = await fetch("/mcp/oauth/result", { | |
| 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 | + | |
| 52 | + if (cancelled) { | |
| 53 | + return; | |
| 54 | + } | |
| 55 | + | |
| 56 | + try { | |
| 57 | + if (window.opener) { | |
| 58 | + window.opener.postMessage(message, window.location.origin); | |
| 59 | + } | |
| 60 | + } finally { | |
| 61 | + window.close(); | |
| 62 | + } | |
| 63 | + }; | |
| 64 | + | |
| 65 | + void finish(); | |
| 66 | + return () => { | |
| 67 | + cancelled = true; | |
| 68 | + }; | |
| 69 | + }, []); | |
| 70 | + | |
| 71 | + return ( | |
| 72 | + <main className="min-h-screen flex items-center justify-center bg-background p-6"> | |
| 73 | + <div className="text-center space-y-2"> | |
| 74 | + <h1 className="text-base font-medium">Finishing OAuth</h1> | |
| 75 | + <p className="text-sm text-muted-foreground">You can close this window if it does not close automatically.</p> | |
| 76 | + </div> | |
| 77 | + </main> | |
| 78 | + ); | |
| 79 | +} | |
| @@ -0,0 +1,79 @@ | ||
| 1 | +import type { NextRequest } from "next/server"; | |
| 2 | +import { NextResponse } from "next/server"; | |
| 3 | +import { | |
| 4 | + decodePopupResultCookieValue, | |
| 5 | + MCP_OAUTH_RESULT_COOKIE, | |
| 6 | + type McpOAuthPopupResult, | |
| 7 | +} from "@/lib/mcp-oauth-provider"; | |
| 8 | + | |
| 9 | +type PopupMessage = | |
| 10 | + | { | |
| 11 | + type: "executor:mcp-oauth-result"; | |
| 12 | + ok: true; | |
| 13 | + sourceUrl: string; | |
| 14 | + payload: { | |
| 15 | + accessToken: string; | |
| 16 | + refreshToken?: string; | |
| 17 | + scope?: string; | |
| 18 | + expiresIn?: number; | |
| 19 | + }; | |
| 20 | + } | |
| 21 | + | { | |
| 22 | + type: "executor:mcp-oauth-result"; | |
| 23 | + ok: false; | |
| 24 | + error: string; | |
| 25 | + }; | |
| 26 | + | |
| 27 | +function toPopupMessage(result: McpOAuthPopupResult | null): PopupMessage { | |
| 28 | + if (!result) { | |
| 29 | + return { | |
| 30 | + type: "executor:mcp-oauth-result", | |
| 31 | + ok: false, | |
| 32 | + error: "OAuth result is missing or expired", | |
| 33 | + }; | |
| 34 | + } | |
| 35 | + | |
| 36 | + if (!result.ok) { | |
| 37 | + return { | |
| 38 | + type: "executor:mcp-oauth-result", | |
| 39 | + ok: false, | |
| 40 | + error: result.error ?? "OAuth failed", | |
| 41 | + }; | |
| 42 | + } | |
| 43 | + | |
| 44 | + const accessToken = result.accessToken?.trim() ?? ""; | |
| 45 | + if (!accessToken) { | |
| 46 | + return { | |
| 47 | + type: "executor:mcp-oauth-result", | |
| 48 | + ok: false, | |
| 49 | + error: "OAuth finished without an access token", | |
| 50 | + }; | |
| 51 | + } | |
| 52 | + | |
| 53 | + return { | |
| 54 | + type: "executor:mcp-oauth-result", | |
| 55 | + ok: true, | |
| 56 | + sourceUrl: result.sourceUrl ?? "", | |
| 57 | + payload: { | |
| 58 | + accessToken, | |
| 59 | + ...(result.refreshToken ? { refreshToken: result.refreshToken } : {}), | |
| 60 | + ...(result.scope ? { scope: result.scope } : {}), | |
| 61 | + ...(typeof result.expiresIn === "number" ? { expiresIn: result.expiresIn } : {}), | |
| 62 | + }, | |
| 63 | + }; | |
| 64 | +} | |
| 65 | + | |
| 66 | +export async function GET(request: NextRequest) { | |
| 67 | + const encoded = request.cookies.get(MCP_OAUTH_RESULT_COOKIE)?.value ?? ""; | |
| 68 | + const parsed = encoded ? decodePopupResultCookieValue(encoded) : null; | |
| 69 | + const message = toPopupMessage(parsed); | |
| 70 | + | |
| 71 | + const response = NextResponse.json(message, { | |
| 72 | + status: message.ok ? 200 : 400, | |
| 73 | + headers: { | |
| 74 | + "cache-control": "no-store", | |
| 75 | + }, | |
| 76 | + }); | |
| 77 | + response.cookies.delete(MCP_OAUTH_RESULT_COOKIE); | |
| 78 | + return response; | |
| 79 | +} | |
| @@ -5,8 +5,10 @@ import { · 4 unmodified lines | ||
| 5 | 5 | buildPendingCookieName, |
| 6 | 6 | createOAuthState, |
| 7 | 7 | encodePendingCookieValue, |
| 8 | + encodePopupResultCookieValue, | |
| 9 | + MCP_OAUTH_RESULT_COOKIE, | |
| 8 | 10 | McpPopupOAuthProvider, |
| 9 | − oauthPopupResultHtml, | |
| 11 | + type McpOAuthPopupResult, | |
| 10 | 12 | } from "@/lib/mcp-oauth-provider"; |
| 11 | 13 | |
| 12 | 14 | function getExternalOrigin(request: NextRequest): string { |
| @@ -18,27 +20,36 @@ function getExternalOrigin(request: NextRequest): string { · 5 unmodified lines | ||
| 18 | 20 | return request.nextUrl.origin; |
| 19 | 21 | } |
| 20 | 22 | |
| 21 | −function badPopupResponse(message: string): NextResponse { | |
| 22 | − return new NextResponse(oauthPopupResultHtml({ ok: false, error: message }), { | |
| 23 | − status: 400, | |
| 24 | − headers: { | |
| 25 | − "content-type": "text/html; charset=utf-8", | |
| 26 | − "cache-control": "no-store", | |
| 27 | − }, | |
| 23 | +function popupResultRedirect(request: NextRequest, payload: McpOAuthPopupResult): NextResponse { | |
| 24 | + const externalOrigin = getExternalOrigin(request); | |
| 25 | + const response = NextResponse.redirect(`${externalOrigin}/mcp/oauth/complete`); | |
| 26 | + response.cookies.set({ | |
| 27 | + name: MCP_OAUTH_RESULT_COOKIE, | |
| 28 | + value: encodePopupResultCookieValue(payload), | |
| 29 | + httpOnly: true, | |
| 30 | + secure: request.nextUrl.protocol === "https:", | |
| 31 | + sameSite: "lax", | |
| 32 | + maxAge: 2 * 60, | |
| 33 | + path: "/", | |
| 28 | 34 | }); |
| 35 | + return response; | |
| 36 | +} | |
| 37 | + | |
| 38 | +function badPopupResponse(request: NextRequest, message: string): NextResponse { | |
| 39 | + return popupResultRedirect(request, { ok: false, error: message }); | |
| 29 | 40 | } |
| 30 | 41 | |
| 31 | 42 | export async function GET(request: NextRequest) { |
| 32 | 43 | const sourceUrlRaw = request.nextUrl.searchParams.get("sourceUrl")?.trim() ?? ""; |
| 33 | 44 | if (!sourceUrlRaw) { |
| 34 | − return badPopupResponse("Missing sourceUrl"); | |
| 45 | + return badPopupResponse(request, "Missing sourceUrl"); | |
| 35 | 46 | } |
| 36 | 47 | |
| 37 | 48 | let sourceUrl: URL; |
| 38 | 49 | try { |
| 39 | 50 | sourceUrl = new URL(sourceUrlRaw); |
| 40 | 51 | } catch { |
| 41 | − return badPopupResponse("Invalid sourceUrl"); | |
| 52 | + return badPopupResponse(request, "Invalid sourceUrl"); | |
| 42 | 53 | } |
| 43 | 54 | |
| 44 | 55 | const state = createOAuthState(); |
| @@ -53,37 +64,28 @@ export async function GET(request: NextRequest) { · 8 unmodified lines | ||
| 53 | 64 | authResult = await auth(provider, { serverUrl: sourceUrl }); |
| 54 | 65 | } catch (error) { |
| 55 | 66 | const message = error instanceof Error ? error.message : "Failed to start OAuth flow"; |
| 56 | − return badPopupResponse(message); | |
| 67 | + return badPopupResponse(request, message); | |
| 57 | 68 | } |
| 58 | 69 | |
| 59 | 70 | if (authResult === "AUTHORIZED") { |
| 60 | 71 | const tokens = provider.getTokens(); |
| 61 | 72 | const accessToken = tokens?.access_token?.trim() ?? ""; |
| 62 | 73 | if (!accessToken) { |
| 63 | − return badPopupResponse("OAuth flow completed without an access token"); | |
| 74 | + return badPopupResponse(request, "OAuth flow completed without an access token"); | |
| 64 | 75 | } |
| 65 | − return new NextResponse( | |
| 66 | − oauthPopupResultHtml({ | |
| 67 | − ok: true, | |
| 68 | − sourceUrl: sourceUrl.toString(), | |
| 69 | − accessToken, | |
| 70 | − refreshToken: tokens?.refresh_token, | |
| 71 | − scope: tokens?.scope, | |
| 72 | − expiresIn: typeof tokens?.expires_in === "number" ? tokens.expires_in : undefined, | |
| 73 | − }), | |
| 74 | − { | |
| 75 | − status: 200, | |
| 76 | − headers: { | |
| 77 | − "content-type": "text/html; charset=utf-8", | |
| 78 | − "cache-control": "no-store", | |
| 79 | − }, | |
| 80 | − }, | |
| 81 | − ); | |
| 76 | + return popupResultRedirect(request, { | |
| 77 | + ok: true, | |
| 78 | + sourceUrl: sourceUrl.toString(), | |
| 79 | + accessToken, | |
| 80 | + refreshToken: tokens?.refresh_token, | |
| 81 | + scope: tokens?.scope, | |
| 82 | + expiresIn: typeof tokens?.expires_in === "number" ? tokens.expires_in : undefined, | |
| 83 | + }); | |
| 82 | 84 | } |
| 83 | 85 | |
| 84 | 86 | const authorizationUrl = provider.getAuthorizationUrl(); |
| 85 | 87 | if (!authorizationUrl) { |
| 86 | − return badPopupResponse("Server did not request an OAuth authorization step"); | |
| 88 | + return badPopupResponse(request, "Server did not request an OAuth authorization step"); | |
| 87 | 89 | } |
| 88 | 90 | |
| 89 | 91 | const pendingCookie = encodePendingCookieValue(provider.toPending(sourceUrl.toString())); |
| @@ -14,7 +14,18 @@ export type McpOAuthPending = { · 13 unmodified lines | ||
| 14 | 14 | clientInformation?: OAuthClientInformationMixed; |
| 15 | 15 | }; |
| 16 | 16 | |
| 17 | +export type McpOAuthPopupResult = { | |
| 18 | + ok: boolean; | |
| 19 | + sourceUrl?: string; | |
| 20 | + accessToken?: string; | |
| 21 | + refreshToken?: string; | |
| 22 | + scope?: string; | |
| 23 | + expiresIn?: number; | |
| 24 | + error?: string; | |
| 25 | +}; | |
| 26 | + | |
| 17 | 27 | const COOKIE_PREFIX = "executor_mcp_oauth_"; |
| 28 | +export const MCP_OAUTH_RESULT_COOKIE = "executor_mcp_oauth_result"; | |
| 18 | 29 | |
| 19 | 30 | export function buildPendingCookieName(state: string): string { |
| 20 | 31 | return `${COOKIE_PREFIX}${state}`; |
| @@ -52,6 +63,32 @@ export function decodePendingCookieValue(raw: string): McpOAuthPending | null { · 31 unmodified lines | ||
| 52 | 63 | } |
| 53 | 64 | } |
| 54 | 65 | |
| 66 | +export function encodePopupResultCookieValue(result: McpOAuthPopupResult): string { | |
| 67 | + return Buffer.from(JSON.stringify(result), "utf8").toString("base64url"); | |
| 68 | +} | |
| 69 | + | |
| 70 | +export function decodePopupResultCookieValue(raw: string): McpOAuthPopupResult | null { | |
| 71 | + try { | |
| 72 | + const decoded = Buffer.from(raw, "base64url").toString("utf8"); | |
| 73 | + const parsed = JSON.parse(decoded) as Partial<McpOAuthPopupResult>; | |
| 74 | + if (typeof parsed.ok !== "boolean") { | |
| 75 | + return null; | |
| 76 | + } | |
| 77 | + | |
| 78 | + return { | |
| 79 | + ok: parsed.ok, | |
| 80 | + ...(typeof parsed.sourceUrl === "string" ? { sourceUrl: parsed.sourceUrl } : {}), | |
| 81 | + ...(typeof parsed.accessToken === "string" ? { accessToken: parsed.accessToken } : {}), | |
| 82 | + ...(typeof parsed.refreshToken === "string" ? { refreshToken: parsed.refreshToken } : {}), | |
| 83 | + ...(typeof parsed.scope === "string" ? { scope: parsed.scope } : {}), | |
| 84 | + ...(typeof parsed.expiresIn === "number" ? { expiresIn: parsed.expiresIn } : {}), | |
| 85 | + ...(typeof parsed.error === "string" ? { error: parsed.error } : {}), | |
| 86 | + }; | |
| 87 | + } catch { | |
| 88 | + return null; | |
| 89 | + } | |
| 90 | +} | |
| 91 | + | |
| 55 | 92 | export class McpPopupOAuthProvider implements OAuthClientProvider { |
| 56 | 93 | public clientMetadata: OAuthClientMetadata; |
| 57 | 94 | private stateValue: string; |
| @@ -139,55 +176,3 @@ export class McpPopupOAuthProvider implements OAuthClientProvider { · 81 unmodified lines | ||
| 139 | 176 | return this.tokenValue; |
| 140 | 177 | } |
| 141 | 178 | } |
| 142 | − | |
| 143 | −export function oauthPopupResultHtml(payload: { | |
| 144 | − ok: boolean; | |
| 145 | − sourceUrl?: string; | |
| 146 | − accessToken?: string; | |
| 147 | − refreshToken?: string; | |
| 148 | − scope?: string; | |
| 149 | − expiresIn?: number; | |
| 150 | − error?: string; | |
| 151 | −}): string { | |
| 152 | − const message = payload.ok | |
| 153 | − ? { | |
| 154 | − type: "executor:mcp-oauth-result", | |
| 155 | − ok: true, | |
| 156 | − sourceUrl: payload.sourceUrl, | |
| 157 | − payload: { | |
| 158 | − accessToken: payload.accessToken, | |
| 159 | − ...(payload.refreshToken ? { refreshToken: payload.refreshToken } : {}), | |
| 160 | − ...(payload.scope ? { scope: payload.scope } : {}), | |
| 161 | − ...(typeof payload.expiresIn === "number" ? { expiresIn: payload.expiresIn } : {}), | |
| 162 | − }, | |
| 163 | − } | |
| 164 | − : { | |
| 165 | − type: "executor:mcp-oauth-result", | |
| 166 | − ok: false, | |
| 167 | − error: payload.error ?? "OAuth failed", | |
| 168 | − }; | |
| 169 | − | |
| 170 | − const serialized = JSON.stringify(message); | |
| 171 | − return `<!doctype html> | |
| 172 | −<html> | |
| 173 | − <head> | |
| 174 | − <meta charset="utf-8" /> | |
| 175 | − <title>MCP OAuth</title> | |
| 176 | − </head> | |
| 177 | − <body> | |
| 178 | − <script> | |
| 179 | − (function () { | |
| 180 | − const msg = ${serialized}; | |
| 181 | − try { | |
| 182 | − if (window.opener) { | |
| 183 | − window.opener.postMessage(msg, window.location.origin); | |
| 184 | − } | |
| 185 | − } finally { | |
| 186 | − window.close(); | |
| 187 | − } | |
| 188 | − })(); | |
| 189 | − </script> | |
| 190 | − <p>You can close this window.</p> | |
| 191 | − </body> | |
| 192 | −</html>`; | |
| 193 | −} | |