Overview
Notion provides a hosted MCP (Model Context Protocol) server that enables AI tools to interact with Notion workspaces. The server is available at:| Transport | URL | Notes |
|---|---|---|
| Streamable HTTP (recommended) | https://mcp.notion.com/mcp | Modern transport, more efficient |
| Server-Sent Events (SSE) | https://mcp.notion.com/sse | Fallback for broader compatibility |
- 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: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.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 implementation uses
Cloudflare’s
workers-oauth-provider package.
Note these characteristics:- Token expiry: Access tokens expire after one hour.
- Refresh token rotation: At any time a grant may have two valid refresh
tokens. When the client uses one, the other is invalidated and a new
refresh token is issued.
- If you correctly switch to the returned
refresh_tokeneach time, older tokens are continuously invalidated. - If a transient failure prevents updating the stored token, you can retry the request with the previously stored token once.
- With concurrent refreshes, only the first succeeds and later attempts may
receive
invalid_grant.
- If you correctly switch to the returned
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
| Field | Type | Description |
|---|---|---|
name | string | Human-readable name of the MCP server |
description | string | Brief description of what the server provides |
icon | string (URL) | URL to the server’s icon |
endpoint | string (URL) | The MCP server endpoint URL |
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