Connection requirements
Notion provides a hosted MCP (Model Context Protocol) server that lets MCP clients work 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.
Your client must support:
- 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:user_id and workspace_id come as a pair. If Notion can’t resolve them, the
response has none of these fields. email_domain is left out when the user has
no email address. bot_id, workspace_name, and workspace_icon are left out
when Notion doesn’t have the connection’s details, and the two workspace fields
can be null. To get the user’s name or email address, or to look up identity
later, see Identify the connected workspace.
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.Session ids on the Streamable HTTP endpoint
The/mcp endpoint may return an Mcp-Session-Id header on the response to
your initialize request. Per the
Streamable HTTP transport spec,
a client that receives one must send it back in an Mcp-Session-Id header on
every later request in that session. The official MCP SDKs do this for you. A
hand-written HTTP client needs to capture the header and echo it.
Notion’s /mcp endpoint is stateless, so the id carries no server-side state:
- The id is opaque. Don’t parse it or attach meaning to its contents.
- Notion doesn’t validate or expire it. A missing, unknown, or stale session id
is never rejected on this endpoint, so there is no
400 Bad Requestor404 Not Foundsession error to recover from and no session to re-establish. - Echoing it lets Notion group the requests of one client session together, which helps when you report an issue — include the id in your report.
Identify the connected workspace
Use the token response fields first. They give youuser_id and workspace_id without another request.
For a user-authorized connection, call notion-get-self with an empty object to
read the person who connected, including their name and email when available.
If user.type is bot, the connection acts as a workspace bot and no person
email is available; use user_id from the authorization-code token response to
identify the person who authorized the connection.
workspace and user
fields. See Supported tools for their shapes.
Older clients can still call notion-fetch with the id self. That response
puts the same identity under a self field. New clients should use
notion-get-self.
An MCP access token works only with Notion MCP. Don’t send it to the REST API,
for example to GET /v1/users/me.
OpenID Connect (OIDC) is the standard OAuth option for receiving identity in
an ID token or from a UserInfo endpoint. It isn’t available from Notion MCP
yet. If Notion enables OIDC for your client, request openid. Add profile
for the user’s name and email for the user’s verified email address. Notion
grants these identity scopes only to clients the Notion team allows. The
consent page tells the user, “See your name and email address.” Notion omits
the email claim when the email isn’t verified.
To check whether a token is still active, and who it belongs to, send it to
the introspection_endpoint from the
authorization server metadata. This endpoint follows
RFC 7662. Send a POST with
a form-encoded token parameter, and authenticate with the same method you
use at the token endpoint. Public clients send only client_id. A client can
only introspect its own tokens.
active: true with client_id, scope, iat, sub,
and iss, plus user_id, exp, and aud when Notion has them. Access tokens
also return token_type: "bearer" and, when Notion has it, workspace_id. An expired, revoked, or unknown
token, or a token issued to another client, returns only { "active": false }.
Check tool availability
You don’t need to check tool access before you call tools. Each tool enforces its own access limits when you call it. Usenotion-search for every content
search. It runs AI search when the user has it, and keyword search otherwise.
Workspace-owned connections keep the earlier tool list: use the search tool the
connection lists, which can be notion-ai-search. See
Supported tools for that case.
If your client wants plan or parameter details up front, call
notion-get-tool-access with {} once and reuse the map for this connection.
ai_search.
When that status is available, the map leaves out search, so request both
keys if you narrow it with tool_names. Keep calling notion-search.
A user lookup (query_type: "user") on the listed search tool has separate
requirements: it needs user information capabilities, and workspace-owned MCP
connections must also expose notion-get-users. AI search access doesn’t grant
these permissions.
Read each restricted_parameters reason against your requested value. A listed
parameter isn’t always forbidden: a restriction on multiple teamspaces still
allows a single teamspace. An upgrade_required status means full access needs
an upgrade; it doesn’t rule out documented fallback results.
See Supported tools for all access statuses,
recovery links, and search behavior.
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
invalid_grant means Notion rejected the assertion. See
Enterprise-managed authorization
for the claims Notion checks and the common causes.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. When a user pastes a link,
the client can detect the MCP server and offer to connect to it.
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