### Install MCP SDK and OAuth Libraries (JavaScript/TypeScript) Source: https://developers.notion.com/guides/mcp/build-mcp-client Install the necessary libraries for building an MCP client in JavaScript or TypeScript. This includes the official MCP SDK and an OAuth 2.0 client library. ```bash npm install @modelcontextprotocol/sdk npm install oauth # or openid-client ``` ```bash npm install --save-dev @types/node ``` -------------------------------- ### MCP Server Discovery File Example Source: https://developers.notion.com/guides/mcp/build-mcp-client This JSON file is hosted at /.well-known/mcp.json on a website's domain to allow MCP clients to discover available MCP servers. It provides essential information about the server. ```json { "name": "Notion", "description": "Connect your Notion workspace to search, update, and trigger workflows across tools.", "icon": "https://www.notion.com/images/notion-logo-block-main.svg", "endpoint": "https://mcp.notion.com/mcp" } ``` -------------------------------- ### Configure Notion MCP Server in VS Code Source: https://developers.notion.com/guides/mcp/get-started-with-mcp Create a .vscode/mcp.json file in your workspace to configure the Notion MCP server. Then, use the Command Palette to list and start the server. ```json { "servers": { "notion": { "type": "http", "url": "https://mcp.notion.com/mcp" } } } ``` -------------------------------- ### Complete Notion MCP Client Example Source: https://developers.notion.com/guides/mcp/build-mcp-client This TypeScript class demonstrates a full implementation for initializing, authenticating, connecting, and managing tokens for the Notion MCP client. It includes methods for handling OAuth flows, token refresh, and fallback transport mechanisms. ```typescript import { Client } from "@modelcontextprotocol/sdk/client/index.js" import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js" import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js" import { randomBytes, createHash } from "crypto" class NotionMcpClient { private serverUrl = "https://mcp.notion.com" private metadata!: OAuthMetadata private clientId!: string private clientSecret?: string private accessToken?: string private refreshToken?: string private client?: Client async initialize(redirectUri: string): Promise { this.metadata = await discoverOAuthMetadata(this.serverUrl) const credentials = await registerClient(this.metadata, redirectUri) this.clientId = credentials.client_id this.clientSecret = credentials.client_secret } async startAuthFlow(redirectUri: string): Promise { const codeVerifier = generateCodeVerifier() const codeChallenge = generateCodeChallenge(codeVerifier) const state = generateState() // Store these securely this.storeSecurely("codeVerifier", codeVerifier) this.storeSecurely("state", state) return buildAuthorizationUrl( this.metadata, this.clientId, redirectUri, codeChallenge, state ) } async handleCallback( callbackUrl: string, redirectUri: string ): Promise { const storedState = this.retrieveSecurely("state") const codeVerifier = this.retrieveSecurely("codeVerifier") const code = await handleCallback( callbackUrl, storedState, codeVerifier ) const tokens = await exchangeCodeForTokens( code, codeVerifier, this.metadata, this.clientId, this.clientSecret, redirectUri ) this.accessToken = tokens.access_token this.refreshToken = tokens.refresh_token // Clean up stored values this.deleteSecurely("state") this.deleteSecurely("codeVerifier") } async connect(): Promise { if (!this.accessToken) { throw new Error("Not authenticated") } try { this.client = await createMcpClient( this.serverUrl, this.accessToken, false ) } catch (error) { console.warn("Streamable HTTP failed, falling back to SSE") this.client = await createMcpClient( this.serverUrl, this.accessToken, true ) } return this.client } async ensureValidToken(): Promise { if (!this.refreshToken) { throw new Error("No refresh token available") } try { const tokens = await refreshAccessToken( this.refreshToken, this.metadata, this.clientId, this.clientSecret ) this.accessToken = tokens.access_token if (tokens.refresh_token) { this.refreshToken = tokens.refresh_token } } catch (error) { if ( error instanceof Error && error.message === "REAUTH_REQUIRED" ) { throw new Error("Re-authentication required") } throw error } } private storeSecurely(key: string, value: string): void { // Implement secure storage } private retrieveSecurely(key: string): string { // Implement secure retrieval return "" } private deleteSecurely(key: string): void { // Implement secure deletion } } ``` -------------------------------- ### GET notion-get-users Source: https://developers.notion.com/guides/mcp/mcp-supported-tools Lists all users in the workspace with their details. ```APIDOC ## GET notion-get-users ### Description Lists all users in the workspace with their details. ### Method GET ### Endpoint notion-get-users ``` -------------------------------- ### GET notion-get-user Source: https://developers.notion.com/guides/mcp/mcp-supported-tools Retrieve user information by ID. ```APIDOC ## GET notion-get-user ### Description Retrieve your user information by ID. ### Method GET ### Endpoint notion-get-user ``` -------------------------------- ### GET notion-get-comments Source: https://developers.notion.com/guides/mcp/mcp-supported-tools Lists all comments and discussions on a page. ```APIDOC ## GET notion-get-comments ### Description Lists all comments and discussions on a page, including block-level and inline discussions. ### Method GET ### Endpoint notion-get-comments ``` -------------------------------- ### GET notion-get-self Source: https://developers.notion.com/guides/mcp/mcp-supported-tools Retrieves information about the bot user and the connected Notion workspace. ```APIDOC ## GET notion-get-self ### Description Retrieves information about your own bot user and the Notion workspace you're connected to. ### Method GET ### Endpoint notion-get-self ``` -------------------------------- ### GET notion-get-teams Source: https://developers.notion.com/guides/mcp/mcp-supported-tools Retrieves a list of teams (teamspaces) in the current workspace. ```APIDOC ## GET notion-get-teams ### Description Retrieves a list of teams (teamspaces) in the current workspace. ### Method GET ### Endpoint notion-get-teams ``` -------------------------------- ### Configure Notion MCP Server in Cursor (Global) Source: https://developers.notion.com/guides/mcp/get-started-with-mcp Paste this JSON configuration into Cursor Settings to add the Notion MCP server globally. Complete the OAuth flow when prompted. ```json { "mcpServers": { "notion": { "url": "https://mcp.notion.com/mcp" } } } ``` -------------------------------- ### notion-create-database Source: https://developers.notion.com/guides/mcp/mcp-supported-tools Creates a new Notion database. ```APIDOC ## notion-create-database ### Description Creates a new Notion database, initial data source, and initial view with specified properties. ### Method TOOL ### Endpoint notion-create-database ``` -------------------------------- ### Add Notion MCP Server URL in Claude Desktop Source: https://developers.notion.com/guides/mcp/get-started-with-mcp Enter this URL in Claude Desktop's Settings to add the Notion MCP connector. Complete the OAuth flow to connect your workspace. ```text https://mcp.notion.com/mcp ``` -------------------------------- ### Configure Notion MCP Server in Windsurf Source: https://developers.notion.com/guides/mcp/get-started-with-mcp Add this JSON configuration to your mcp_config.json file in Windsurf settings to connect to the Notion MCP server. Save and restart Windsurf. ```json { "mcpServers": { "notion": { "serverUrl": "https://mcp.notion.com/mcp" } } } ``` -------------------------------- ### Add Notion MCP Server to Antigravity Configuration Source: https://developers.notion.com/guides/mcp/get-started-with-mcp Configure Antigravity to connect to Notion MCP as a custom server. Add the Notion MCP server URL to your `mcp_config.json` file, following Antigravity's instructions for custom servers. ```json ```json { "mcpServers": { "notion": { "serverUrl": "https://mcp.notion.com/mcp" } } } ``` ``` -------------------------------- ### Connect to MCP Server with Authentication Source: https://developers.notion.com/guides/mcp/build-mcp-client Creates an MCP client, supporting Streamable HTTP and SSE transports. It automatically falls back to SSE if Streamable HTTP fails. Ensure you provide a valid server URL and access token. ```typescript import { Client } from "@modelcontextprotocol/sdk/client/index.js" import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js" import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js" async function createMcpClient( serverUrl: string, accessToken: string, useSSE: boolean = false ): Promise { const client = new Client( { name: "your-mcp-client", version: "1.0.0", }, { capabilities: { roots: {}, sampling: {}, }, } ) let transport if (useSSE) { transport = new SSEClientTransport(new URL(`${serverUrl}/sse`), { requestInit: { headers: { Authorization: `Bearer ${accessToken}`, "User-Agent": "YourApp-MCP-Client/1.0", }, }, }) } else { transport = new StreamableHTTPClientTransport( new URL(`${serverUrl}/mcp`), { requestInit: { headers: { Authorization: `Bearer ${accessToken}`, "User-Agent": "YourApp-MCP-Client/1.0", }, }, } ) } await client.connect(transport) return client } // Usage with automatic fallback async function connectToNotionMcp(accessToken: string): Promise { const serverUrl = "https://mcp.notion.com" try { return await createMcpClient(serverUrl, accessToken, false) } catch (error) { console.warn("Streamable HTTP failed, falling back to SSE:", error) return await createMcpClient(serverUrl, accessToken, true) } } ``` -------------------------------- ### Register Client Dynamically Source: https://developers.notion.com/guides/mcp/build-mcp-client Dynamically registers a client with the Notion MCP server using its registration endpoint. This function handles the POST request and returns client credentials. Ensure the server supports dynamic client registration by checking for a `registration_endpoint` in its metadata. ```typescript type ClientRegistration = { client_name: string client_uri?: string redirect_uris: string[] grant_types: string[] response_types: string[] token_endpoint_auth_method: string scope?: string } type ClientCredentials = { client_id: string client_secret?: string client_id_issued_at?: number client_secret_expires_at?: number } async function registerClient( metadata: OAuthMetadata, redirectUri: string ): Promise { if (!metadata.registration_endpoint) { throw new Error("Server does not support dynamic client registration") } const registrationRequest: ClientRegistration = { client_name: "Your MCP Client", client_uri: "https://example.com", redirect_uris: [redirectUri], grant_types: ["authorization_code", "refresh_token"], response_types: ["code"], token_endpoint_auth_method: "none", } const response = await fetch(metadata.registration_endpoint, { method: "POST", headers: { "Content-Type": "application/json", Accept: "application/json", }, body: JSON.stringify(registrationRequest), }) if (!response.ok) { const errorBody = await response.text() throw new Error( `Client registration failed: ${response.status} - ${errorBody}` ) } const credentials = (await response.json()) as ClientCredentials // Store credentials securely return credentials } ``` -------------------------------- ### notion-create-pages Source: https://developers.notion.com/guides/mcp/mcp-supported-tools Creates one or more Notion pages. ```APIDOC ## notion-create-pages ### Description Creates one or more Notion pages with specified properties and content. Supports database templates, icons, and cover images. ### Method TOOL ### Endpoint notion-create-pages ``` -------------------------------- ### POST notion-query-data-sources Source: https://developers.notion.com/guides/mcp/mcp-supported-tools Queries across multiple Notion data sources with support for summaries, grouping, and filters. ```APIDOC ## POST notion-query-data-sources ### Description Query across multiple Notion data sources directly with structured summaries, grouping, and filters. Returns organized results with counts and rollups. ### Method POST ### Endpoint notion-query-data-sources ``` -------------------------------- ### Configure Notion MCP via STDIO using mcp-remote Source: https://developers.notion.com/guides/mcp/get-started-with-mcp Use this JSON configuration for tools that only support local stdio servers. It leverages `mcp-remote` to bridge the connection to the Notion MCP server. ```json ```json { "mcpServers": { "notion": { "command": "npx", "args": ["-y", "mcp-remote", "https://mcp.notion.com/mcp"] } } } ``` ``` -------------------------------- ### notion-create-view Source: https://developers.notion.com/guides/mcp/mcp-supported-tools Creates a new view on a Notion database. ```APIDOC ## notion-create-view ### Description Create a new view on a Notion database. Supports various view types like table, board, list, calendar, etc., with configuration for filters and sorts. ### Method TOOL ### Endpoint notion-create-view ``` -------------------------------- ### Discover MCP Server from URL Source: https://developers.notion.com/guides/mcp/build-mcp-client This function checks a given URL's origin for a /.well-known/mcp.json file to discover an MCP server. It returns the server's name and endpoint if found, otherwise null. ```typescript async function discoverMcpServer( url: string ): Promise<{ name: string; endpoint: string } | null> { try { const origin = new URL(url).origin const response = await fetch( `${origin}/.well-known/mcp.json` ) if (!response.ok) return null return await response.json() } catch { return null } } ``` -------------------------------- ### Configure Notion MCP for Streamable HTTP Transport Source: https://developers.notion.com/guides/mcp/get-started-with-mcp Set up a JSON configuration for tools that support Streamable HTTP transport. This is the recommended modern transport for connecting to Notion MCP. ```json ```json { "mcpServers": { "notion": { "url": "https://mcp.notion.com/mcp" } } } ``` ``` -------------------------------- ### Add Notion MCP Server to Codex Configuration Source: https://developers.notion.com/guides/mcp/get-started-with-mcp Configure Codex to use the Notion MCP server by adding its URL to your `~/.codex/config.toml` file. This sets up Codex to communicate with Notion's MCP endpoint. ```toml ```toml [mcp_servers.notion] url = "https://mcp.notion.com/mcp" ``` ``` -------------------------------- ### Add Notion MCP Server in Claude Code Source: https://developers.notion.com/guides/mcp/get-started-with-mcp Use this command to add the Notion MCP server to Claude Code. You will then need to authenticate via OAuth in Claude Code. ```bash claude mcp add --transport http notion https://mcp.notion.com/mcp ``` -------------------------------- ### notion-search Source: https://developers.notion.com/guides/mcp/mcp-supported-tools Search across your Notion workspace and connected tools. ```APIDOC ## notion-search ### Description Search across your Notion workspace and connected tools like Slack, Google Drive, and Jira. Requires Notion AI access for connected tools. ### Method TOOL ### Endpoint notion-search ``` -------------------------------- ### POST notion-query-database-view Source: https://developers.notion.com/guides/mcp/mcp-supported-tools Queries data from a Notion database using a pre-defined view's filters and sorts. ```APIDOC ## POST notion-query-database-view ### Description Query data from a Notion database using a pre-defined view's filters and sorts. ### Method POST ### Endpoint notion-query-database-view ``` -------------------------------- ### notion-move-pages Source: https://developers.notion.com/guides/mcp/mcp-supported-tools Moves Notion pages or databases. ```APIDOC ## notion-move-pages ### Description Move one or more Notion pages or databases to a new parent. ### Method TOOL ### Endpoint notion-move-pages ``` -------------------------------- ### Generate PKCE Parameters Source: https://developers.notion.com/guides/mcp/build-mcp-client Generates a code verifier and its corresponding SHA256-based code challenge for secure OAuth flows. The code verifier must be stored securely and used later during the token exchange. ```typescript import { randomBytes, createHash } from "crypto" function base64URLEncode(str: Buffer): string { return str .toString("base64") .replace(/\+\/g, "-") .replace(/\//g, "_") .replace(/=/g, "") } function generateCodeVerifier(): string { // Generate 32 random bytes = 256 bits // Base64 encoding produces ~43 characters const bytes = randomBytes(32) return base64URLEncode(bytes) } function generateCodeChallenge(verifier: string): string { const hash = createHash("sha256").update(verifier).digest() return base64URLEncode(hash) } // Usage const codeVerifier = generateCodeVerifier() const codeChallenge = generateCodeChallenge(codeVerifier) // Store codeVerifier securely - you'll need it for token exchange ``` -------------------------------- ### Build Authorization URL Source: https://developers.notion.com/guides/mcp/build-mcp-client Constructs the authorization URL for initiating the OAuth flow, including necessary parameters like client ID, redirect URI, PKCE challenge, and state. It's crucial to generate a unique state for each request and store it securely along with the code verifier for CSRF protection and token exchange. ```typescript function buildAuthorizationUrl( metadata: OAuthMetadata, clientId: string, redirectUri: string, codeChallenge: string, state: string, scopes: string[] = [] ): string { const params = new URLSearchParams({ response_type: "code", client_id: clientId, redirect_uri: redirectUri, scope: scopes.join(" "), state: state, code_challenge: codeChallenge, code_challenge_method: "S256", prompt: "consent", }) return `${metadata.authorization_endpoint}?${params.toString()}` } function generateState(): string { return randomBytes(32).toString("hex") } // Usage const state = generateState() const authorizationUrl = buildAuthorizationUrl( metadata, clientId, redirectUri, codeChallenge, state ) // Store state and codeVerifier in secure session storage // Redirect user to authorizationUrl window.location.href = authorizationUrl // Browser redirect ``` -------------------------------- ### notion-fetch Source: https://developers.notion.com/guides/mcp/mcp-supported-tools Retrieves content from a Notion page, database, or data source. ```APIDOC ## notion-fetch ### Description Retrieves content from a Notion page, database, or data source by its URL or ID. Includes schema and property details for data sources. ### Method TOOL ### Endpoint notion-fetch ``` -------------------------------- ### POST notion-update-view Source: https://developers.notion.com/guides/mcp/mcp-supported-tools Updates a view's configuration including name, filters, sorts, and display settings. ```APIDOC ## POST notion-update-view ### Description Update a view's name, filters, sorts, or display configuration. Only the fields you specify will be changed. ### Method POST ### Endpoint notion-update-view ``` -------------------------------- ### Discover OAuth Metadata for MCP Server Source: https://developers.notion.com/guides/mcp/build-mcp-client Implement the complete RFC 9470 → RFC 8414 discovery flow to obtain OAuth endpoints for an MCP server. This function fetches Protected Resource Metadata first, then Authorization Server Metadata, and validates the response. ```typescript type OAuthMetadata = { issuer: string authorization_endpoint: string token_endpoint: string registration_endpoint?: string code_challenge_methods_supported?: string[] grant_types_supported?: string[] response_types_supported?: string[] scopes_supported?: string[] } /** * Discovers OAuth configuration for an MCP server using RFC 9470 + RFC 8414. */ async function discoverOAuthMetadata( mcpServerUrl: string ): Promise { const url = new URL(mcpServerUrl) const protectedResourceUrl = new URL( "/.well-known/oauth-protected-resource", url ) // Step 1: RFC 9470 - Get Protected Resource Metadata const protectedResourceResponse = await fetch( protectedResourceUrl.toString() ) if (!protectedResourceResponse.ok) { throw new Error( `Failed to fetch protected resource metadata: ` + `${protectedResourceResponse.status}` ) } const protectedResource = await protectedResourceResponse.json() const authServers = protectedResource.authorization_servers if (!Array.isArray(authServers) || authServers.length === 0) { throw new Error( "No authorization servers found in protected resource metadata" ) } // Use the first authorization server const authServerUrl = authServers[0] // Step 2: RFC 8414 - Get Authorization Server Metadata const metadataUrl = new URL( "/.well-known/oauth-authorization-server", authServerUrl ) const metadataResponse = await fetch(metadataUrl.toString()) if (!metadataResponse.ok) { throw new Error( `Failed to fetch authorization server metadata: ` + `${metadataResponse.status}` ) } const metadata = (await metadataResponse.json()) as OAuthMetadata // Validate required fields if (!metadata.authorization_endpoint || !metadata.token_endpoint) { throw new Error("Missing required OAuth endpoints in metadata") } // Warn if PKCE support isn't advertised if (!metadata.code_challenge_methods_supported?.includes("S256")) { console.warn( "Server does not advertise S256 PKCE support, " + "but we will use it anyway" ) } return metadata } ``` -------------------------------- ### Authenticate Codex MCP Login for Notion Source: https://developers.notion.com/guides/mcp/get-started-with-mcp Log in to Notion MCP using the Codex CLI. This command initiates an OAuth flow to authenticate your Notion workspace connection. ```bash ```bash codex mcp login notion ``` ``` -------------------------------- ### Configure Notion MCP for SSE Transport Source: https://developers.notion.com/guides/mcp/get-started-with-mcp Configure Notion MCP using Server-Sent Events (SSE) transport. This JSON configuration is for legacy clients that require SSE. ```json ```json { "mcpServers": { "notion": { "type": "sse", "url": "https://mcp.notion.com/sse" } } } ``` ``` -------------------------------- ### POST notion-create-comment Source: https://developers.notion.com/guides/mcp/mcp-supported-tools Adds a comment to a page or specific content block. ```APIDOC ## POST notion-create-comment ### Description Add a comment to a page or specific content. Supports page-level comments, block-level comments, and replies. ### Method POST ### Endpoint notion-create-comment ``` -------------------------------- ### notion-update-data-source Source: https://developers.notion.com/guides/mcp/mcp-supported-tools Updates a Notion data source. ```APIDOC ## notion-update-data-source ### Description Update a Notion data source's properties, name, description, or other attributes. ### Method TOOL ### Endpoint notion-update-data-source ``` -------------------------------- ### notion-duplicate-page Source: https://developers.notion.com/guides/mcp/mcp-supported-tools Duplicates a Notion page. ```APIDOC ## notion-duplicate-page ### Description Duplicate a Notion page within your workspace. This action completes asynchronously. ### Method TOOL ### Endpoint notion-duplicate-page ``` -------------------------------- ### Handle OAuth Callback Source: https://developers.notion.com/guides/mcp/build-mcp-client Parses the callback URL from an OAuth redirect to extract authorization code, state, or error information. Validates the state parameter to prevent CSRF attacks and throws an error if any issues are detected. ```typescript interface CallbackParams { code?: string state?: string error?: string error_description?: string } function parseCallback(url: string): CallbackParams { const urlParams = new URLSearchParams(new URL(url).search) return { code: urlParams.get("code") || undefined, state: urlParams.get("state") || undefined, error: urlParams.get("error") || undefined, error_description: urlParams.get("error_description") || undefined, } } async function handleCallback( callbackUrl: string, storedState: string, codeVerifier: string ): Promise { const params = parseCallback(callbackUrl) if (params.error) { throw new Error( `OAuth error: ${params.error} - ` + `${params.error_description || "Unknown error"}` ) } if (params.state !== storedState) { throw new Error("Invalid state parameter - possible CSRF attack") } if (!params.code) { throw new Error("Missing authorization code") } return params.code } ``` -------------------------------- ### notion-update-page Source: https://developers.notion.com/guides/mcp/mcp-supported-tools Updates a Notion page. ```APIDOC ## notion-update-page ### Description Update a Notion page's properties, content, icon, or cover. Supports applying database templates. ### Method TOOL ### Endpoint notion-update-page ``` -------------------------------- ### Handle Token Refresh Source: https://developers.notion.com/guides/mcp/build-mcp-client Refreshes an access token using a refresh token. Handles common OAuth errors like 'invalid_grant' and 'invalid_client'. Always store the new 'refresh_token' if provided in the response due to potential rotation. ```typescript async function refreshAccessToken( refreshToken: string, metadata: OAuthMetadata, clientId: string, clientSecret: string | undefined ): Promise { const params = new URLSearchParams({ grant_type: "refresh_token", refresh_token: refreshToken, client_id: clientId, }) if (clientSecret) { params.append("client_secret", clientSecret) } const response = await fetch(metadata.token_endpoint, { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded", Accept: "application/json", }, body: params.toString(), }) if (!response.ok) { const errorBody = await response.text() try { const error = JSON.parse(errorBody) if (error.error === "invalid_grant") { throw new Error("REAUTH_REQUIRED") } if (error.error === "invalid_client") { throw new Error("INVALID_CLIENT") } } catch (parseError) { // Not JSON error response } throw new Error( `Token refresh failed: ${response.status} - ${errorBody}` ) } const tokens = await response.json() return tokens } ``` -------------------------------- ### Exchange Authorization Code for Tokens Source: https://developers.notion.com/guides/mcp/build-mcp-client Exchanges an authorization code for access and refresh tokens by making a POST request to the token endpoint. Includes client credentials and code verifier for security. Handles potential errors during the exchange process. ```typescript type TokenResponse = { access_token: string token_type: string expires_in?: number refresh_token?: string scope?: string } async function exchangeCodeForTokens( code: string, codeVerifier: string, metadata: OAuthMetadata, clientId: string, clientSecret: string | undefined, redirectUri: string ): Promise { const params = new URLSearchParams({ grant_type: "authorization_code", code: code, client_id: clientId, redirect_uri: redirectUri, code_verifier: codeVerifier, }) if (clientSecret) { params.append("client_secret", clientSecret) } const response = await fetch(metadata.token_endpoint, { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded", Accept: "application/json", "User-Agent": "YourApp-MCP-Client/1.0", }, body: params.toString(), }) if (!response.ok) { const errorBody = await response.text() throw new Error( `Token exchange failed: ${response.status} - ${errorBody}` ) } const tokens = await response.json() if (!tokens.access_token) { throw new Error("Missing access_token in response") } return tokens } ``` === COMPLETE CONTENT === This response contains all available snippets from this library. No additional content exists. Do not make further requests.