Notion Remote MCP Server
Enables secure interaction with Notion through a suite of tools for searching, reading, and modifying pages and databases. It features OAuth 2.1 authentication with PKCE and encrypted token storage to provide an enterprise-ready interface for MCP clients.
README
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" },
"properties": { "type": "object" }
},
"required": ["id", "url"]
}
notion.get_database
Input:
{
"type": "object",
"properties": { "database_id": { "type": "string" }, "data_source_id": { "type": "string" } },
"anyOf": [{ "required": ["database_id"] }, { "required": ["data_source_id"] }],
"additionalProperties": false
}
Output:
{
"type": "object",
"properties": {
"id": { "type": "string" },
"title": { "type": "string" },
"url": { "type": ["string", "null"] },
"properties": { "type": "object" }
},
"required": ["id", "url"]
}
notion.query_database
Input:
{
"type": "object",
"properties": {
"database_id": { "type": "string" },
"data_source_id": { "type": "string" },
"filter": { "type": "object" },
"sorts": { "type": "array" },
"page_size": { "type": "integer", "minimum": 1, "maximum": 100 },
"start_cursor": { "type": "string" }
},
"anyOf": [{ "required": ["database_id"] }, { "required": ["data_source_id"] }],
"additionalProperties": false
}
Output:
{
"type": "object",
"properties": {
"results": { "type": "array" },
"next_cursor": { "type": ["string", "null"] },
"has_more": { "type": "boolean" }
},
"required": ["results", "has_more"]
}
notion.create_page
Input:
{
"type": "object",
"properties": {
"parent": { "type": "object", "properties": { "database_id": { "type": "string" }, "data_source_id": { "type": "string" }, "page_id": { "type": "string" } }, "additionalProperties": false },
"properties": { "type": "object" },
"children": { "type": "array" }
},
"required": ["parent", "properties"],
"additionalProperties": false
}
Output:
{
"type": "object",
"properties": {
"id": { "type": "string" },
"url": { "type": "string" },
"created_time": { "type": "string" }
},
"required": ["id", "url"]
}
notion.update_page
Input:
{
"type": "object",
"properties": {
"page_id": { "type": "string" },
"properties": { "type": "object" },
"archived": { "type": "boolean" }
},
"required": ["page_id"],
"additionalProperties": false
}
Output:
{
"type": "object",
"properties": {
"id": { "type": "string" },
"url": { "type": "string" },
"archived": { "type": "boolean" }
},
"required": ["id", "url"]
}
notion.append_block
Input:
{
"type": "object",
"properties": {
"block_id": { "type": "string" },
"children": { "type": "array" }
},
"required": ["block_id", "children"],
"additionalProperties": false
}
Output:
{
"type": "object",
"properties": {
"id": { "type": "string" },
"has_more": { "type": "boolean" }
},
"required": ["id"]
}
notion.list_users
Input:
{
"type": "object",
"properties": {
"page_size": { "type": "integer", "minimum": 1, "maximum": 100 },
"start_cursor": { "type": "string" }
},
"additionalProperties": false
}
Output:
{
"type": "object",
"properties": {
"results": { "type": "array" },
"next_cursor": { "type": ["string", "null"] },
"has_more": { "type": "boolean" }
},
"required": ["results", "has_more"]
}
notion.whoami
Input:
{
"type": "object",
"properties": {},
"additionalProperties": false
}
Output:
{
"type": "object",
"properties": {
"bot_id": { "type": "string" },
"workspace_id": { "type": "string" },
"owner": { "type": "object" }
},
"required": ["bot_id"]
}
Examples
List tools:
curl -s http://localhost:8787/mcp \
-H "Authorization: Bearer $MCP_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
Search pages:
curl -s http://localhost:8787/mcp \
-H "Authorization: Bearer $MCP_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"notion.search","arguments":{"query":"Roadmap","page_size":5}}}'
Create a page in a database:
curl -s http://localhost:8787/mcp \
-H "Authorization: Bearer $MCP_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"notion.create_page","arguments":{"parent":{"database_id":"YOUR_DB_ID"},"properties":{"Name":{"title":[{"text":{"content":"Q1 Plan"}}]}}}}}'
Docker
docker build -t notion-mcp .
docker run --env-file .env -p 8787:8787 notion-mcp
Security Notes
- OAuth 2.1 + PKCE enforced for MCP clients.
- Token storage is AES-256-GCM encrypted via
TOKEN_ENC_KEY. - Access tokens are short-lived; refresh tokens rotate access tokens.
- Origin allowlist for browser clients via
ALLOWED_ORIGINS. - Rate limiting: defaults are 120 requests/min for
/mcpand 30 requests/min for auth endpoints. - If encryption/state keys are not set, they are auto-generated and stored under
./data/for local dev.
Trade-offs / Next Steps
- Notion does not document PKCE support for its own OAuth; PKCE is enforced for MCP clients, and upstream Notion OAuth uses standard code exchange.
- Dynamic client registration stores client metadata in the encrypted store; could add client secrets and approval workflows for stricter control.
- Tool output is returned as JSON text; could add structured content types once supported.
- Add rate limiting, audit logs, and per-tenant encryption keys for stronger governance.
- For Notion API version 2025-09-03, prefer
data_source_idfor database-like operations;database_idis kept for backward compatibility.
Recommended Servers
playwright-mcp
A Model Context Protocol server that enables LLMs to interact with web pages through structured accessibility snapshots without requiring vision models or screenshots.
Magic Component Platform (MCP)
An AI-powered tool that generates modern UI components from natural language descriptions, integrating with popular IDEs to streamline UI development workflow.
Audiense Insights MCP Server
Enables interaction with Audiense Insights accounts via the Model Context Protocol, facilitating the extraction and analysis of marketing insights and audience data including demographics, behavior, and influencer engagement.
VeyraX MCP
Single MCP tool to connect all your favorite tools: Gmail, Calendar and 40 more.
Kagi MCP Server
An MCP server that integrates Kagi search capabilities with Claude AI, enabling Claude to perform real-time web searches when answering questions that require up-to-date information.
graphlit-mcp-server
The Model Context Protocol (MCP) Server enables integration between MCP clients and the Graphlit service. Ingest anything from Slack to Gmail to podcast feeds, in addition to web crawling, into a Graphlit project - and then retrieve relevant contents from the MCP client.
Qdrant Server
This repository is an example of how to create a MCP server for Qdrant, a vector search engine.
Neon Database
MCP server for interacting with Neon Management API and databases
Exa Search
A Model Context Protocol (MCP) server lets AI assistants like Claude use the Exa AI Search API for web searches. This setup allows AI models to get real-time web information in a safe and controlled way.
E2B
Using MCP to run code via e2b.