Unified · Split
| @@ -4,26 +4,34 @@ import { auth } from "@modelcontextprotocol/sdk/client/auth.js"; · 3 unmodified lines | |||
| 4 | import { | 4 | import { |
| 5 | buildPendingCookieName, | 5 | buildPendingCookieName, |
| 6 | decodePendingCookieValue, | 6 | decodePendingCookieValue, |
| 7 | encodePopupResultCookieValue, | ||
| 8 | MCP_OAUTH_RESULT_COOKIE, | ||
| 7 | McpPopupOAuthProvider, | 9 | McpPopupOAuthProvider, |
| 8 | oauthPopupResultHtml, | 10 | type McpOAuthPopupResult, |
| 9 | } from "@/lib/mcp-oauth-provider"; | 11 | } from "@/lib/mcp-oauth-provider"; |
| 10 | 12 | ||
| 11 | function popupHtmlResponse(payload: { | 13 | function popupResultRedirect( |
| 12 | ok: boolean; | 14 | request: NextRequest, |
| 13 | sourceUrl?: string; | 15 | pendingCookieName: string | null, |
| 14 | accessToken?: string; | 16 | payload: McpOAuthPopupResult, |
| 15 | refreshToken?: string; | 17 | ): NextResponse { |
| 16 | scope?: string; | 18 | const host = request.headers.get("x-forwarded-host") ?? request.headers.get("host"); |
| 17 | expiresIn?: number; | 19 | const proto = request.headers.get("x-forwarded-proto") ?? request.nextUrl.protocol.replace(":", ""); |
| 18 | error?: string; | 20 | const origin = host && proto ? `${proto}://${host}` : request.nextUrl.origin; |
| 19 | }): NextResponse { | 21 | const response = NextResponse.redirect(`${origin}/mcp/oauth/complete`); |
| 20 | return new NextResponse(oauthPopupResultHtml(payload), { | 22 | response.cookies.set({ |
| 21 | status: payload.ok ? 200 : 400, | 23 | name: MCP_OAUTH_RESULT_COOKIE, |
| 22 | headers: { | 24 | value: encodePopupResultCookieValue(payload), |
| 23 | "content-type": "text/html; charset=utf-8", | 25 | httpOnly: true, |
| 24 | "cache-control": "no-store", | 26 | secure: request.nextUrl.protocol === "https:", |
| 25 | }, | 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 | export async function GET(request: NextRequest) { | 37 | export async function GET(request: NextRequest) { |
| @@ -32,7 +40,7 @@ export async function GET(request: NextRequest) { · 2 unmodified lines | |||
| 32 | const error = request.nextUrl.searchParams.get("error")?.trim(); | 40 | const error = request.nextUrl.searchParams.get("error")?.trim(); |
| 33 | 41 | ||
| 34 | if (!state) { | 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 | const cookieName = buildPendingCookieName(state); | 46 | const cookieName = buildPendingCookieName(state); |
| @@ -40,30 +48,28 @@ export async function GET(request: NextRequest) { · 1 unmodified line | |||
| 40 | const pending = rawPending ? decodePendingCookieValue(rawPending) : null; | 48 | const pending = rawPending ? decodePendingCookieValue(rawPending) : null; |
| 41 | 49 | ||
| 42 | if (!pending) { | 50 | if (!pending) { |
| 43 | const response = popupHtmlResponse({ ok: false, error: "OAuth session expired. Try connecting again." }); | 51 | return popupResultRedirect(request, cookieName, { |
| 44 | response.cookies.delete(cookieName); | 52 | ok: false, |
| 45 | return response; | 53 | error: "OAuth session expired. Try connecting again.", |
| 54 | }); | ||
| 46 | } | 55 | } |
| 47 | 56 | ||
| 48 | if (error) { | 57 | if (error) { |
| 49 | const response = popupHtmlResponse({ ok: false, error: `OAuth error: ${error}` }); | 58 | return popupResultRedirect(request, cookieName, { ok: false, error: `OAuth error: ${error}` }); |
| 50 | response.cookies.delete(cookieName); | ||
| 51 | return response; | ||
| 52 | } | 59 | } |
| 53 | 60 | ||
| 54 | if (!code) { | 61 | if (!code) { |
| 55 | const response = popupHtmlResponse({ ok: false, error: "Missing OAuth authorization code" }); | 62 | return popupResultRedirect(request, cookieName, { |
| 56 | response.cookies.delete(cookieName); | 63 | ok: false, |
| 57 | return response; | 64 | error: "Missing OAuth authorization code", |
| 65 | }); | ||
| 58 | } | 66 | } |
| 59 | 67 | ||
| 60 | let sourceUrl: URL; | 68 | let sourceUrl: URL; |
| 61 | try { | 69 | try { |
| 62 | sourceUrl = new URL(pending.sourceUrl); | 70 | sourceUrl = new URL(pending.sourceUrl); |
| 63 | } catch { | 71 | } catch { |
| 64 | const response = popupHtmlResponse({ ok: false, error: "Invalid MCP source URL" }); | 72 | return popupResultRedirect(request, cookieName, { ok: false, error: "Invalid MCP source URL" }); |
| 65 | response.cookies.delete(cookieName); | ||
| 66 | return response; | ||
| 67 | } | 73 | } |
| 68 | 74 | ||
| 69 | const provider = new McpPopupOAuthProvider({ | 75 | const provider = new McpPopupOAuthProvider({ |
| @@ -79,23 +85,22 @@ export async function GET(request: NextRequest) { · 9 unmodified lines | |||
| 79 | authorizationCode: code, | 85 | authorizationCode: code, |
| 80 | }); | 86 | }); |
| 81 | } catch (finishError) { | 87 | } catch (finishError) { |
| 82 | const response = popupHtmlResponse({ | 88 | return popupResultRedirect(request, cookieName, { |
| 83 | ok: false, | 89 | ok: false, |
| 84 | error: finishError instanceof Error ? finishError.message : "Failed to finish OAuth", | 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 | const tokens = provider.getTokens(); | 94 | const tokens = provider.getTokens(); |
| 91 | const accessToken = tokens?.access_token?.trim() ?? ""; | 95 | const accessToken = tokens?.access_token?.trim() ?? ""; |
| 92 | if (!accessToken) { | 96 | if (!accessToken) { |
| 93 | const response = popupHtmlResponse({ ok: false, error: "OAuth completed without an access token" }); | 97 | return popupResultRedirect(request, cookieName, { |
| 94 | response.cookies.delete(cookieName); | 98 | ok: false, |
| 95 | return response; | 99 | error: "OAuth completed without an access token", |
| 100 | }); | ||
| 96 | } | 101 | } |
| 97 | 102 | ||
| 98 | const response = popupHtmlResponse({ | 103 | return popupResultRedirect(request, cookieName, { |
| 99 | ok: true, | 104 | ok: true, |
| 100 | sourceUrl: pending.sourceUrl, | 105 | sourceUrl: pending.sourceUrl, |
| 101 | accessToken, | 106 | accessToken, |
| @@ -103,6 +108,4 @@ export async function GET(request: NextRequest) { · 1 unmodified line | |||
| 103 | scope: tokens?.scope, | 108 | scope: tokens?.scope, |
| 104 | expiresIn: typeof tokens?.expires_in === "number" ? tokens.expires_in : undefined, | 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 | buildPendingCookieName, | 5 | buildPendingCookieName, |
| 6 | createOAuthState, | 6 | createOAuthState, |
| 7 | encodePendingCookieValue, | 7 | encodePendingCookieValue, |
| 8 | encodePopupResultCookieValue, | ||
| 9 | MCP_OAUTH_RESULT_COOKIE, | ||
| 8 | McpPopupOAuthProvider, | 10 | McpPopupOAuthProvider, |
| 9 | oauthPopupResultHtml, | 11 | type McpOAuthPopupResult, |
| 10 | } from "@/lib/mcp-oauth-provider"; | 12 | } from "@/lib/mcp-oauth-provider"; |
| 11 | 13 | ||
| 12 | function getExternalOrigin(request: NextRequest): string { | 14 | function getExternalOrigin(request: NextRequest): string { |
| @@ -18,27 +20,36 @@ function getExternalOrigin(request: NextRequest): string { · 5 unmodified lines | |||
| 18 | return request.nextUrl.origin; | 20 | return request.nextUrl.origin; |
| 19 | } | 21 | } |
| 20 | 22 | ||
| 21 | function badPopupResponse(message: string): NextResponse { | 23 | function popupResultRedirect(request: NextRequest, payload: McpOAuthPopupResult): NextResponse { |
| 22 | return new NextResponse(oauthPopupResultHtml({ ok: false, error: message }), { | 24 | const externalOrigin = getExternalOrigin(request); |
| 23 | status: 400, | 25 | const response = NextResponse.redirect(`${externalOrigin}/mcp/oauth/complete`); |
| 24 | headers: { | 26 | response.cookies.set({ |
| 25 | "content-type": "text/html; charset=utf-8", | 27 | name: MCP_OAUTH_RESULT_COOKIE, |
| 26 | "cache-control": "no-store", | 28 | value: encodePopupResultCookieValue(payload), |
| 27 | }, | 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 | export async function GET(request: NextRequest) { | 42 | export async function GET(request: NextRequest) { |
| 32 | const sourceUrlRaw = request.nextUrl.searchParams.get("sourceUrl")?.trim() ?? ""; | 43 | const sourceUrlRaw = request.nextUrl.searchParams.get("sourceUrl")?.trim() ?? ""; |
| 33 | if (!sourceUrlRaw) { | 44 | if (!sourceUrlRaw) { |
| 34 | return badPopupResponse("Missing sourceUrl"); | 45 | return badPopupResponse(request, "Missing sourceUrl"); |
| 35 | } | 46 | } |
| 36 | 47 | ||
| 37 | let sourceUrl: URL; | 48 | let sourceUrl: URL; |
| 38 | try { | 49 | try { |
| 39 | sourceUrl = new URL(sourceUrlRaw); | 50 | sourceUrl = new URL(sourceUrlRaw); |
| 40 | } catch { | 51 | } catch { |
| 41 | return badPopupResponse("Invalid sourceUrl"); | 52 | return badPopupResponse(request, "Invalid sourceUrl"); |
| 42 | } | 53 | } |
| 43 | 54 | ||
| 44 | const state = createOAuthState(); | 55 | const state = createOAuthState(); |
| @@ -53,37 +64,28 @@ export async function GET(request: NextRequest) { · 8 unmodified lines | |||
| 53 | authResult = await auth(provider, { serverUrl: sourceUrl }); | 64 | authResult = await auth(provider, { serverUrl: sourceUrl }); |
| 54 | } catch (error) { | 65 | } catch (error) { |
| 55 | const message = error instanceof Error ? error.message : "Failed to start OAuth flow"; | 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 | if (authResult === "AUTHORIZED") { | 70 | if (authResult === "AUTHORIZED") { |
| 60 | const tokens = provider.getTokens(); | 71 | const tokens = provider.getTokens(); |
| 61 | const accessToken = tokens?.access_token?.trim() ?? ""; | 72 | const accessToken = tokens?.access_token?.trim() ?? ""; |
| 62 | if (!accessToken) { | 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( | 76 | return popupResultRedirect(request, { |
| 66 | oauthPopupResultHtml({ | 77 | ok: true, |
| 67 | ok: true, | 78 | sourceUrl: sourceUrl.toString(), |
| 68 | sourceUrl: sourceUrl.toString(), | 79 | accessToken, |
| 69 | accessToken, | 80 | refreshToken: tokens?.refresh_token, |
| 70 | refreshToken: tokens?.refresh_token, | 81 | scope: tokens?.scope, |
| 71 | scope: tokens?.scope, | 82 | expiresIn: typeof tokens?.expires_in === "number" ? tokens.expires_in : undefined, |
| 72 | expiresIn: typeof tokens?.expires_in === "number" ? tokens.expires_in : undefined, | 83 | }); |
| 73 | }), | ||
| 74 | { | ||
| 75 | status: 200, | ||
| 76 | headers: { | ||
| 77 | "content-type": "text/html; charset=utf-8", | ||
| 78 | "cache-control": "no-store", | ||
| 79 | }, | ||
| 80 | }, | ||
| 81 | ); | ||
| 82 | } | 84 | } |
| 83 | 85 | ||
| 84 | const authorizationUrl = provider.getAuthorizationUrl(); | 86 | const authorizationUrl = provider.getAuthorizationUrl(); |
| 85 | if (!authorizationUrl) { | 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 | const pendingCookie = encodePendingCookieValue(provider.toPending(sourceUrl.toString())); | 91 | const pendingCookie = encodePendingCookieValue(provider.toPending(sourceUrl.toString())); |
| @@ -14,7 +14,18 @@ export type McpOAuthPending = { · 13 unmodified lines | |||
| 14 | clientInformation?: OAuthClientInformationMixed; | 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 | const COOKIE_PREFIX = "executor_mcp_oauth_"; | 27 | const COOKIE_PREFIX = "executor_mcp_oauth_"; |
| 28 | export const MCP_OAUTH_RESULT_COOKIE = "executor_mcp_oauth_result"; | ||
| 18 | 29 | ||
| 19 | export function buildPendingCookieName(state: string): string { | 30 | export function buildPendingCookieName(state: string): string { |
| 20 | return `${COOKIE_PREFIX}${state}`; | 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 | export class McpPopupOAuthProvider implements OAuthClientProvider { | 92 | export class McpPopupOAuthProvider implements OAuthClientProvider { |
| 56 | public clientMetadata: OAuthClientMetadata; | 93 | public clientMetadata: OAuthClientMetadata; |
| 57 | private stateValue: string; | 94 | private stateValue: string; |
| @@ -139,55 +176,3 @@ export class McpPopupOAuthProvider implements OAuthClientProvider { · 81 unmodified lines | |||
| 139 | return this.tokenValue; | 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 | } | ||