Unified · Split
| @@ -92,6 +92,7 @@ · 91 unmodified lines | ||
| 92 | 92 | "@streamdown/code": "^1.0.1", |
| 93 | 93 | "@tanstack/react-query": "^5.80.7", |
| 94 | 94 | "@workos-inc/authkit-nextjs": "^2.13.0", |
| 95 | + "better-result": "^2.7.0", | |
| 95 | 96 | "class-variance-authority": "^0.7.1", |
| 96 | 97 | "clsx": "^2.1.1", |
| 97 | 98 | "convex": "^1.31.7", |
| @@ -17,6 +17,7 @@ · 16 unmodified lines | ||
| 17 | 17 | "@streamdown/code": "^1.0.1", |
| 18 | 18 | "@tanstack/react-query": "^5.80.7", |
| 19 | 19 | "@workos-inc/authkit-nextjs": "^2.13.0", |
| 20 | + "better-result": "^2.7.0", | |
| 20 | 21 | "class-variance-authority": "^0.7.1", |
| 21 | 22 | "clsx": "^2.1.1", |
| 22 | 23 | "convex": "^1.31.7", |
| @@ -1,5 +1,6 @@ | ||
| 1 | 1 | import type { NextRequest } from "next/server"; |
| 2 | 2 | import { NextResponse } from "next/server"; |
| 3 | +import { Result } from "better-result"; | |
| 3 | 4 | import { auth } from "@modelcontextprotocol/sdk/client/auth.js"; |
| 4 | 5 | import { |
| 5 | 6 | buildPendingCookieName, |
| @@ -34,6 +35,19 @@ function popupResultRedirect( · 28 unmodified lines | ||
| 34 | 35 | return response; |
| 35 | 36 | } |
| 36 | 37 | |
| 38 | +function resultErrorMessage(error: unknown, fallback: string): string { | |
| 39 | + const cause = typeof error === "object" && error && "cause" in error | |
| 40 | + ? (error as { cause?: unknown }).cause | |
| 41 | + : error; | |
| 42 | + if (cause instanceof Error && cause.message.trim()) { | |
| 43 | + return cause.message; | |
| 44 | + } | |
| 45 | + if (typeof cause === "string" && cause.trim()) { | |
| 46 | + return cause; | |
| 47 | + } | |
| 48 | + return fallback; | |
| 49 | +} | |
| 50 | + | |
| 37 | 51 | export async function GET(request: NextRequest) { |
| 38 | 52 | const code = request.nextUrl.searchParams.get("code")?.trim() ?? ""; |
| 39 | 53 | const state = request.nextUrl.searchParams.get("state")?.trim() ?? ""; |
| @@ -65,12 +79,11 @@ export async function GET(request: NextRequest) { · 25 unmodified lines | ||
| 65 | 79 | }); |
| 66 | 80 | } |
| 67 | 81 | |
| 68 | − let sourceUrl: URL; | |
| 69 | − try { | |
| 70 | − sourceUrl = new URL(pending.sourceUrl); | |
| 71 | − } catch { | |
| 82 | + const sourceUrlResult = Result.try(() => new URL(pending.sourceUrl)); | |
| 83 | + if (!sourceUrlResult.isOk()) { | |
| 72 | 84 | return popupResultRedirect(request, cookieName, { ok: false, error: "Invalid MCP source URL" }); |
| 73 | 85 | } |
| 86 | + const sourceUrl = sourceUrlResult.value; | |
| 74 | 87 | |
| 75 | 88 | const provider = new McpPopupOAuthProvider({ |
| 76 | 89 | redirectUrl: pending.redirectUrl, |
| @@ -79,15 +92,14 @@ export async function GET(request: NextRequest) { · 2 unmodified lines | ||
| 79 | 92 | clientInformation: pending.clientInformation, |
| 80 | 93 | }); |
| 81 | 94 | |
| 82 | − try { | |
| 83 | − await auth(provider, { | |
| 95 | + const authResult = await Result.tryPromise(() => auth(provider, { | |
| 84 | 96 | serverUrl: sourceUrl, |
| 85 | 97 | authorizationCode: code, |
| 86 | − }); | |
| 87 | − } catch (finishError) { | |
| 98 | + })); | |
| 99 | + if (!authResult.isOk()) { | |
| 88 | 100 | return popupResultRedirect(request, cookieName, { |
| 89 | 101 | ok: false, |
| 90 | − error: finishError instanceof Error ? finishError.message : "Failed to finish OAuth", | |
| 102 | + error: resultErrorMessage(authResult.error, "Failed to finish OAuth"), | |
| 91 | 103 | }); |
| 92 | 104 | } |
| 93 | 105 | |
| @@ -1,5 +1,6 @@ | ||
| 1 | 1 | import type { NextRequest } from "next/server"; |
| 2 | 2 | import { NextResponse } from "next/server"; |
| 3 | +import { Result } from "better-result"; | |
| 3 | 4 | import { discoverOAuthProtectedResourceMetadata } from "@modelcontextprotocol/sdk/client/auth.js"; |
| 4 | 5 | |
| 5 | 6 | type DetectResponse = { |
| @@ -17,37 +18,49 @@ function noStoreJson(payload: DetectResponse, status = 200): NextResponse { · 11 unmodified lines | ||
| 17 | 18 | }); |
| 18 | 19 | } |
| 19 | 20 | |
| 21 | +function resultErrorMessage(error: unknown, fallback: string): string { | |
| 22 | + const cause = typeof error === "object" && error && "cause" in error | |
| 23 | + ? (error as { cause?: unknown }).cause | |
| 24 | + : error; | |
| 25 | + if (cause instanceof Error && cause.message.trim()) { | |
| 26 | + return cause.message; | |
| 27 | + } | |
| 28 | + if (typeof cause === "string" && cause.trim()) { | |
| 29 | + return cause; | |
| 30 | + } | |
| 31 | + return fallback; | |
| 32 | +} | |
| 33 | + | |
| 20 | 34 | export async function GET(request: NextRequest) { |
| 21 | 35 | const sourceUrlRaw = request.nextUrl.searchParams.get("sourceUrl")?.trim() ?? ""; |
| 22 | 36 | if (!sourceUrlRaw) { |
| 23 | 37 | return noStoreJson({ oauth: false, authorizationServers: [], detail: "Missing sourceUrl" }, 400); |
| 24 | 38 | } |
| 25 | 39 | |
| 26 | − let sourceUrl: URL; | |
| 27 | − try { | |
| 28 | − sourceUrl = new URL(sourceUrlRaw); | |
| 29 | − } catch { | |
| 40 | + const sourceUrlResult = Result.try(() => new URL(sourceUrlRaw)); | |
| 41 | + if (!sourceUrlResult.isOk()) { | |
| 30 | 42 | return noStoreJson({ oauth: false, authorizationServers: [], detail: "Invalid sourceUrl" }, 400); |
| 31 | 43 | } |
| 44 | + const sourceUrl = sourceUrlResult.value; | |
| 32 | 45 | |
| 33 | − try { | |
| 34 | − const metadata = await discoverOAuthProtectedResourceMetadata(sourceUrl); | |
| 35 | − const authorizationServers = Array.isArray((metadata as { authorization_servers?: unknown }).authorization_servers) | |
| 36 | − ? (metadata as { authorization_servers: unknown[] }).authorization_servers.filter( | |
| 37 | − (entry): entry is string => typeof entry === "string" && entry.trim().length > 0, | |
| 38 | − ) | |
| 39 | − : []; | |
| 40 | − | |
| 41 | − return noStoreJson({ | |
| 42 | − oauth: authorizationServers.length > 0, | |
| 43 | − authorizationServers, | |
| 44 | − }); | |
| 45 | − } catch (error) { | |
| 46 | − const message = error instanceof Error ? error.message : "OAuth metadata lookup failed"; | |
| 46 | + const metadataResult = await Result.tryPromise(() => discoverOAuthProtectedResourceMetadata(sourceUrl)); | |
| 47 | + if (!metadataResult.isOk()) { | |
| 47 | 48 | return noStoreJson({ |
| 48 | 49 | oauth: false, |
| 49 | 50 | authorizationServers: [], |
| 50 | − detail: message, | |
| 51 | + detail: resultErrorMessage(metadataResult.error, "OAuth metadata lookup failed"), | |
| 51 | 52 | }); |
| 52 | 53 | } |
| 54 | + | |
| 55 | + const metadata = metadataResult.value; | |
| 56 | + const authorizationServers = Array.isArray((metadata as { authorization_servers?: unknown }).authorization_servers) | |
| 57 | + ? (metadata as { authorization_servers: unknown[] }).authorization_servers.filter( | |
| 58 | + (entry): entry is string => typeof entry === "string" && entry.trim().length > 0, | |
| 59 | + ) | |
| 60 | + : []; | |
| 61 | + | |
| 62 | + return noStoreJson({ | |
| 63 | + oauth: authorizationServers.length > 0, | |
| 64 | + authorizationServers, | |
| 65 | + }); | |
| 53 | 66 | } |
| @@ -1,5 +1,6 @@ | ||
| 1 | 1 | import type { NextRequest } from "next/server"; |
| 2 | 2 | import { NextResponse } from "next/server"; |
| 3 | +import { Result } from "better-result"; | |
| 3 | 4 | import { auth } from "@modelcontextprotocol/sdk/client/auth.js"; |
| 4 | 5 | import { |
| 5 | 6 | buildPendingCookieName, |
| @@ -39,18 +40,30 @@ function badPopupResponse(request: NextRequest, message: string): NextResponse { · 33 unmodified lines | ||
| 39 | 40 | return popupResultRedirect(request, { ok: false, error: message }); |
| 40 | 41 | } |
| 41 | 42 | |
| 43 | +function resultErrorMessage(error: unknown, fallback: string): string { | |
| 44 | + const cause = typeof error === "object" && error && "cause" in error | |
| 45 | + ? (error as { cause?: unknown }).cause | |
| 46 | + : error; | |
| 47 | + if (cause instanceof Error && cause.message.trim()) { | |
| 48 | + return cause.message; | |
| 49 | + } | |
| 50 | + if (typeof cause === "string" && cause.trim()) { | |
| 51 | + return cause; | |
| 52 | + } | |
| 53 | + return fallback; | |
| 54 | +} | |
| 55 | + | |
| 42 | 56 | export async function GET(request: NextRequest) { |
| 43 | 57 | const sourceUrlRaw = request.nextUrl.searchParams.get("sourceUrl")?.trim() ?? ""; |
| 44 | 58 | if (!sourceUrlRaw) { |
| 45 | 59 | return badPopupResponse(request, "Missing sourceUrl"); |
| 46 | 60 | } |
| 47 | 61 | |
| 48 | − let sourceUrl: URL; | |
| 49 | − try { | |
| 50 | − sourceUrl = new URL(sourceUrlRaw); | |
| 51 | − } catch { | |
| 62 | + const sourceUrlResult = Result.try(() => new URL(sourceUrlRaw)); | |
| 63 | + if (!sourceUrlResult.isOk()) { | |
| 52 | 64 | return badPopupResponse(request, "Invalid sourceUrl"); |
| 53 | 65 | } |
| 66 | + const sourceUrl = sourceUrlResult.value; | |
| 54 | 67 | |
| 55 | 68 | const state = createOAuthState(); |
| 56 | 69 | const redirectUrl = `${getExternalOrigin(request)}/mcp/oauth/callback`; |
| @@ -59,15 +72,12 @@ export async function GET(request: NextRequest) { · 2 unmodified lines | ||
| 59 | 72 | state, |
| 60 | 73 | }); |
| 61 | 74 | |
| 62 | − let authResult: "AUTHORIZED" | "REDIRECT"; | |
| 63 | − try { | |
| 64 | − authResult = await auth(provider, { serverUrl: sourceUrl }); | |
| 65 | − } catch (error) { | |
| 66 | − const message = error instanceof Error ? error.message : "Failed to start OAuth flow"; | |
| 67 | − return badPopupResponse(request, message); | |
| 75 | + const authResult = await Result.tryPromise(() => auth(provider, { serverUrl: sourceUrl })); | |
| 76 | + if (!authResult.isOk()) { | |
| 77 | + return badPopupResponse(request, resultErrorMessage(authResult.error, "Failed to start OAuth flow")); | |
| 68 | 78 | } |
| 69 | 79 | |
| 70 | − if (authResult === "AUTHORIZED") { | |
| 80 | + if (authResult.value === "AUTHORIZED") { | |
| 71 | 81 | const tokens = provider.getTokens(); |
| 72 | 82 | const accessToken = tokens?.access_token?.trim() ?? ""; |
| 73 | 83 | if (!accessToken) { |
| @@ -3,6 +3,7 @@ · 2 unmodified lines | ||
| 3 | 3 | import { useState, type ReactNode } from "react"; |
| 4 | 4 | import { Plus } from "lucide-react"; |
| 5 | 5 | import { useAction, useMutation, useQuery } from "convex/react"; |
| 6 | +import { Result } from "better-result"; | |
| 6 | 7 | import { toast } from "sonner"; |
| 7 | 8 | import { Button } from "@/components/ui/button"; |
| 8 | 9 | import { |
| @@ -28,6 +29,19 @@ import { · 19 unmodified lines | ||
| 28 | 29 | } from "./use-add-source-form-state"; |
| 29 | 30 | import { saveSourceWithCredentials } from "./add-source-submit"; |
| 30 | 31 | |
| 32 | +function resultErrorMessage(error: unknown, fallback: string): string { | |
| 33 | + const cause = typeof error === "object" && error && "cause" in error | |
| 34 | + ? (error as { cause?: unknown }).cause | |
| 35 | + : error; | |
| 36 | + if (cause instanceof Error && cause.message.trim()) { | |
| 37 | + return cause.message; | |
| 38 | + } | |
| 39 | + if (typeof cause === "string" && cause.trim()) { | |
| 40 | + return cause; | |
| 41 | + } | |
| 42 | + return fallback; | |
| 43 | +} | |
| 44 | + | |
| 31 | 45 | export function AddSourceDialog({ |
| 32 | 46 | existingSourceNames, |
| 33 | 47 | onSourceAdded, |
| @@ -87,8 +101,8 @@ export function AddSourceDialog({ · 53 unmodified lines | ||
| 87 | 101 | } |
| 88 | 102 | |
| 89 | 103 | setSubmitting(true); |
| 90 | − try { | |
| 91 | − const result = await saveSourceWithCredentials({ | |
| 104 | + const saveResult = await Result.tryPromise(() => | |
| 105 | + saveSourceWithCredentials({ | |
| 92 | 106 | context, |
| 93 | 107 | sourceToEdit, |
| 94 | 108 | credentialsLoading, |
| @@ -108,28 +122,34 @@ export function AddSourceDialog({ · 13 unmodified lines | ||
| 108 | 122 | hasCredentialInput: form.hasCredentialInput, |
| 109 | 123 | buildSecretJson: form.buildSecretJson, |
| 110 | 124 | }, |
| 111 | − }); | |
| 112 | − | |
| 113 | − onSourceAdded?.(result.source, { connected: result.connected }); | |
| 114 | − toast.success( | |
| 115 | − sourceToEdit | |
| 116 | − ? result.connected | |
| 117 | − ? `Source "${form.name.trim()}" updated with credentials` | |
| 118 | − : `Source "${form.name.trim()}" updated` | |
| 119 | − : result.connected | |
| 120 | − ? `Source "${form.name.trim()}" added with credentials — loading tools…` | |
| 121 | − : `Source "${form.name.trim()}" added — loading tools…`, | |
| 122 | − ); | |
| 123 | − if (!sourceToEdit) { | |
| 124 | − form.reserveSourceName(form.name.trim()); | |
| 125 | − } | |
| 126 | − form.setView("catalog"); | |
| 127 | − setOpen(false); | |
| 128 | − } catch (err) { | |
| 129 | − toast.error(err instanceof Error ? err.message : sourceToEdit ? "Failed to update source" : "Failed to add source"); | |
| 130 | − } finally { | |
| 131 | − setSubmitting(false); | |
| 125 | + }) | |
| 126 | + ); | |
| 127 | + setSubmitting(false); | |
| 128 | + | |
| 129 | + if (!saveResult.isOk()) { | |
| 130 | + toast.error(resultErrorMessage( | |
| 131 | + saveResult.error, | |
| 132 | + sourceToEdit ? "Failed to update source" : "Failed to add source", | |
| 133 | + )); | |
| 134 | + return; | |
| 135 | + } | |
| 136 | + | |
| 137 | + const result = saveResult.value; | |
| 138 | + onSourceAdded?.(result.source, { connected: result.connected }); | |
| 139 | + toast.success( | |
| 140 | + sourceToEdit | |
| 141 | + ? result.connected | |
| 142 | + ? `Source "${form.name.trim()}" updated with credentials` | |
| 143 | + : `Source "${form.name.trim()}" updated` | |
| 144 | + : result.connected | |
| 145 | + ? `Source "${form.name.trim()}" added with credentials — loading tools…` | |
| 146 | + : `Source "${form.name.trim()}" added — loading tools…`, | |
| 147 | + ); | |
| 148 | + if (!sourceToEdit) { | |
| 149 | + form.reserveSourceName(form.name.trim()); | |
| 132 | 150 | } |
| 151 | + form.setView("catalog"); | |
| 152 | + setOpen(false); | |
| 133 | 153 | }; |
| 134 | 154 | |
| 135 | 155 | const handleMcpOAuthConnect = async () => { |
| @@ -143,18 +163,19 @@ export function AddSourceDialog({ · 7 unmodified lines | ||
| 143 | 163 | } |
| 144 | 164 | |
| 145 | 165 | setMcpOAuthBusy(true); |
| 146 | − try { | |
| 147 | − const result = await startMcpOAuthPopup(endpoint); | |
| 148 | − if (form.authType !== "bearer") { | |
| 149 | − form.handleAuthTypeChange("bearer"); | |
| 150 | − } | |
| 151 | − form.handleAuthFieldChange("tokenValue", result.accessToken); | |
| 152 | − toast.success("OAuth linked successfully."); | |
| 153 | − } catch (error) { | |
| 154 | − toast.error(error instanceof Error ? error.message : "Failed to connect OAuth"); | |
| 155 | − } finally { | |
| 156 | − setMcpOAuthBusy(false); | |
| 166 | + const oauthResult = await Result.tryPromise(() => startMcpOAuthPopup(endpoint)); | |
| 167 | + setMcpOAuthBusy(false); | |
| 168 | + | |
| 169 | + if (!oauthResult.isOk()) { | |
| 170 | + toast.error(resultErrorMessage(oauthResult.error, "Failed to connect OAuth")); | |
| 171 | + return; | |
| 172 | + } | |
| 173 | + | |
| 174 | + if (form.authType !== "bearer") { | |
| 175 | + form.handleAuthTypeChange("bearer"); | |
| 157 | 176 | } |
| 177 | + form.handleAuthFieldChange("tokenValue", oauthResult.value.accessToken); | |
| 178 | + toast.success("OAuth linked successfully."); | |
| 158 | 179 | }; |
| 159 | 180 | |
| 160 | 181 | const dialogTitle = sourceToEdit ? "Edit Tool Source" : "Add Tool Source"; |
| @@ -53,10 +53,8 @@ export async function startMcpOAuthPopup(sourceUrl: string): Promise<McpOAuthPop · 52 unmodified lines | ||
| 53 | 53 | const cleanup = () => { |
| 54 | 54 | window.removeEventListener("message", onMessage); |
| 55 | 55 | window.clearInterval(closedPoll); |
| 56 | − try { | |
| 56 | + if (!popup.closed) { | |
| 57 | 57 | popup.close(); |
| 58 | − } catch { | |
| 59 | − // ignore | |
| 60 | 58 | } |
| 61 | 59 | }; |
| 62 | 60 | |
| @@ -1,4 +1,5 @@ | ||
| 1 | 1 | import { randomUUID } from "node:crypto"; |
| 2 | +import { Result } from "better-result"; | |
| 2 | 3 | import type { OAuthClientProvider } from "@modelcontextprotocol/sdk/client/auth.js"; |
| 3 | 4 | import type { |
| 4 | 5 | OAuthClientInformationMixed, |
| @@ -40,27 +41,32 @@ export function encodePendingCookieValue(pending: McpOAuthPending): string { · 35 unmodified lines | ||
| 40 | 41 | } |
| 41 | 42 | |
| 42 | 43 | export function decodePendingCookieValue(raw: string): McpOAuthPending | null { |
| 43 | − try { | |
| 44 | − const decoded = Buffer.from(raw, "base64url").toString("utf8"); | |
| 45 | − const parsed = JSON.parse(decoded) as Partial<McpOAuthPending>; | |
| 46 | − if ( | |
| 47 | − typeof parsed.state !== "string" | |
| 48 | − || typeof parsed.sourceUrl !== "string" | |
| 49 | − || typeof parsed.redirectUrl !== "string" | |
| 50 | − ) { | |
| 51 | − return null; | |
| 52 | − } | |
| 44 | + const decodedResult = Result.try(() => Buffer.from(raw, "base64url").toString("utf8")); | |
| 45 | + if (!decodedResult.isOk()) { | |
| 46 | + return null; | |
| 47 | + } | |
| 53 | 48 | |
| 54 | − return { | |
| 55 | − state: parsed.state, | |
| 56 | − sourceUrl: parsed.sourceUrl, | |
| 57 | − redirectUrl: parsed.redirectUrl, | |
| 58 | − ...(typeof parsed.codeVerifier === "string" ? { codeVerifier: parsed.codeVerifier } : {}), | |
| 59 | − ...(parsed.clientInformation ? { clientInformation: parsed.clientInformation } : {}), | |
| 60 | − }; | |
| 61 | − } catch { | |
| 49 | + const parsedResult = Result.try(() => JSON.parse(decodedResult.value) as Partial<McpOAuthPending>); | |
| 50 | + if (!parsedResult.isOk()) { | |
| 62 | 51 | return null; |
| 63 | 52 | } |
| 53 | + | |
| 54 | + const parsed = parsedResult.value; | |
| 55 | + if ( | |
| 56 | + typeof parsed.state !== "string" | |
| 57 | + || typeof parsed.sourceUrl !== "string" | |
| 58 | + || typeof parsed.redirectUrl !== "string" | |
| 59 | + ) { | |
| 60 | + return null; | |
| 61 | + } | |
| 62 | + | |
| 63 | + return { | |
| 64 | + state: parsed.state, | |
| 65 | + sourceUrl: parsed.sourceUrl, | |
| 66 | + redirectUrl: parsed.redirectUrl, | |
| 67 | + ...(typeof parsed.codeVerifier === "string" ? { codeVerifier: parsed.codeVerifier } : {}), | |
| 68 | + ...(parsed.clientInformation ? { clientInformation: parsed.clientInformation } : {}), | |
| 69 | + }; | |
| 64 | 70 | } |
| 65 | 71 | |
| 66 | 72 | export function encodePopupResultCookieValue(result: McpOAuthPopupResult): string { |
| @@ -68,25 +74,30 @@ export function encodePopupResultCookieValue(result: McpOAuthPopupResult): strin · 1 unmodified line | ||
| 68 | 74 | } |
| 69 | 75 | |
| 70 | 76 | 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 | + const decodedResult = Result.try(() => Buffer.from(raw, "base64url").toString("utf8")); | |
| 78 | + if (!decodedResult.isOk()) { | |
| 79 | + return null; | |
| 80 | + } | |
| 77 | 81 | |
| 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 { | |
| 82 | + const parsedResult = Result.try(() => JSON.parse(decodedResult.value) as Partial<McpOAuthPopupResult>); | |
| 83 | + if (!parsedResult.isOk()) { | |
| 88 | 84 | return null; |
| 89 | 85 | } |
| 86 | + | |
| 87 | + const parsed = parsedResult.value; | |
| 88 | + if (typeof parsed.ok !== "boolean") { | |
| 89 | + return null; | |
| 90 | + } | |
| 91 | + | |
| 92 | + return { | |
| 93 | + ok: parsed.ok, | |
| 94 | + ...(typeof parsed.sourceUrl === "string" ? { sourceUrl: parsed.sourceUrl } : {}), | |
| 95 | + ...(typeof parsed.accessToken === "string" ? { accessToken: parsed.accessToken } : {}), | |
| 96 | + ...(typeof parsed.refreshToken === "string" ? { refreshToken: parsed.refreshToken } : {}), | |
| 97 | + ...(typeof parsed.scope === "string" ? { scope: parsed.scope } : {}), | |
| 98 | + ...(typeof parsed.expiresIn === "number" ? { expiresIn: parsed.expiresIn } : {}), | |
| 99 | + ...(typeof parsed.error === "string" ? { error: parsed.error } : {}), | |
| 100 | + }; | |
| 90 | 101 | } |
| 91 | 102 | |
| 92 | 103 | export class McpPopupOAuthProvider implements OAuthClientProvider { |