Skip to main content
Glama
awkoy

notion-mcp-server

by awkoy

Notion MCP Server — Connect Claude, Cursor & VS Code to Notion

npm version NPM Downloads License Model Context Protocol Stars

Give your AI full read/write access to Notion with one token and one paste. This is an agent-first Notion MCP server: your AI client (Claude Code, Claude Desktop, Cursor, VS Code, Cline, Zed — anything that speaks MCP) can create pages, query databases, append blocks, apply templates, comment, and upload files in natural language.

Three reasons it exists when Notion ships its own MCP:

  • Built for agents, not humans-in-the-loop. Notion's hosted MCP is OAuth-only — it cannot run headless. This server authenticates with a token, so it works in CI, cron jobs, background agents, and self-hosted deployments where nobody can click "Authorize".

  • 94% smaller tool footprint at connection. Three MCP tools (1,005 tokens, the operation menus included) instead of one schema per endpoint — the official open-source server loads 17,163 tokens of tool schemas before you do anything. Operation schemas load on demand via notion_describe, so even a typical multi-operation task stays 75–90% lighter. Measured, reproducible →

  • The operational stuff is built in. Batched mutations with atomic rollback, idempotency keys, automatic retry on rate limits, slim token-efficient responses, full markdown round-trip, and self-healing validation errors that let the model fix its own bad payloads in one turn.

⚡ Quick start

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.

Claude Code

claude mcp add notion -s user \
  -e NOTION_TOKEN=ntn_paste_your_token_here \
  -- npx -y notion-mcp-server

Cursor

Install MCP Server

Click the badge (then replace YOUR_NOTION_TOKEN in the generated entry), or add to ~/.cursor/mcp.json yourself:

{
  "mcpServers": {
    "notion": {
      "command": "npx",
      "args": ["-y", "notion-mcp-server"],
      "env": { "NOTION_TOKEN": "ntn_paste_your_token_here" }
    }
  }
}

VS Code (Copilot agent mode)

Install in VS Code

VS Code prompts for the token on install and stores it as a secret input.

Gemini CLI

gemini extensions install https://github.com/awkoy/notion-mcp-server

The repo ships a gemini-extension.json, so Gemini CLI installs it as an extension: it asks for your Notion token once (kept in your system keychain) and starts the server with npx.

Claude Desktop

Easiest: the one-click extension. Download notion-mcp-server.mcpb from the latest release, double-click it (or drag into Claude Desktop → Settings → Extensions), paste your Notion token when prompted — done. No config files, Node.js not required.

Or via the config file: Settings → Developer → Edit Config, then add:

{
  "mcpServers": {
    "notion": {
      "command": "npx",
      "args": ["-y", "notion-mcp-server"],
      "env": { "NOTION_TOKEN": "ntn_paste_your_token_here" }
    }
  }
}

Quit Claude Desktop fully (Cmd+Q / tray → Quit) and reopen. Never used a config file before? Follow the step-by-step walkthrough for non-developers — it assumes nothing.

Docker / Podman / OrbStack

claude mcp add notion -s user \
  -e NOTION_TOKEN=ntn_paste_your_token_here \
  -- docker run --rm -i -e NOTION_TOKEN ghcr.io/awkoy/notion-mcp-server:latest

The -i flag is required (stdio transport). The image is OCI-compliant — Podman, OrbStack, colima, Rancher Desktop, Finch, and nerdctl all work with the same flags. For a long-running HTTP container (and the health check that goes with it), see Remote / HTTP transport.

Step 3 — try it. In a new chat:

"Use Notion to make a page called 'Hello from my agent' and add a checklist of three things to try today."

Your AI calls notion_write and replies with a live page link.

Related MCP server: Notion MCP Server

💡 What your AI can do with it

  • "Find every row in my Tasks database where Status is 'Doing' and tell me which are overdue." — typed where filters, flattened rows

  • "Rename these 50 pages to the new convention." — one batched call, 10-way parallel, idempotent retry

  • "Create a page from my 'Weekly review' template and fill in this summary." — Notion templates support

  • "Rewrite that spec page: fix the headings and add a code sample." — full markdown round-trip (get_page_markdown → edit → update_page_markdown)

  • "Comment on yesterday's meeting notes with a one-paragraph summary."

  • "Upload this diagram to the design page." — single- and multi-part file uploads

  • "Look at the screenshot on that bug report and tell me what's wrong."get_image hands the model the picture itself

Full capability list in Features; the complete operation catalog (47 ops) is in the Operations menu.

🧭 Which Notion MCP should you use?

Three options exist. Honest guidance:

Best for

Auth

Headless / CI

Notes

Notion hosted MCP (mcp.notion.com)

Interactive chat in claude.ai, ChatGPT, Cursor

OAuth (human must click; Notion says non-interactive auth is in the works)

First-party, ~34 markdown tools (11 of them Custom Agent session tools that need Notion AI), some plan-gated

Official open-source server

Token

Notion calls it deprecated and “no longer actively maintained”; the repo says it “may sunset” it and that issues and PRs are not actively monitored

This server

Agents, automation, CI, self-hosting, token-sensitive workloads

Token (PAT)

Actively maintained, agent-first design below

If you just want to chat with your Notion in claude.ai's web UI, use Notion's hosted connector — it's one click. Use this server when your agent runs unattended, when context/token cost matters, or when you want batch/idempotent semantics and self-hosting.

Capability

Official Notion MCP (open source)

This server

Tool surface

24 tools (one per endpoint), 17,163 tokens loaded into context

3 tools, 1,005 tokens — 94% less schema at connection

Operations covered

~24 endpoints

47 operations (plus a trash_page alias) across pages, blocks, databases, data sources, views, templates, comments, users, files

Batch mutations

Not documented

✅ Universal { items: [...] } envelope; up to 10 in parallel

Atomic batches + rollback

Not documented

atomic: true aborts on first failure, best-effort archives entities created earlier

Idempotency

Not documented

idempotency_key — same key + op returns the cached result for 5 minutes

Rate-limit handling

429s bubble up

✅ Token-bucket limiter (3 req/s default) + exponential backoff, honors Retry-After

Response shapes

Raw Notion SDK JSON

Slim shapers drop noise by default; verbose: true opts out

Database queries

Raw properties bag per row

Flattened name → primitive map (all 20+ property types)

Wire format

Default SDK serialization

Compact JSON — ~30% smaller payloads

Markdown

Page-level markdown tools

✅ Markdown accepted by create_page / append_blocks / update_block / comments + full round-trip (get_page_markdown / update_page_markdown), full GFM

Templates

create_page from a Notion template + list_data_source_templates discovery

File uploads

Not in the documented tool surface

✅ Single- and multi-part (5 MB chunks), MIME inferred

Validation errors

Plain error string

Self-healing: { code, message, path, issues, schema, example, fix } — corrected in one round-trip

Notion API version

Pinned 2026-03-11 (data sources, views, templates)

Real-world impact:

  • Renaming 50 pages — one notion_write call with { items: [...], concurrency: 10 } instead of 50 separate tool calls through the agent's reasoning loop: roughly an order of magnitude faster, and the prompt-token savings are the bigger win.

  • Tool list in context — 3 schema blobs per conversation instead of ~24, no matter which of the 47 operations get called; the operation menu ships inside them as enums, so the model never has to read a resource to learn what exists.

  • Reading a 100-row database — flattened rows are typically 5–10× fewer tokens than the raw properties bag, with no information loss.

🪄 Complete walkthrough (no coding required)

What you'll need

  1. A Notion account.

  2. The Claude Desktop app.

  3. About 5 minutes.

Step 1 — Get your Notion token

A Personal Access Token (PAT) is like a key that lets the AI act as you inside Notion — it sees every page you can see, with no per-page setup.

  1. Open app.notion.com/developers/tokens while logged into Notion — that's the Personal access tokens page of Notion's developer portal (also reachable from the app via Settings → Connections → Develop or manage integrationsPersonal access tokens in the sidebar).

  2. Click + New token.

  3. Name it (e.g. Claude), pick the workspace, leave the default Notion API capability checked, click Create token.

  4. Copy the token now — Notion shows it only once. It starts with ntn_. Treat it like a password.

PATs expire 1 year after creation — set a reminder to rotate. No "Personal access tokens" tab? Your admin disabled them; use the Internal Integration alternative.

Step 2 — Tell Claude Desktop about the server

  1. Open Claude Desktop → Claude menu (top-left on Mac, hamburger on Windows) → SettingsDeveloperEdit Config.

  2. A file named claude_desktop_config.json opens. Don't panic at the curly braces — it's just text.

  3. Select all (Cmd+A / Ctrl+A), delete, and paste:

{
  "mcpServers": {
    "notion": {
      "command": "npx",
      "args": ["-y", "notion-mcp-server"],
      "env": {
        "NOTION_TOKEN": "ntn_paste_your_token_here"
      }
    }
  }
}

This block tells Claude Desktop how to launch the connector. npx downloads and runs it automatically the first time. The only thing you change is the token.

  1. Replace ntn_paste_your_token_here with your token — keep the quotation marks.

  2. Save (Cmd+S / Ctrl+S).

  3. Quit Claude Desktop completely (Mac: Cmd+Q; Windows: tray icon → Quit) and reopen it.

Step 3 — Check and try

Type / in a new chat — you should see notion_read, notion_write and notion_describe in the list. Then ask:

"Use Notion to make a new page called 'Hello from Claude' and add a checklist of three things I want to try today."

Claude calls the tool and replies with a page link. If something's off, it's almost always a token typo or Claude Desktop not fully quit — see Troubleshooting.

🔧 Configuration

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.

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/tokensRevoke 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.

Environment variables

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; the whole URL or the bare 32-char id both work)

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_CONFIRM_DESTRUCTIVE

true/1 makes every destructive operation ask you to confirm first, through MCP elicitation — see Restricting operations

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 — see Files

NOTION_FILE_URLS

full

ref replaces Notion's signed file URLs (~1,650 chars, valid for an hour) in slim responses with short notion-file: refs that get_file_url / get_image resolve on demand — see Files

HTTPS_PROXY / HTTP_PROXY

Route all outbound traffic — Notion API calls and the downloads in get_image / upload_file's url source — 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 or v2.x? Your env vars all still work unchanged. The only break is the tool surface (v1's five tools, then v2's notion_execute, became notion_read + notion_write; notion_describe is unchanged); modern clients rediscover tools automatically. Details: MIGRATION.md.

Restricting operations

Limit what an agent can do with NOTION_ALLOWED_OPERATIONS (allowlist) and/or NOTION_BLOCKED_OPERATIONS (blocklist) — each a comma-separated list of group presets or exact operation names.

Preset

Expands to

read

every non-mutating operation

write

every mutating operation

destructive

operations whose purpose is removal (archive_page/trash_page, delete_block, batch_mixed_blocks, delete_comment, delete_view)

pages blocks databases data_sources views comments users files

every operation in that family (read and write)

Read-only deployment (most common):

{ "env": { "NOTION_TOKEN": "ntn_xxx", "NOTION_ALLOWED_OPERATIONS": "read" } }

Everything except destructive ops:

{ "env": { "NOTION_BLOCKED_OPERATIONS": "destructive" } }

Mix presets and individual ops:

{ "env": { "NOTION_ALLOWED_OPERATIONS": "read,append_blocks,add_page_comment" } }

Rules: case-insensitive; unknown tokens ignored with a warning; blocklist wins; an allowlist that resolves to zero operations disables everything (fail-closed). Disabled operations disappear from the tools' operation enums, from notion_describe and from the notion://operations menu, so a call naming one fails validation before it runs; when no write operation is enabled (NOTION_READ_ONLY, or an allowlist of reads) notion_write is not advertised at all.

On startup the server logs one line to stderr summarizing what resolved — check it first if the config doesn't behave as expected:

Operation access: 22/48 enabled (allow=read; block=(none))

Confirm instead of block. NOTION_CONFIRM_DESTRUCTIVE=true keeps destructive operations enabled but makes notion_write ask you before running one, through MCP elicitation: a yes/no dialog in your client that names the operation and its target — the page, database, data source or block title when one retrieve can fetch it (bounded to 5 s), otherwise the id; for a batch, how many items. Restores (restore_page, delete_database / delete_data_source with in_trash: false) and a batch_mixed_blocks call with no delete entry do not prompt, and a blocked operation is still rejected with operation_not_allowed before anyone is asked. Decline, cancel or answer no and the call returns confirmation_declined; the server instructions tell the model not to retry it and to ask you instead. A client that has not declared the elicitation capability gets confirmation_unavailable rather than a silent run — use a client that supports elicitation, unset the variable, or block destructive operations outright with NOTION_BLOCKED_OPERATIONS=destructive.

Domain

Read

Write

pages

search_pages get_page get_page_markdown

create_page set_page_title set_page_property set_page_properties update_page_markdown move_page restore_page archive_pagetrash_page

blocks

get_block get_block_children

append_blocks update_block delete_blockbatch_mixed_blocks

databases

query_database

create_database update_database delete_database

data_sources

list_data_sources get_data_source list_data_source_templates

update_data_source delete_data_source

views

list_views get_view query_view

create_view update_view delete_view

comments

list_comments get_comment

add_page_comment add_discussion_comment update_comment delete_comment

users

list_users get_user get_bot_user get_self

files

list_file_uploads get_file_upload get_file_url get_image

upload_file

† = also in the destructive group.

Limitations (control is per-operation, not per-parameter): update_page_markdown is a write op that can replace a page body, and blocking destructive does not disable it. For a guaranteed no-mutation deployment use NOTION_ALLOWED_OPERATIONS=read or NOTION_READ_ONLY=true. MCP prompts may still mention disabled operations, but execution is rejected.

Files

Uploads. upload_file takes its bytes as base64, a public url, or a local path the server reads directly. A path source can read any file the server process can, so when a model composes the path set NOTION_UPLOAD_ROOT to confine it: relative paths resolve inside the root, and symlinks are resolved before the check so they cannot point out of it.

File URLs. Notion mints a fresh signed S3 URL for every hosted file on every read — about 1,650 characters (~500 tokens), valid for an hour, different each time, and easy for a small model to mangle. NOTION_FILE_URLS=ref replaces them in the slim responses (get_page, search_pages, query_database, query_view, get_block, get_block_children, …) with short, stable refs:

Ref

Names

notion-file:block/<block-id>

The file in an image block

notion-file:page/<page-id>/<property>/<index>

One entry of a page's files property (property name URL-encoded)

Two read operations turn a ref back into content. Both re-read the object through the Notion API, so a ref stays valid for as long as the file does:

Operation

Payload

Returns

get_file_url

{ ref }

{ ref, url } — a fresh signed URL, good for about an hour

get_image

{ ref } (a ref, or a bare image-block id)

The image itself as MCP image content, so the model can look at it. Only image/* responses up to 5 MB; anything else is a tool error

get_image fetches only the URL Notion returned for a Notion-hosted file, never a URL supplied by the caller — so it cannot be steered at a LAN, a cloud metadata endpoint, or an exfil host. External URLs (linked images, external files) are short and stable already: they pass through untouched in either mode, and get_image returns them as text rather than fetching them. get_page_markdown is Notion's own rendered markdown and is not rewritten. The default, full, leaves every response as before.

🌐 Remote / HTTP transport

By default the server speaks stdio (the local path above). To run it as a remote/hosted endpoint — web clients, networked agents, shared deployments — set MCP_TRANSPORT=http:

MCP_TRANSPORT=http PORT=3000 NOTION_TOKEN=ntn_xxx npx -y notion-mcp-server
# -> notion-mcp-server vX.Y.Z running on http://127.0.0.1:3000/mcp

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

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

Try it locally:

curl http://127.0.0.1:3000/health
# -> {"status":"healthy","transport":"http","port":3000}
npx @modelcontextprotocol/inspector --transport http --server-url http://127.0.0.1:3000/mcp

In Docker:

docker run --rm -e NOTION_TOKEN=ntn_xxx -e MCP_TRANSPORT=http -e HOST=0.0.0.0 -e MCP_AUTH_TOKEN=change-me \
  -p 3000:3000 ghcr.io/awkoy/notion-mcp-server

HOST=0.0.0.0 is what makes the published port reachable — inside the container 127.0.0.1 is the container's own loopback — and a non-loopback bind is exactly where MCP_AUTH_TOKEN matters.

Health check. The image ships without a HEALTHCHECK: it starts in stdio mode, where nothing listens, so a built-in probe of /health would mark every stdio container unhealthy. For an HTTP deployment add one yourself — the same command sits in the Dockerfile, commented out:

docker run --rm -e NOTION_TOKEN=ntn_xxx -e MCP_TRANSPORT=http -e HOST=0.0.0.0 -e MCP_AUTH_TOKEN=change-me \
  -p 3000:3000 \
  --health-cmd "node -e \"fetch('http://127.0.0.1:'+(process.env.PORT||3000)+'/health').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))\"" \
  --health-interval 30s --health-timeout 3s --health-start-period 5s --health-retries 3 \
  ghcr.io/awkoy/notion-mcp-server

Or in Compose:

services:
  notion-mcp-server:
    image: ghcr.io/awkoy/notion-mcp-server:latest
    environment:
      NOTION_TOKEN: ${NOTION_TOKEN:?NOTION_TOKEN is required}
      MCP_TRANSPORT: http
      HOST: 0.0.0.0
      MCP_AUTH_TOKEN: ${MCP_AUTH_TOKEN:?MCP_AUTH_TOKEN is required}
    ports: ["3000:3000"]
    healthcheck:
      test: ["CMD", "node", "-e", "fetch('http://127.0.0.1:'+(process.env.PORT||3000)+'/health').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"]
      interval: 30s
      timeout: 3s
      start_period: 5s
      retries: 3

🌟 Features: what this Notion MCP server does

  • Three-tool surfacenotion_read (look), notion_write (change), notion_describe (learn the shape). Each tool's operation is an enum of what it runs, so the tool list is the menu and your client can auto-approve reads while writes still ask.

  • Universal batch envelope — every mutating op accepts { items: [...], atomic?, idempotency_key?, concurrency? } with per-item validation and results.

  • Atomic batches with best-effort rollbackatomic: 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 everywherecreate_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, tables, images, dividers, inline formatting), plus full round-trip via get_page_markdown / update_page_markdown.

  • Notion templatescreate_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.

  • Plain property values — a database row's properties take plain values: { Status: "Done", "Due Date": "2026-10-01", Tags: ["a", "b"], Done: true, Notes: null }. The server reads the data source's schema (cached for 5 minutes) and types each value; title always addresses the title property, an unknown property name or status option is rejected with the valid names, and the full Notion shapes still work. get_data_source lists select / multi_select / status options inline.

  • Typed where filter shorthandquery_database takes { Status: "Done", Priority: { in: ["High", "Medium"] }, OR: [...] }, resolves each property's type from the data source's schema and compiles it to Notion filter JSON; sorts: ["-Due Date"] sorts descending. Raw filter / object sorts still accepted.

  • Warnings, not rejections — an unknown top-level field is ignored, the call still runs, and the result's warnings names the field and the fields the operation accepts; a property name that differs only in case is corrected with a warning. No extra round-trip when the rest of the payload was right.

  • 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.

  • Short file refs + image readsNOTION_FILE_URLS=ref swaps Notion's ~500-token signed file URLs for notion-file: refs; get_file_url mints a fresh URL and get_image returns the picture as MCP image content. See Files.

  • Opt-in auto-paginationpaginate: 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 controlNOTION_READ_ONLY one-switch read-only mode plus per-operation allow/block lists.

📚 MCP tools (notion_read, notion_write & notion_describe)

The server exposes exactly three MCP tools — your client loads three schemas regardless of which of the 47 operations gets called. notion_read runs the read operations and notion_write the write operations; each tool's operation field is an enum of exactly the operations enabled on this server, so the menu ships with the tool list, a client can validate a call before sending it, and a name sent to the wrong tool fails in one round-trip with a message naming the right one.

Per-tool permissions

MCP clients grant permissions by tool name, so the split lets you approve reads once and keep writes behind a prompt. In Claude Code (~/.claude/settings.json or the project's .claude/settings.json, with notion being whatever you named the server):

{
  "permissions": {
    "allow": ["mcp__notion__notion_read", "mcp__notion__notion_describe"]
  }
}

Cursor's MCP settings offer the same per-tool allowlist. notion_read is annotated readOnlyHint: true and notion_write destructiveHint: true, for clients that read annotations.

notion_read

{ operation, payload } for any read operation — search, page and block reads, database queries, users, comments, files.

{ "operation": "search_pages", "payload": { "query": "Q3 plan" } }
// a page as markdown, addressed by the link Notion copies
{
  "operation": "get_page_markdown",
  "payload": { "page_id": "https://www.notion.so/Q3-plan-1f3c1a2b3c4d5e6f7a8b9c0d1e2f3a4b" }
}

notion_write

{ operation, payload } for any write operation, where payload is a single object or { items: [...] } for batch mode.

// single call
{
  "operation": "set_page_title",
  "payload": { "page_id": "<page-id>", "title": "Q3 plan" }
}

Every id field (page_id, block_id, database_id, view_id, …) also accepts a Notion URL — paste what Share → Copy link gives you and the server extracts the id. A block link's #fragment is used for block_id fields and a database link's ?v= for view_id fields.

// batch
{
  "operation": "set_page_title",
  "payload": {
    "items": [
      { "page_id": "<p1>", "title": "First" },
      { "page_id": "<p2>", "title": "Second" }
    ],
    "atomic": false,
    "concurrency": 3,
    "idempotency_key": "rename-pass-2026-07-02"
  }
}
// markdown shortcut (create_page, append_blocks, update_block, update_page_markdown)
{
  "operation": "create_page",
  "payload": {
    "parent": { "type": "page_id", "page_id": "<parent>" },
    "title": "Notes",
    "markdown": "# Heading\n\n- [ ] todo\n- [x] done\n\n```ts\nconst x = 1;\n```"
  }
}
// a database row: plain property values, typed from the data source's schema
{
  "operation": "create_page",
  "payload": {
    "parent": { "type": "data_source_id", "data_source_id": "<data-source-id>" },
    "title": "Write the report",
    "properties": { "Status": "In Progress", "Priority": "High", "Due Date": "2026-10-01", "Tags": ["q3", "docs"] }
  }
}
// upload a file and place it on a page in one call
{
  "operation": "upload_file",
  "payload": {
    "source": { "type": "path", "path": "~/Desktop/chart.png" },
    "attach_to": { "block_id": "<page-or-block-id>", "caption": "Q3 revenue" }
  }
}

If a payload doesn't validate, the error response includes the operation's full JSON Schema, a working example, and a fix hint — the next call can be corrected without a notion_describe round-trip.

notion_describe

Returns the JSON Schema + working example for one operation, plus tool (which of the two runs it) — useful before complex calls (filter expressions, mixed block batches, database property definitions).

{ "operation": "query_database" }

Operations menu (47 ops, plus one alias)

Read operations (get_*, list_*, search_pages, query_database, query_view) go through notion_read; everything else through notion_write. The notion://operations resource lists the tool next to each operation.

Area

Operations

Pages

create_page, get_page, set_page_title, set_page_property, set_page_properties, archive_page (alias: trash_page), restore_page, search_pages, move_page, get_page_markdown, update_page_markdown

Blocks

append_blocks, get_block, get_block_children, update_block, delete_block, batch_mixed_blocks

Databases

create_database, query_database, update_database, delete_database

Data sources

list_data_sources, get_data_source, update_data_source, delete_data_source, list_data_source_templates

Views

list_views, get_view, query_view, create_view, update_view, delete_view

Comments

list_comments, add_page_comment, add_discussion_comment, get_comment, update_comment, delete_comment

Users

list_users, get_user, get_bot_user, get_self

Files

upload_file, list_file_uploads, get_file_upload, get_file_url, get_image

The authoritative list (with batchability) is served as an MCP resource at notion://operations.

MCP resources

Clients that support resource attachment (@-mention) can pull Notion content into context without a tool call:

Resource URI

Returns

notion://operations

Markdown cheat sheet of every enabled operation

notion://page/<page_id>

Page body as markdown

notion://database/<data_source_id>

Data source schema as JSON

Dynamic resources route through the same auth, rate limiting, and access gating as tool calls.

❓ Troubleshooting the Notion MCP server

  • object_not_found / "Could not find …" — an Internal Integration token only sees pages explicitly Connected to it. Switch to a PAT to skip per-page sharing.

  • "Notion auth failed" on every call — token missing, revoked, or expired (PATs expire after 1 year). Check NOTION_TOKEN in your client config, then confirm the token is still listed as Active at app.notion.com/developers/tokens.

  • "No parent page configured" — pass parent in the call, or set NOTION_PAGE_ID.

  • multi_source_database from query_database or create_page — the database has multiple data sources. Call list_data_sources, then pass data_source_id (or a data_source_id parent) instead of database_id.

  • A successful result carries warnings — the call ran; each entry names a field that was ignored (misspelt or misplaced) or a property name that was corrected. Fix the payload next time, nothing to retry.

  • Tools don't appear in Claude Desktop — token typo (must stay inside the quotes) or the app wasn't fully quit (Cmd+Q, not window close) before reopening.

  • Startup logs "Notion auth check failed" but tools work — the startup check is best-effort; ignore if calls succeed.

  • Docker exits immediately / "Connection closed" — the -i flag is required: docker run --rm -i ….

  • Docker: "NOTION_TOKEN is not set" despite -e — use -e NOTION_TOKEN (forwards from parent env) or -e NOTION_TOKEN=ntn_xxx, not -e NOTION_TOKEN ntn_xxx.

Still stuck? GitHub Issues · Notion API reference · MCP spec

💬 FAQ: Notion MCP server

What is the Notion MCP server and how does it work?

A Model Context Protocol server that connects AI assistants — Claude, Cursor, VS Code Copilot, Cline, Zed, Continue, anything that speaks MCP — to your Notion workspace. It runs locally (or in Docker, or as an HTTP endpoint) and exposes three MCP tools the AI calls to read, write and inspect Notion operations. You authenticate once with a Notion token; everything else is natural language.

How do I connect Claude to Notion using MCP?

See the Quick start: get a PAT at app.notion.com/developers/tokens, then one claude mcp add command (Claude Code) or one JSON paste (Claude Desktop). Non-developers: the complete walkthrough assumes nothing.

What's the difference between this and Notion's official MCP?

Notion's hosted MCP (mcp.notion.com) is OAuth-only and built for interactive chat — it can't run headless (Notion says non-interactive authorization is in the works, but not yet). Their open-source server is, in Notion's words, "no longer actively maintained" and exposes one tool per endpoint. This server authenticates with a token (works in CI/automation), exposes 2 tools dispatching 47 operations, batches mutations with idempotency and retries, and slims responses to cut token cost. See Which Notion MCP should you use?.

Can I use it with Cursor, VS Code, ChatGPT, or Cline?

Cursor, VS Code (Copilot agent mode), Cline, Zed, Continue: yes — install badges and config blocks are in the Quick start. ChatGPT's built-in connectors require OAuth-hosted servers, so use Notion's hosted MCP there; developers can still reach this server from the OpenAI API's mcp tool by pointing it at a self-hosted HTTP endpoint with a bearer token.

Is it safe to give an AI my Notion token?

The token lives in your MCP client's local config and is only sent to api.notion.com over HTTPS. The server is open source — read every line. A PAT has the same access you do, so don't paste it into untrusted clients, and revoke it at app.notion.com/developers/tokens if a laptop is lost. For agents that should never write, set NOTION_READ_ONLY=true.

Does it work with self-hosted or local-only LLMs?

Yes — anything that speaks MCP stdio (or Streamable HTTP) works. The server doesn't care what's on the other side of the protocol.

🔒 Privacy

The server runs on your machine or your own host and talks only to api.notion.com, over HTTPS, with the token you configure. There is no telemetry, no analytics, and no server of ours in the path: nothing you read or write in Notion goes anywhere else. The token stays where your MCP client keeps it (its config file, or a keychain / secret store for clients that have one). With HTTPS_PROXY set, traffic goes through your proxy instead. get_image fetches only the signed URLs Notion returns for files it hosts, never a URL supplied by the model, and upload_file reads a local file only when asked to, inside NOTION_UPLOAD_ROOT when that is set. Notion's own handling of your data is covered by Notion's privacy policy.

🛠 Development

git clone https://github.com/awkoy/notion-mcp-server.git
cd notion-mcp-server
npm install
echo "NOTION_TOKEN=ntn_xxx" > .env

npm run build       # tsc -> build/
npm test            # vitest suite
npm run inspector   # MCP inspector against the built binary

Local build instead of npx:

claude mcp add notion -s user \
  -e NOTION_TOKEN=ntn_xxx \
  -- node "$(pwd)/build/index.js"

Everything the server logs goes to stderr, as before, and is also sent to the client as MCP notifications/message entries (logger notion-mcp-server), so it shows up in the client's own log view — VS Code's output channel, MCP Inspector, Claude Desktop's logs — where stderr is usually hidden. The server honours logging/setLevel; the default is info. At debug you also get one line per notion_read / notion_write call (operation, batch size, duration, ok or error — never the payload or page content).

  • TypeScript + MCP TypeScript SDK v2 (@modelcontextprotocol/server + @modelcontextprotocol/node 2.0.0); stdio + Streamable HTTP transports

  • Notion SDK @notionhq/client@^5.22.0, pinned Notion-Version: 2026-03-11

  • Zod 4 payload validation; emits draft-7 JSON Schema with $defs deduplication for error envelopes

  • Markdown → Notion blocks via remark / remark-gfm

  • Bounded-concurrency batch worker (default 3, max 10); shared token-bucket rate limiter; withRetry with exponential backoff around every dispatched call

  • In-memory idempotency cache (5-minute TTL, 512 entries)

  • Slim shapers per entity type with verbose: true opt-out

  • Vitest suite covering the markdown parser, shapers, schema emitter, dispatcher, batch semantics (partial success / atomic rollback / idempotency), access control, and HTTP transport

End-to-end smoke test

npm test runs against a mocked Notion client. scripts/e2e.mjs drives the built server over stdio against a real workspace — every read operation, the resources and prompts, notion_describe for every operation, and (with --write) every write operation inside one throwaway page:

npm run build
printf 'NOTION_TOKEN=ntn_...\nNOTION_PAGE_ID=<page the token can write under>\n' > .env   # gitignored
npm run e2e                      # read-only pass
npm run e2e -- --write           # full pass; creates one page under NOTION_PAGE_ID and trashes it at the end
npm run e2e -- --write --keep    # keep the test page for inspection

It prints a PASS/FAIL table per check and lists any operation the run did not reach, and exits non-zero on a failure. It is not part of CI.

🤝 Contributing

PRs welcome. Fork → branch → commit → push → PR. Run npm test before submitting.

📄 License

MIT — see LICENSE.


mcp-name: io.github.awkoy/notion-mcp-server

Available Tools

2 tools
notion_describeNotion DescribeA
Read-only

Return the JSON Schema and a working example for one operation. Use this BEFORE notion_execute when the payload shape is non-trivial (query filters, structured block trees, database property definitions). For simple ops, just call notion_execute — its errors carry the schema.

ParametersJSON Schema
NameRequiredDescriptionDefault
operationYesOperation name to describe.

TDQS

A4.7/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations already indicate readOnlyHint=true; description adds that it returns schema and example without contradicting. No additional behavioral traits needed.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Two sentences, no wasted words, information front-loaded. Every sentence adds value.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a simple tool with one parameter and no output schema, the description fully explains what it returns and when to use it. Complete and appropriately sized.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 100% for the single parameter, so baseline is 3. Description adds no additional meaning beyond the schema, which is sufficient.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description explicitly states it returns 'JSON Schema and a working example for one operation', which clearly defines the action and resource. It also distinguishes from the sibling tool notion_execute.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Provides clear guidance: use BEFORE notion_execute for non-trivial payloads, otherwise just call notion_execute. Includes explicit alternatives and when-not-to-use.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

notion_executeNotion ExecuteA
Destructive

Execute a Notion operation by name.

Two ways to call: • Single: { operation: "set_page_title", payload: { page_id, title } } • Batch: { operation: "set_page_title", payload: { items: [{page_id, title}, ...], atomic?: false, idempotency_key?: "...", concurrency?: 3 } }

If the payload is malformed, the error response includes the full schema + a working example so you can correct and retry in one round-trip. Call notion_describe(operation) ahead of time only for complex shapes (query_database filters, batch_mixed_blocks).

Most responses are slimmed by default. Pass verbose:true inside payload (single) or per-item (batch) to get the raw Notion SDK response.

ParametersJSON Schema
NameRequiredDescriptionDefault
payloadYesOperation parameters. Pass either single-op fields directly, or { items: [...], atomic?, idempotency_key?, concurrency? } for batch.
operationYesOperation name. See notion_describe for the schema of any operation, or read the notion://operations resource for the full menu. Common ops: set_page_title, append_blocks, get_page, search_pages, query_database.

TDQS

A4.9/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Discloses default slimmed responses, optional verbose mode, batch features (atomic, idempotency_key, concurrency), and error response behavior. Annotations indicate destructiveHint=true and openWorldHint=true, and the description adds valuable operational context without contradiction.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is well-structured with bullet points and front-loaded purpose. While every sentence adds value, it could be slightly more concise, but it remains efficient for the complexity.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a complex, open-world tool with no output schema, the description thoroughly covers usage patterns (single/batch), response behavior (verbose, slimmed), error handling, and links to companion tool notion_describe. No gaps remain.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters5/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 100% but the description enriches both parameters: 'operation' links to notion_describe and lists common ops; 'payload' explains single vs batch structure and optional flags (verbose, atomic, etc.), adding substantial meaning beyond schema.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states 'Execute a Notion operation by name' and provides concrete examples of single and batch calls. It distinguishes itself from the sibling notion_describe tool, making the purpose unambiguous.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Explicitly advises when to use notion_describe ('for complex shapes') and when to use this tool directly. Also explains how to handle malformed payloads with automatic schema feedback, guiding effective retries.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections. Dates show when Glama detected each change.

  1. 15 tool updatesv1.0.1
    • Removedappend_block_children
    • Removedarchive_page
    • Removedbatch_append_block_children
    • Removedbatch_delete_blocks
    • Removedbatch_mixed_operations
    • Removedbatch_update_blocks
    • Removedcreate_page
    • Removeddelete_block
    • Addednotion_describe
    • Addednotion_execute
    • Removedrestore_page
    • Removedretrieve_block
    • Removedretrieve_block_children
    • Removedsearch_pages
    • Removedupdate_block
  2. 13 tool updatesv1.0.0
    • First observedappend_block_children
    • First observedarchive_page
    • First observedbatch_append_block_children
    • First observedbatch_delete_blocks
    • First observedbatch_mixed_operations
    • First observedbatch_update_blocks
    • First observedcreate_page
    • First observeddelete_block
    • First observedrestore_page
    • First observedretrieve_block
    • First observedretrieve_block_children
    • First observedsearch_pages
    • First observedupdate_block

TDQS

A4.5/5.0
Disambiguation5/5

The two tools have distinct, complementary roles: notion_describe provides schema guidance for complex payloads, while notion_execute performs the actual operations. No overlap in purpose.

Naming Consistency5/5

Both tools follow a clear 'notion_' prefix with a verb (describe/execute), maintaining a consistent naming pattern.

Tool Count3/5

With only 2 tools, the server feels minimal for a comprehensive Notion API wrapper. However, the design centralizes operations into notion_execute, which could be suitable if the goal is a thin execution layer.

Completeness2/5

The tool surface lacks explicit representation of common Notion operations (e.g., query database, create page, list blocks). All operations are hidden behind the generic notion_execute tool, making it hard for agents to discover capabilities without external documentation.

Maintenance

ActivityActive
ResponsivenessWithin a week

Resources

Unclaimed servers have limited discoverability.

Looking for Admin?

If you are the server author, to access and configure the admin panel.

Related MCP Connectors

Related MCP Servers

  • -
    license
    Not graded
    quality
    Not graded
    maintenance
    An MCP server that enables natural language interaction with the Notion API, allowing users to search, comment, create pages, and access content within their Notion workspace.
    172,212
    -
  • A
    license
    A
    quality
    C
    maintenance
    An MCP server for Notion API with optimized token efficiency and full database property filtering, enabling AI assistants to manage pages, databases, and blocks.
    32
    38
    1
    MIT

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/awkoy/notion-mcp-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server