Notion Remote MCP Server (OAuth + PKCE)
Remote MCP server that connects to Notion via OAuth and exposes a practical, enterprise-friendly tool surface. It implements Streamable HTTP transport over POST /mcp, MCP-compatible OAuth endpoints, PKCE, token refresh, and encrypted token storage.
Quick Start (5 minutes)
- Create a Notion integration
- Create a public integration in Notion.
- Add the OAuth redirect URL:
http://localhost:8787/oauth/callback - Enable capabilities (least-privilege):
- Read content
- Update content
- Insert content
- Read user info
- Configure env
cp .env.example .env
Generate an encryption key and HMAC secret:
node -e "console.log(require('crypto').randomBytes(32).toString('base64'))"
Set:
TOKEN_ENC_KEYto the generated base64 keySTATE_SIGNING_KEYto another random secret- (Optional)
TOKEN_ENC_KEY_FILE/STATE_SIGNING_KEY_FILEfor file-based secrets (defaults under./data/) NOTION_CLIENT_ID/NOTION_CLIENT_SECRETBASE_URL(if not localhost)ALLOWED_REDIRECT_URISfor your MCP clientNOTION_VERSION(default: 2025-09-03)
- Run
npm install && npm run start
Server: http://localhost:8787
OAuth for MCP Clients
This server is an OAuth 2.1 Authorization Server for MCP clients and uses Notion OAuth behind the scenes.
Authorization URL:
GET /authorize?response_type=code&client_id=mcp-cli&redirect_uri=http://localhost:3000/callback&scope=notion.read%20notion.write&state=xyz&code_challenge=...&code_challenge_method=S256
Token endpoint:
POST /token (application/x-www-form-urlencoded)
Supported scopes:
notion.readnotion.writenotion.admin
Token refresh is supported via grant_type=refresh_token.
Dynamic client registration example:
curl -s http://localhost:8787/register \
-H "Content-Type: application/json" \
-d '{"client_name":"my-mcp-client","redirect_uris":["http://localhost:3000/callback"],"scope":"notion.read notion.write"}'
MCP Endpoint
POST /mcp(Streamable HTTP)GET /mcpreturns 405 (only POST is supported)
Headers:
Authorization: Bearer <access_token>MCP-Protocol-Version: 2025-11-25(optional; supported: 2025-11-25, 2025-06-18, 2025-03-26)Accept: application/json, text/event-stream
OAuth metadata:
/.well-known/oauth-protected-resource/.well-known/oauth-authorization-serverPOST /register(dynamic client registration)
Tool Surface
All tools validate inputs with JSON Schema and return JSON-encoded results.
| Tool | Scope | Purpose |
|---|---|---|
notion.search |
notion.read |
Search pages/databases |
notion.get_page |
notion.read |
Retrieve a page |
notion.get_database |
notion.read |
Retrieve a database/data source |
notion.query_database |
notion.read |
Query database/data source rows |
notion.create_page |
notion.write |
Create a page |
notion.update_page |
notion.write |
Update page properties |
notion.append_block |
notion.write |
Append blocks |
notion.list_users |
notion.admin |
Governance: list users |
notion.whoami |
notion.admin |
Governance: integration identity |
JSON Schemas
`notion.search`
Input:
{
"type": "object",
"properties": {
"query": { "type": "string" },
"filter": { "type": "object", "properties": { "object": { "type": "string", "enum": ["page", "database"] } }, "additionalProperties": false },
"sort": { "type": "object", "properties": { "direction": { "type": "string", "enum": ["ascending", "descending"] }, "timestamp": { "type": "string", "enum": ["last_edited_time", "created_time"] } }, "additionalProperties": false },
"page_size": { "type": "integer", "minimum": 1, "maximum": 100 },
"start_cursor": { "type": "string" }
},
"additionalProperties": false
}
Output:
{
"type": "object",
"properties": {
"results": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": { "type": "string" },
"object": { "type": "string" },
"url": { "type": "string" },
"title": { "type": "string" },
"last_edited_time": { "type": "string" }
},
"required": ["id", "object", "url"]
}
},
"next_cursor": { "type": ["string", "null"] },
"has_more": { "type": "boolean" }
},
"required": ["results", "has_more"]
}
`notion.get_page`
Input:
{
"type": "object",
"properties": {
"page_id": { "type": "string" },
"include_properties": { "type": "boolean", "default": false }
},
"required": ["page_id"],
"additionalProperties": false
}
Output:
{
"type": "object",
"properties": {
"id": { "type": "string" },
"url": { "type": "string" },
"created_time": { "type": "string" },
"last_edited_time": { "type": "string" },
"archived": { "type": "boolean" },
"title": { "type": "string" },
"propertie
Tools 9
notion.searchSearch pages and databases in Notionnotion.get_pageRetrieve a specific Notion pagenotion.get_databaseRetrieve a database or data sourcenotion.query_databaseQuery database rowsnotion.create_pageCreate a new page in Notionnotion.update_pageUpdate properties of an existing pagenotion.append_blockAppend blocks to a pagenotion.list_usersList users in the workspacenotion.whoamiGet integration identity informationEnvironment Variables
TOKEN_ENC_KEYrequiredBase64 encoded encryption key for tokensSTATE_SIGNING_KEYrequiredSecret key for signing OAuth stateNOTION_CLIENT_IDrequiredNotion integration client IDNOTION_CLIENT_SECRETrequiredNotion integration client secret