Overview
Notion provides a hosted MCP (Model Context Protocol) server that enables AI tools to interact with Notion workspaces. The server is available at:
Both endpoints support the same MCP protocol and OAuth authentication. Your
client should try Streamable HTTP first and fall back to SSE if needed.
Key requirements:
- OAuth 2.0 Authorization Code flow with PKCE
- Support for Streamable HTTP (
/mcp) or SSE (/sse) transports - Token refresh handling
- Secure credential storage
Prerequisites
This guide uses TypeScript/JavaScript examples, but the concepts apply to any
programming language. The OAuth 2.0 flow, PKCE implementation, and MCP
protocol are language-agnostic.
Alternative libraries for other languages
Python
Python
- MCP SDK: python-sdk (official)
- OAuth 2.0:
authlib(recommended) orrequests-oauthlib - PKCE: Built into both
authlibandrequests-oauthlib
Go
Go
- MCP SDK: go-sdk (official)
- OAuth 2.0:
golang.org/x/oauth2(official extended package) - PKCE: Supported via
oauth2.SetAuthURLParam("code_challenge", ...)andoauth2.SetAuthURLParam("code_challenge_method", "S256")
Rust
Rust
Java
Java
- MCP SDK: Use HTTP client libraries (Apache HttpClient, OkHttp, or Java 11+
HttpClient) - OAuth 2.0:
Spring Security OAuth2(recommended) orScribeJava - PKCE: Built into Spring Security OAuth2 Client; supported in ScribeJava via
PKCEconfiguration
C# / .NET
C# / .NET
- MCP SDK: Use
HttpClientwithSystem.Net.Http.Json - OAuth 2.0:
IdentityModel.OidcClientorMicrosoft.Identity.Web - PKCE: Built into both libraries
Key references
- MCP Specification — Model Context Protocol standard
- RFC 6749 — OAuth 2.0 Authorization Framework
- RFC 7636 — PKCE (Proof Key for Code Exchange)
- RFC 8414 — OAuth 2.0 Authorization Server Metadata
- RFC 9470 — OAuth 2.0 Protected Resource Metadata
Step 1: OAuth discovery
Before connecting to an MCP server, discover its OAuth configuration. Given the MCP server URL (e.g.,https://mcp.notion.com/mcp), use a standard two-step
discovery process:
- RFC 9470: Fetch Protected Resource Metadata to find which authorization server(s) protect this resource
- RFC 8414: Fetch Authorization Server Metadata to get OAuth endpoints
Understanding the discovery flow
An MCP server (the protected resource) might be hosted atmcp.example.com but
delegate authentication to a separate OAuth server at auth.example.com. The
Protected Resource Metadata tells you where to find the authorization server,
and the Authorization Server Metadata tells you the specific OAuth endpoints to
use.
Standard discovery implementation
Here’s a function that implements the complete RFC 9470 → RFC 8414 discovery flow:- Fetches Protected Resource Metadata from
https://mcp.notion.com/mcp/.well-known/oauth-protected-resource— Returns:{ "authorization_servers": ["https://..."], ... } - Extracts the authorization server URL from the
authorization_serversarray - Fetches Authorization Server Metadata from
{authServerUrl}/.well-known/oauth-authorization-server— Returns: OAuth endpoints likeauthorization_endpoint,token_endpoint, etc. - Validates that all required fields are present and warns if PKCE support isn’t advertised
This approach is universal and works for any MCP server that follows RFC 9470
and RFC 8414 standards, not just Notion’s MCP server.
Step 2: Generate PKCE parameters
PKCE (Proof Key for Code Exchange) is mandatory for secure OAuth flows. Generate a code verifier and challenge:Step 3: Dynamic client registration
Notion MCP server supports dynamic client registration (RFC 7591). Check ifregistration_endpoint exists in the metadata:
Step 4: Initiate authorization flow
Redirect the user to the authorization endpoint with PKCE parameters:Step 5: Handle OAuth callback
After user authorizes, they’ll be redirected back to yourredirectUri with an
authorization code:
Step 6: Exchange authorization code for tokens
Exchange the authorization code for access and refresh tokens:Identity fields in the token responseSuccessful authorization-code exchanges also return
user_id and
workspace_id, the Notion IDs of the authorizing user and workspace, plus
email_domain, the lowercased domain of the authorizing user’s email
address. Use them to associate the connection with a user and workspace
without an extra call. Refresh responses don’t include these fields, so
store them from the initial exchange.Notion may add fields to the token response over time. Parse it leniently
and ignore fields you don’t recognize.Step 7: Connect to MCP server with authentication
Notion’s MCP server supports two transport protocols. Your client should try Streamable HTTP first and automatically fall back to SSE if needed.Identify the connected workspace
The OAuth token response includesuser_id and workspace_id, but not
display names, and the public REST API’s GET /v1/users/me does not accept
MCP-audienced tokens. To label a connection with the workspace name after
connecting, call the fetch tool with the special id self:
Step 8: Handle token refresh
Access tokens expire. Implement automatic refresh with proper error handling:Many servers rotate refresh tokens for security (RFC 6749 Section 10.4).
Always store the new
refresh_token if provided in the response.Complete example
Here’s a complete class that ties all the steps together:Security best practices
- Always use HTTPS — Never use HTTP except for localhost development
- Validate state parameter — Always verify state matches stored value on callback
- Secure token storage — Encrypt tokens at rest, never expose to client-side code
- PKCE is mandatory — Always use PKCE even if server doesn’t advertise support
- Token expiry handling — Check token expiry before each request, refresh proactively
- Error handling — Handle
invalid_granterrors gracefully (re-authentication required) - HTTPS verification — Validate SSL certificates in production
- Rate limiting — Implement rate limiting for token refresh to prevent abuse
- Scope minimization — Only request the scopes you actually need
- Audit logging — Log all OAuth operations for security auditing
Troubleshooting
Invalid state parameter
Invalid state parameter
- Store the state securely and validate on callback
- Expire after ~10 minutes
Invalid code_verifier
Invalid code_verifier
- Use the exact verifier that produced the code_challenge
- Ensure base64url encoding (no +, /, =)
Discovery fails
Discovery fails
- Use RFC 9470 (protected resource) then RFC 8414 (authorization server)
- Confirm server supports OAuth and your URL is correct
Connection timeout
Connection timeout
- Prefer Streamable HTTP; fall back to SSE
- Check proxy or firewall rules
CORS errors in browser
CORS errors in browser
- Do token exchange server-side; browser should only handle redirects
Token refresh fails with invalid_grant
Token refresh fails with invalid_grant
When refresh returns
{ "error": "invalid_grant" }, the refresh token is
invalid, expired, revoked, or superseded by rotation. Do not retry refresh;
prompt re-authentication.Common causes:- Rotation on use — Providers rotate refresh tokens and revoke the old
one. Fix: Persist the new
refresh_tokenatomically with the access token. - Expired refresh token — Often 30–90 days. Fix: Re-authenticate; monitor early expirations.
- Client credential mismatch —
client_idorclient_secretdiffers from initial auth. Fix: Keep credentials consistent for the token’s lifetime. - Explicit revocation or policy event — User revoked access, password change, or security policy. Fix: Show clear reconnect UI.
- Concurrent refreshes — Parallel refreshes cause losers to see
invalid_grant. Fix: Use a mutex or distributed lock around refresh.
invalid_grant→ re-authenticatetemporarily_unavailableor network errors → retry with backoff- Refresh 5–10 minutes before expiry to avoid races
- Cache access tokens with accurate expiry
Notion MCP OAuth specifics
Notion’s remote MCP server is built on Cloudflare’sworkers-oauth-provider package.
Its token lifecycle has a few behaviors worth designing your client around. The
guarantees below are the ones you can rely on; build for them as the strictest
case.
Token lifecycle
- Access tokens currently last about eight hours, but this duration is
subject to change. Always use the token response’s
expires_invalue and refresh proactively rather than hard-coding a lifetime. - Refresh tokens expire under either of two conditions, whichever comes
first: an absolute maximum lifetime of 180 days measured from when the user
first authorized the connection (this cap does not slide — refreshing does not
extend it), or 30 consecutive days of inactivity (no successful refresh).
An actively used connection keeps working until the 180-day mark; an idle one
lapses after 30 days. In either case the next refresh returns
invalid_grantand the user must re-authorize. Treat periodic reconnection as expected, not exceptional, and make sure your reconnect flow is easy to reach.
- Persist dynamic client registration credentials (
client_idandclient_secret) durably and reuse them, because re-registering orphans prior grants. Alternatively, use a Client ID Metadata Document (CIMD), which Notion MCP supports. - Persist each rotated
refresh_tokenatomically and serialize refreshes per grant. Never refresh the same grant concurrently from multiple processes. - Treat
invalid_grantas terminal: clear the stored tokens and reauthorize once. Do not retry-loop.
Refresh token rotation
Every refresh rotates the refresh token: the token response returns a newrefresh_token, and the one you sent is retired. To keep a connection healthy:
- Persist the new
refresh_tokenfrom each refresh response atomically with the new access token, before you issue the next request. - A grant keeps at most two refresh tokens valid at once — the current one and the immediately previous one (a one-step window). If a transient failure prevents you from storing a rotated token, you may retry once with the previously stored token.
Handling invalid_grant
invalid_grant from the token endpoint always means the connection is dead and
the user must reconnect, whether the cause is refresh-token reuse, the 30-day
lifetime, an explicit revocation, or a credential mismatch. Stop refreshing that
connection, clear its tokens, and prompt re-authorization. See
Troubleshooting above for the full list of causes and fixes.
Optional: MCP server discovery via mcp.json
The
mcp.json convention described here is not part of the MCP specification.
It is an unofficial convention, championed by Notion and Cursor, that MCP
clients can optionally support to improve the user experience./.well-known/mcp.json file on a website’s domain. This enables a better
experience when users paste links into an AI tool — the client can detect that
an MCP server is available and suggest connecting to it instead of (or in
addition to) fetching the web page.
For example, Notion hosts its discovery file at:
Schema
How to use this in your client
When a user pastes a URL (e.g.,https://www.notion.com/some-page), your client
can check for /.well-known/mcp.json on that domain. If the file exists, your
client can:
- Show a prompt suggesting the user connect to the MCP server for richer interaction
- Auto-connect if the user has previously authorized the server
- Use the MCP server to fetch structured data instead of scraping the web page
Publishing your own mcp.json
If you operate an MCP server, you can publish a mcp.json file at
/.well-known/mcp.json on your domain so that MCP clients can discover your
server automatically.
Additional resources
- Notion MCP Server GitHub
- MCP SDK Documentation
- MCP Registry — Anthropic’s MCP server registry