Notion
About
Enables AI systems to create, update, and manage Notion pages and blocks through a Node.js bridge to the Notion API, supporting efficient batch operations for documentation and knowledge base maintenance.
Details
- Repository
- awkoy/notion-mcp-server
- License
- MIT
Explore
- Two-tool surface — notion_execute (do it) + notion_describe (learn the shape). The whole API is one schema deep.
- Universal batch envelope — every mutating op accepts { items: [...], atomic?, idempotency_key?, concurrency? } with per-item validation and results.
- Atomic batches with best-effort rollback — atomic: true aborts on first failure and archives anything created earlier in the batch.
- Idempotency keys — same (operation, idempotency_key) returns the cached result for 5 minutes. Safe to retry on flaky networks.
- Rate-limit + retry baked in — token-bucket limiter (3 req/s default, NOTION_RATE_LIMIT to change) with exponential backoff on 429/5xx/timeouts, honoring Retry-After.
- Self-healing validation errors — failures return { schema, example, fix } so the model corrects bad payloads in one round-trip.
- Markdown everywhere — create_page / append_blocks / update_block / comment bodies accept a markdown string (full GFM: headings 1–4, lists, nested to-dos, blockquotes, fenced code with language detection, images, dividers, inline formatting), plus full round-trip via get_page_markdown / update_page_markdown.
- Notion templates — create_page can apply a data source's template (template: { type: "template_id" | "default" }), with list_data_source_templates to discover template IDs.
- Database views — list/get/query/create/update/delete views; query_view runs a view's stored filters/sorts and returns hydrated rows.
- Typed where filter shorthand — query_database takes {Status: {equals: "Done"}, AND: [...]} and compiles it to Notion filter JSON (raw filter still accepted for edge cases).
- Slim responses + flattened rows — noisy fields dropped by default, query_database rows flattened to name → primitive maps, compact JSON wire format (~30% smaller). verbose: true opts out per call.
- File uploads — single-part and multi-part (5 MB chunks) transparently; MIME inferred from filename.
- Opt-in auto-pagination — paginate: true on search_pages / list_comments / query_database walks next_cursor for you (default cap ≈ 1000 items).
- HTTP(S) proxy support — standard HTTPS_PROXY / HTTP_PROXY env vars for corporate networks.
- Access control — NOTION_READ_ONLY one-switch read-only mode plus per-operation allow/block lists.
Setting up with Highlight
This MCP is not yet compatible with Highlight’s one-click setup. However, you can still use it with Highlight by following these steps:
- Download and install Highlight from highlightai.com/download
- Navigate to the plugins tab and select "Add Custom Plugin"
-
Configure the plugin with the settings below
Plugin Name
NotionCommand (node, npx, python, etc.)npxArguments-
Argument 1
-y -
Argument 2
notion-mcp-server
Environment-
NOTION_TOKEN
ntn_paste_your_token_here
Please refer to the README for specific instructions on how to obtain API keys or other required environment variables.
-
Argument 1
- Enable "Start Automatically" if you want the plugin to start when Highlight launches
From the repository
Step 1 — get a Notion token (1 minute). Open app.notion.com/developers/tokens (the Personal access tokens page of Notion's developer portal) → + New token → name it, pick your workspace → Create token → copy the ntn_… value. That's it — a PAT sees everything you can see, no per-page sharing required. (Page missing or empty? Your admin disabled PATs — see auth alternatives.)

Step 2 — add the server to your client.
Both use the same NOTION_TOKEN env var — only where you get the token differs.
| | Personal Access Token (recommended) | Internal Integration (scoped) |
| --- | --- | --- |
| Where | app.notion.com/developers/tokens → + New token | app.notion.com/developers/connections → + New connection |
| Scope | Everything you can see | Only pages where you clicked • • • → Connect → \<integration\> |
| Friction | None | Per-page Connect step for every page/database |
| Use when | Default: personal + team workspaces, prototyping | Admin requires explicit per-resource scoping, or shared production bots |
> 💡 Most object_not_found errors are a wrong auth choice, not a bug: an Internal Integration token that was never Connected to the page. Switch to a PAT.
<details>
<summary><b>PAT details: capabilities, expiry, revocation, admin-disabled fallback</b></summary>
Can: read every page you have access to; create/update pages and databases where you have edit rights; comment as you; upload files.
Can't: access pages you can't see; bypass workspace permissions; act as another user; change admin settings. A PAT's scope = your account — if you lose access to a page, so does the PAT. Issue separate tokens per teammate.
Expiry: PATs expire 1 year after creation (Notion docs); set a reminder for ~11 months.
Revoking: app.notion.com/developers/tokens → Revoke next to the token (immediate). Workspace admins can revoke anyone's from Settings & members → Connections → All personal access tokens.
Admin disabled PATs? Ask them to enable, or create an Internal Integration at app.notion.com/developers/connections (+ New connection) and • • • → Connect it to every page the agent should touch — same NOTION_TOKEN env var.
Official reference: PAT guide · Authorization overview.
</details>
| Env var | Required | Default | Meaning |
| --- | --- | --- | --- |
| NOTION_TOKEN | ✅ | — | PAT (ntn_…, recommended) or Internal Integration secret (secret_… / ntn_…) |
| NOTION_PAGE_ID | — | — | Default parent for create_page / create_database when no parent is passed (page → Share → Copy link; ID = last 32 chars) |
| NOTION_RATE_LIMIT | — | 3 | Requests/second for the shared limiter (Notion's documented per-integration limit) |
| NOTION_READ_ONLY | — | — | true/1/yes disables every write operation in one switch |
| NOTION_ALLOWED_OPERATIONS | — | all | Comma-separated allowlist of operations or group presets — see Restricting operations |
| NOTION_BLOCKED_OPERATIONS | — | — | Comma-separated blocklist (same vocabulary); wins over the allowlist |
| NOTION_UPLOAD_ROOT | — | — | Confine upload_file's path source to one directory. Unset, a path source can read any file the server process can — set this if a model composes the path. Relative paths resolve inside it; symlinks are resolved before the check, so they can't point out |
| HTTPS_PROXY / HTTP_PROXY | — | — | Route Notion API traffic through an HTTP(S) proxy (standard env vars, lowercase also accepted) |
| NOTION_DAILY_LOG_PAGE_ID | — | — | Only used by the daily-log MCP prompt |
HTTP-transport variables (MCP_TRANSPORT, PORT, HOST, MCP_AUTH_TOKEN, …) are covered in Remote / HTTP transport.
> Upgrading from v1.x? Your env vars all still work unchanged. The only break is the tool surface (v1's five tools became notion_execute + notion_describe); modern clients rediscover tools automatically. Details: MIGRATION.md.
It serves MCP Streamable HTTP at POST/GET/DELETE /mcp (stateful sessions via the mcp-session-id header) plus an unauthenticated GET /health. It's single-tenant — every request acts as the one NOTION_TOKEN the process started with.
| env | default | meaning |
| --- | --- | --- |
| MCP_TRANSPORT | stdio | set to http to enable HTTP |
| PORT | 3000 | listen port (0 = OS-assigned) |
| HOST | 127.0.0.1 | bind address; set 0.0.0.0 to expose externally (only with MCP_AUTH_TOKEN) |
| MCP_AUTH_TOKEN | — | when set, every /mcp request must send Authorization: Bearer <token> |
| MCP_ALLOWED_HOSTS | localhost + bound host | comma-list for DNS-rebinding Host allowlist |
| MCP_ALLOWED_ORIGINS | localhost origins | comma-list for browser Origin allowlist |
> ⚠️ Whoever reaches /mcp acts as your NOTION_TOKEN. On loopback (the default) that's just local processes. Before binding a non-loopback HOST, set MCP_AUTH_TOKEN (the server warns if you don't) and/or front it with an authenticating reverse proxy.
Connect from clients that support headers (Claude Code, Cursor, VS Code):
bashclaude mcp add --transport http notion https://your-host/mcp \
--header "Authorization: Bearer <MCP_AUTH_TOKEN>"
Try it locally:
bashcurl http://127.0.0.1:3000/health
notion_execute
Run any operation with a payload. Supports both single and batch operations, where the payload can be a single object or an array of items for batch processing.
notion_describe
Returns the JSON Schema and a working example for one operation, useful for understanding the structure of the operation before making complex calls.
The server exposes exactly two MCP tools — your client loads two schemas regardless of which of the 43 operations gets called.
Claude Desktop / Cursor
Paste into your MCP client config file to install this server.
{
"mcpServers": {
"notion": {
"env": {
"NOTION_TOKEN": "ntn_paste_your_token_here"
},
"args": [
"-y",
"notion-mcp-server"
],
"command": "npx"
}
}
}
Linux
{
"env": {
"NOTION_TOKEN": "ntn_paste_your_token_here"
},
"args": [
"-y",
"notion-mcp-server"
],
"command": "npx"
}
Macos
{
"env": {
"NOTION_TOKEN": "ntn_paste_your_token_here"
},
"args": [
"-y",
"notion-mcp-server"
],
"command": "npx"
}
Windows
{
"env": {
"NOTION_TOKEN": "ntn_paste_your_token_here"
},
"args": [
"/c",
"npx",
"-y",
"notion-mcp-server"
],
"command": "cmd"
}
It serves MCP Streamable HTTP at POST/GET/DELETE /mcp (stateful sessions via the mcp-session-id header) plus an unauthenticated GET /health. It's single-tenant — every request acts as the one NOTION_TOKEN the process started with.
| env | default | meaning |
| --- | --- | --- |
| MCP_TRANSPORT | stdio | set to http to enable HTTP |
| PORT | 3000 | listen port (0 = OS-assigned) |
| HOST | 127.0.0.1 | bind address; set 0.0.0.0 to expose externally (only with MCP_AUTH_TOKEN) |
| MCP_AUTH_TOKEN | — | when set, every /mcp request must send Authorization: Bearer <token> |
| MCP_ALLOWED_HOSTS | localhost + bound host | comma-list for DNS-rebinding Host allowlist |
| MCP_ALLOWED_ORIGINS | localhost origins | comma-list for browser Origin allowlist |
> ⚠️ Whoever reaches /mcp acts as your NOTION_TOKEN. On loopback (the default) that's just local processes. Before binding a non-loopback HOST, set MCP_AUTH_TOKEN (the server warns if you don't) and/or front it with an authenticating reverse proxy.
Connect from clients that support headers (Claude Code, Cursor, VS Code):
bashclaude mcp add --transport http notion https://your-host/mcp \
--header "Authorization: Bearer <MCP_AUTH_TOKEN>"
Try it locally:
bashcurl http://127.0.0.1:3000/health
Sign in to leave a review
Use Google, GitHub, or an email account so ratings stay tied to real people.
No reviews posted yet.



