Notion

by awkoy

92 6k downloads Not rated yet MIT

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:

  1. Download and install Highlight from highlightai.com/download
  2. Navigate to the plugins tab and select "Add Custom Plugin"
  3. Configure the plugin with the settings below
    Plugin Name Notion
    Command (node, npx, python, etc.) npx
    Arguments
    • 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.

  4. 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.)

Notion developer portal — the Personal access tokens page with the + New token button in the top right

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):

bash
claude mcp add --transport http notion https://your-host/mcp \
--header "Authorization: Bearer <MCP_AUTH_TOKEN>"

Try it locally:

bash
curl 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):

bash
claude mcp add --transport http notion https://your-host/mcp \
--header "Authorization: Bearer <MCP_AUTH_TOKEN>"

Try it locally:

bash
curl http://127.0.0.1:3000/health

No reviews yet — be the first

Sign in to leave a review

Use Google, GitHub, or an email account so ratings stay tied to real people.

Email sign in

No reviews posted yet.

Videos about Notion

Relevant YouTube tutorials, setups, and demos