notion-mcp-server
Connect your AI assistant to Notion with token-based read/write access through three MCP tools (notion_read, notion_write, notion_describe) covering 47 operations across pages, blocks, databases, data sources, views, comments, users, and files.
Pages: search, get, create, update title/properties, rewrite via markdown, move, archive/trash/restore, and apply templates.
Blocks: get children, append, update, delete, and mixed block batches, with markdown accepted.
Databases & data sources: create/update/delete, query with typed
wherefilters and sorts, list templates, and handle multi-source databases.Views: list, get, query (with stored filters/sorts), create, update, delete.
Comments: list, add page/discussion comments, get, update, delete.
Users: list, get, bot user, self.
Files: upload from base64/URL/path, list/uploads, get signed URLs, and view images via
get_image.Batch & reliability: batch mutations (up to 10 parallel), atomic rollback, idempotency keys, rate-limit handling, retries.
Efficiency & control: slim responses, markdown round-trip, verbose opt-out, read-only mode, operation allow/block lists, destructive-action confirmation.
Transports: stdio for local clients and Streamable HTTP for remote/Docker deployments.
Provides tools for reading, creating, and modifying Notion content through natural language interactions, including page operations (creation, archiving, restoration, searching), block operations (retrieval, appending, updating, deleting), and batch operations for efficient content management.
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@notion-mcp-servercreate a new page with today's tasks"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
Notion MCP Server — Connect Claude, Cursor & VS Code to Notion
Give your AI read/write access to Notion with one token and one command. Claude Code, Claude Desktop, Cursor, VS Code, Cline, Zed, anything that speaks MCP: it can create pages, query databases, append blocks, apply templates, comment and upload files, in plain language.
Notion ships its own MCP server. Where this one differs:
It authenticates with a token, so it runs headless. Notion's hosted MCP is OAuth-only and someone has to click "Authorize". This one works in CI, cron jobs, background agents and self-hosted deployments.
It doesn't spend your context on tool schemas. The official open-source server loads 24 endpoint schemas into the model's context at connection: 17,163 tokens, re-sent with every request for the rest of the session. This one loads three tools, 1,005 tokens. That is 94% less, 17× smaller, and operation schemas are fetched only when a task actually touches one, so even a heavy eight-operation session stays 76% lighter. Measured, reproducible →
Responses are slimmed on the way back too: a database query returns flat name → value rows, typically 5–10× fewer tokens than Notion's raw properties bags, with nothing lost. Batched mutations with atomic rollback, idempotency keys, retry on rate limits and self-healing validation errors are built in, and the comparison below has the rest.
Quick start
1. Get a Notion token. Open app.notion.com/developers/tokens → + New token → name it, pick your workspace → Create token → copy the ntn_… value. A Personal Access Token sees everything you can see, with no per-page sharing. (Page missing or empty? Your admin disabled PATs — see auth alternatives.)
2. Install it.
npx add-mcp notion-mcp-server --env NOTION_TOKEN=ntn_paste_your_token_hereadd-mcp finds the MCP clients on your machine and writes the config for the ones you pick: Claude Code, Claude Desktop, Cursor, VS Code, Codex, Gemini CLI, Cline, Windsurf, Zed and a dozen others. Add -g to install at user level instead of the current project, -a claude-code to skip the picker, --all to write every client at once.
Keep the
--envflag. Without it the entry is written without a token, and the server starts and then fails every call with an auth error.
Any client that reads an mcpServers block (Cursor's ~/.cursor/mcp.json, Claude Desktop's claude_desktop_config.json, Cline's settings, Zed, Continue…):
{
"mcpServers": {
"notion": {
"command": "npx",
"args": ["-y", "notion-mcp-server"],
"env": { "NOTION_TOKEN": "ntn_paste_your_token_here" }
}
}
}Claude Code:
claude mcp add notion -s user \
-e NOTION_TOKEN=ntn_paste_your_token_here \
-- npx -y notion-mcp-serverClaude Code speaks the 2025-era protocol over stdio unless told otherwise. Set MCP_PROTOCOL_NEGOTIATION=auto in its environment and it probes for MCP 2026-07-28 (stateless requests, cache hints on every list). The server serves both.
Cursor: — click, then replace
YOUR_NOTION_TOKEN in the generated entry.
VS Code (Copilot agent mode): — VS Code prompts for the token and stores it as a secret input.
Gemini CLI:
gemini extensions install https://github.com/awkoy/notion-mcp-serverThe repo ships a gemini-extension.json, so this installs as an extension: it asks for the token once, keeps it in your system keychain, and starts the server with npx.
Claude Desktop, without Node.js: download notion-mcp-server.mcpb from the latest release and double-click it (or drag it into Settings → Extensions), then paste your token when prompted. Never edited a config file before? The step-by-step walkthrough 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:latestThe -i flag is required for stdio. The image is OCI-compliant, so Podman, OrbStack, colima, Rancher Desktop, Finch and nerdctl take the same flags. For a long-running HTTP container see Remote / HTTP transport.
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
wherefilters, 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."
"Rewrite that spec page: fix the headings and add a code sample." — markdown round-trip via
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 uploads
"Look at the screenshot on that bug report and tell me what's wrong." —
get_imagehands the model the picture itself
The full catalog is the operations menu: 47 operations behind three tools.
Which Notion MCP should you use?
Best for | Auth | Headless / CI | Notes | |
Notion hosted MCP ( | Interactive chat in claude.ai, ChatGPT, Cursor | OAuth (a 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 |
— | 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 |
To chat with your Notion in claude.ai's web UI, use Notion's hosted connector: it's one click. Use this server when the agent runs unattended, when context cost matters, or when you want batch and idempotent semantics and your own host.
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 |
Batch mutations | Not documented | ✅ Universal |
Atomic batches + rollback | Not documented | ✅ |
Idempotency | Not documented | ✅ |
Rate-limit handling | 429s bubble up | ✅ Token-bucket limiter (3 req/s default) + exponential backoff, honors |
Response shapes | Raw Notion SDK JSON | Slim shapers drop noise by default; |
Database queries | Raw | Flattened name → primitive map (all 20+ property types) |
Writing properties | Full Notion property JSON | Plain values: |
Filters | Raw Notion filter JSON | Typed |
Unknown fields | Rejected | Ignored with a |
Pagination | Manual cursors | Opt-in |
Wire format | Default SDK serialization | Compact JSON — ~30% smaller payloads |
Markdown | Page-level markdown tools | ✅ Accepted by |
Templates | — | ✅ |
Database views | — | ✅ list / get / query / create / update / delete; |
File uploads | Not in the documented tool surface | ✅ Single- and multi-part (5 MB chunks), MIME inferred |
Validation errors | Plain error string | Self-healing: |
Notion API version | — | Pinned |
What that buys you in practice: renaming 50 pages is one notion_write call with { items: [...], concurrency: 10 } rather than 50 trips through the agent's reasoning loop, and the prompt-token savings are the bigger half of the win. The benchmark has the method, the tokenizer and an honest worst case.
Configuration
Token: PAT or internal integration
Both go in the same NOTION_TOKEN env var; only where you get them 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 | A Connect step per page or database |
Use when | Default: personal and team workspaces, prototyping | An admin requires explicit per-resource scoping, or for shared production bots |
💡 Most
object_not_founderrors are the wrong auth choice rather than 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 and update pages and databases where you have edit rights; comment as you; upload files. Can't: reach pages you can't see, bypass workspace permissions, act as another user, or change admin settings. A PAT's scope is your account, so 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 month 11.
Revoking: app.notion.com/developers/tokens → Revoke next to the token, effective immediately. Workspace admins can revoke anyone's from Settings & members → Connections → All personal access tokens.
Admin disabled PATs? Ask them to enable it, 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 |
| ✅ | — | PAT ( |
| — | — | Default parent for |
| — |
| Requests/second for the shared limiter (Notion's documented per-integration limit) |
| — | — |
|
| — | all | Comma-separated allowlist of operations or group presets — see Restricting operations |
| — | — | Comma-separated blocklist (same vocabulary); wins over the allowlist |
| — | — |
|
| — | — | Confine |
| — |
|
|
| — | — | Route all outbound traffic — Notion API calls and the downloads in |
| — | — | Only used by the daily-log MCP prompt |
HTTP-transport variables (MCP_TRANSPORT, PORT, HOST, MCP_AUTH_TOKEN, …) are in Remote / HTTP transport.
Upgrading from v1.x or v2.x? Every env var still works unchanged. The break is the tool surface: v1's five tools, then v2's
notion_execute, becamenotion_read+notion_write, andnotion_describeis as it was. Modern clients rediscover tools automatically. Details in MIGRATION.md.
Restricting operations
NOTION_ALLOWED_OPERATIONS (allowlist) and NOTION_BLOCKED_OPERATIONS (blocklist) each take a comma-separated list of group presets or exact operation names.
Preset | Expands to |
| every non-mutating operation |
| every mutating operation |
| operations whose purpose is removal ( |
| every operation in that family, read and write |
{ "env": { "NOTION_ALLOWED_OPERATIONS": "read" } } // read-only, the common case
{ "env": { "NOTION_BLOCKED_OPERATIONS": "destructive" } } // everything except removals
{ "env": { "NOTION_ALLOWED_OPERATIONS": "read,append_blocks,add_page_comment" } }Names are case-insensitive, unknown tokens are ignored with a warning, the blocklist wins, and an allowlist that resolves to zero operations disables everything (fail-closed). Disabled operations vanish from the tools' operation enums, from notion_describe and from the notion://operations menu, so naming one fails validation before it runs; when no write operation is enabled, notion_write is not advertised at all. One line on stderr at startup says what resolved. Check it first when the config doesn't behave:
Operation access: 22/48 enabled (allow=read; block=(none))Confirm instead of block. NOTION_CONFIRM_DESTRUCTIVE=true keeps destructive operations available and makes notion_write ask you before running one, through MCP elicitation: an elicitation/create request on 2025-era clients, an input_required round trip on MCP 2026-07-28 clients, where the retry carries a sealed requestState that only matches the call it was minted for. You get a yes/no dialog naming the operation and its target (the page, database, data source or block title when one retrieve can fetch it within 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 never 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 and to ask you instead. A client that hasn't 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.
Domain | Read | Write |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| — |
|
|
|
† = 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 slim responses (get_page, search_pages, query_database, query_view, get_block, get_block_children, …) with short, stable refs.
Ref | Names | Resolved by |
| The file in an image block |
|
| One entry of a page's |
|
Both resolvers re-read the object through the Notion API, so a ref stays valid as long as the file does. get_image fetches only the URL Notion returned for a Notion-hosted file, never one supplied by the caller, so it cannot be steered at a LAN host, a cloud metadata endpoint or an exfil target. 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 it was.
Remote / HTTP transport
The server speaks stdio by default. Set MCP_TRANSPORT=http to run it as a remote endpoint instead, for web clients, networked agents and shared deployments:
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/mcpIt serves MCP Streamable HTTP on /mcp for both current protocol generations, picked per request from what the client sends. MCP 2026-07-28 clients get the stateless path, where every POST stands alone: no session, server/discover, cache hints on every list. 2024-11-05 … 2025-11-25 clients get sessions via the mcp-session-id header plus the GET stream and DELETE, and a GET/DELETE without a session id is answered 405. There is also an unauthenticated GET /health. The process is single-tenant: every request acts as the one NOTION_TOKEN it started with.
env | default | meaning |
|
| set to |
|
| listen port ( |
|
| bind address; set |
| — | when set, every |
| localhost + bound host | comma-list for DNS-rebinding |
| localhost origins | comma-list for browser |
⚠️ Whoever reaches
/mcpacts as yourNOTION_TOKEN. On loopback, the default, that means local processes only. Before binding a non-loopbackHOST, setMCP_AUTH_TOKEN(the server warns if you don't) and put an authenticating reverse proxy in front of it.
Connecting from a client that supports headers (Claude Code, Cursor, VS Code), and checking it locally:
claude mcp add --transport http notion https://your-host/mcp \
--header "Authorization: Bearer <MCP_AUTH_TOKEN>"
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/mcpIn Docker, HOST=0.0.0.0 is what makes the published port reachable, since inside the container 127.0.0.1 is the container's own loopback. A non-loopback bind is exactly where MCP_AUTH_TOKEN earns its keep:
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-serverClaude Desktop builds affected by anthropics/claude-code#93290 send a 2026-07-28 body under a
MCP-Protocol-Version: 2025-11-25header. The server realigns that one known mismatch so those builds work; every other header/body disagreement gets the rejection the spec prescribes (-32020).
The image ships without a HEALTHCHECK because it starts in stdio mode, where nothing listens and a built-in probe of /health would mark every stdio container unhealthy. Add one yourself for an HTTP deployment. The same command sits commented out in the Dockerfile, and works as --health-cmd on docker run too:
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: 3MCP tools
Three tools, whichever of the 47 operations you end up calling. notion_read runs the reads, notion_write the writes, and notion_describe returns one operation's JSON Schema plus a working example, which is worth a round-trip before a complex call: filter expressions, mixed block batches, database property definitions. Each tool's operation field is an enum of exactly what this server has enabled, 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.
Every id field (page_id, block_id, database_id, view_id, …) also accepts a Notion URL, so paste what Share → Copy link gives you. A block link's #fragment is used for block_id fields and a database link's ?v= for view_id fields.
// notion_read
{ "operation": "search_pages", "payload": { "query": "Q3 plan" } }
{ "operation": "get_page_markdown", "payload": { "page_id": "https://www.notion.so/Q3-plan-1f3c…" } }
// notion_write, single call
{ "operation": "set_page_title", "payload": { "page_id": "<page-id>", "title": "Q3 plan" } }
// notion_write, batch: every mutating op takes { items: [...], atomic?, concurrency?, idempotency_key? }
{
"operation": "set_page_title",
"payload": {
"items": [{ "page_id": "<p1>", "title": "First" }, { "page_id": "<p2>", "title": "Second" }],
"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", "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" }
}
}A payload that doesn't validate comes back with the operation's full JSON Schema, a working example and a fix hint, so the next call can be corrected without a notion_describe round-trip.
Per-tool permissions
MCP clients grant permission by tool name, so the read/write 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, where notion is 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.
Operations menu (47 ops, plus one alias)
Reads (get_*, list_*, search_pages, query_database, query_view) go through notion_read, everything else through notion_write.
Area | Operations |
Pages |
|
Blocks |
|
Databases |
|
Data sources |
|
Views |
|
Comments |
|
Users |
|
Files |
|
The authoritative list, with batchability and the tool that runs each op, 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. Dynamic resources route through the same auth, rate limiting and access gating as tool calls.
Resource URI | Returns |
| Markdown cheat sheet of every enabled operation |
| Page body as markdown |
| Data source schema as JSON |
Troubleshooting
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 last a year). Check
NOTION_TOKENin your client config, then that the token is still Active at app.notion.com/developers/tokens. Installed withadd-mcpand skipped--env? The entry has no token; re-run with it."No parent page configured" — pass
parentin the call, or setNOTION_PAGE_ID.multi_source_databasefromquery_databaseorcreate_page— the database has several data sources. Calllist_data_sources, then passdata_source_id(or adata_source_idparent) instead ofdatabase_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 (it 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 it if calls succeed.
Docker exits immediately / "Connection closed" — the
-iflag is required:docker run --rm -i ….Docker: "NOTION_TOKEN is not set" despite
-e— write-e NOTION_TOKEN(forwards from the parent env) or-e NOTION_TOKEN=ntn_xxx, not-e NOTION_TOKEN ntn_xxx.
Still stuck? GitHub Issues · FAQ · Notion API reference · MCP spec
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. No telemetry, no analytics, 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, in its config file or in a keychain 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 binaryPoint a client at the local build instead of npx:
claude mcp add notion -s user -e NOTION_TOKEN=ntn_xxx -- node "$(pwd)/build/index.js"Logs go to stderr and are also sent to the client as MCP notifications/message entries (logger notion-mcp-server), so they show up in the client's own log view — VS Code's output channel, MCP Inspector, Claude Desktop's logs — where stderr is usually hidden. 2025-era clients pick the level with logging/setLevel (default info); MCP 2026-07-28 clients have no such call and ask per request with the io.modelcontextprotocol/logLevel envelope key, so a request without it gets no log notifications. Stderr is unaffected either way. 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/node2.0.0); stdio + Streamable HTTP transports; protocol revisions 2024-11-05 through 2026-07-28 (serveStdio/createMcpHandlerfor the stateless 2026-07-28 path, the sessionful transport for the rest)Notion SDK
@notionhq/client@^5.22.0, pinnedNotion-Version: 2026-03-11Zod 4 payload validation; emits draft-7 JSON Schema with
$defsdeduplication for error envelopesMarkdown → Notion blocks via
remark/remark-gfmBounded-concurrency batch worker (default 3, max 10); shared token-bucket rate limiter;
withRetrywith exponential backoff around every dispatched callIn-memory idempotency cache (5-minute TTL, 512 entries)
Slim shapers per entity type with
verbose: trueopt-outVitest suite covering the markdown parser, shapers, schema emitter, dispatcher, batch semantics (partial success / atomic rollback / idempotency), access control, and HTTP transport
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
npm run e2e -- --modern # any of the above as an MCP 2026-07-28 client (stateless envelope, input_required confirmations)It prints a PASS/FAIL table per check, lists any operation the run did not reach, and exits non-zero on 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
3 toolsnotion_describeNotion DescribeARead-only
Return the JSON Schema and a working example for one operation, plus which tool runs it (notion_read or notion_write). Use this BEFORE calling the operation when the payload shape is non-trivial (query filters, structured block trees, database property definitions). For simple ops, just call it — errors carry the schema.
| Name | Required | Description | Default |
|---|---|---|---|
| operation | Yes | Operation name to describe, as listed by notion_read / notion_write. |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already declare readOnlyHint=true and destructiveHint=false. The description adds meaningful behavioral context: the tool returns both a schema and a working example, determines which sibling tool executes the operation, and that errors carry the schema, making the describe call skippable for simple operations. This goes beyond the structured annotations without contradicting them.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Two sentences with no filler. The primary return value is stated first, followed by precise usage conditions and an explicit exclusion case. Every clause earns its place.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
For a lightweight describe tool with one parameter, clear annotations, and no output schema, the description fully covers what the agent needs: what it returns, when to call it, and when to skip it. The mention of notion_read/notion_write links it to its siblings adequately.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema coverage is 100% and the single parameter 'operation' is well-documented in the schema. The description adds only marginal semantic value by clarifying that the operation name is 'as listed by notion_read / notion_write', which is useful but largely redundant with the schema description. Baseline 3 is appropriate.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description names a specific verb ('Return') and resource ('JSON Schema and a working example for one operation, plus which tool runs it'). It clearly distinguishes this meta-tool from the sibling tools notion_read and notion_write by stating it describes operations rather than performing them.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Explicitly states when to use it ('BEFORE calling the operation when the payload shape is non-trivial'), gives concrete examples of such cases, and explains when it is unnecessary ('For simple ops, just call it'). This is decisive routing guidance with no ambiguity.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
notion_readNotion ReadARead-onlyIdempotent
Run one Notion read operation by name. Nothing is modified.
Call: { operation, payload } — payload carries that operation's fields. Common: search_pages { query }, get_page { page_id }, get_page_markdown { page_id }, query_database { database_id, where? }, get_block_children { block_id }.
Responses are slimmed; pass verbose:true in payload for the raw Notion object. Every id field (page_id, block_id, database_id, view_id, …) also accepts a Notion URL, as copied from Share → Copy link. A block link's #fragment is used for block_id fields and a database link's ?v= for view_id fields.
If the payload is malformed, the error response includes the 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).
| Name | Required | Description | Default |
|---|---|---|---|
| payload | Yes | Operation parameters. Pass either single-op fields directly, or { items: [...], atomic?, idempotency_key?, concurrency? } for batch. | |
| operation | Yes | The read operation to run. This list is the complete menu of read operations enabled on this server; notion_describe(operation) returns any operation's full schema. |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already indicate read-only, idempotent, and non-destructive behavior; the description adds valuable behavioral details: responses are slimmed, verbose:true returns raw Notion objects, id fields accept Notion URLs with fragment/v parameters, and malformed payloads return a schema plus working example for one-round-trip retry.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Front-loaded with the core purpose, then the call shape, common operations, and edge-case behavior. Every sentence contributes actionable detail; no fluff or repetition of schema contents.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
For a multi-operation read dispatcher with many enum values and no output schema, the description covers operation selection, payload examples, response verbosity, URL input flexibility, error recovery, and when to consult notion_describe. An agent has everything needed to call and recover from errors.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Although schema coverage is 100%, the description enriches parameter meaning substantially: it shows the { operation, payload } dispatch shape, gives per-operation example payloads, explains the verbose flag, and clarifies URL flexibility for every id field. This goes well beyond enum/property names in the schema.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
States a specific action and scope: 'Run one Notion read operation by name.' Immediately clarifies that 'Nothing is modified,' and lists common operations, making it distinguishable from notion_describe (which only returns schemas) and notion_write (which modifies).
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Gives explicit routing guidance: 'Call notion_describe(operation) ahead of time only for complex shapes (query_database filters).' It also explains the call shape and provides concrete examples for common operations, so the agent knows how to select and invoke this tool.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
notion_writeNotion WriteADestructive
Run one Notion write operation by name. Archive, trash and delete operations remove content — confirm with the user before running them.
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 } } create_page, append_blocks, update_block and update_page_markdown also take a markdown string.
Responses are slimmed; pass verbose:true inside payload (single) or per item (batch) for the raw Notion object. Every id field (page_id, block_id, database_id, view_id, …) also accepts a Notion URL, as copied from Share → Copy link.
If the payload is malformed, the error response includes the 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 (block trees, database property definitions, batch_mixed_blocks).
| Name | Required | Description | Default |
|---|---|---|---|
| payload | Yes | Operation parameters. Pass either single-op fields directly, or { items: [...], atomic?, idempotency_key?, concurrency? } for batch. | |
| operation | Yes | The write operation to run. This list is the complete menu of write operations enabled on this server; notion_describe(operation) returns any operation's full schema. |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Despite annotations already flagging destructive behavior, the description goes further by naming exactly which operations (archive, trash, delete) remove content and instructing the agent to confirm with the user. It also discloses response slimming, the verbose:true escape hatch, URL acceptance for IDs, and the error-recovery behavior. This adds significant value beyond the annotations and never contradicts them.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The description is compact for the complexity it covers, front-loading the core purpose and destructive warning. It uses bullet-like formatting for the call modes and keeps each sentence informative. It is a bit dense—three paragraphs of dense detail—but there is no filler or repetition; it earns its length.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
For a tool with 26 operations, batch support, and no output schema, the description covers every critical aspect an agent needs: the destructive actions, call syntax, response slimming, URL handling, error feedback, and when to consult notion_describe. Nothing essential is missing; it even handles the malformed-payload case to keep the agent on track.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
The input schema is minimal—payload is just an object with propertyNames and additionalProperties, so it conveys almost nothing about the actual shape. The description fills that void by explaining the single vs. batch call modes, the batch-specific fields (items, atomic, idempotency_key, concurrency), and the per-operation markdown strings. It also documents the verbose:true parameter and URL flexibility, all of which the schema omits.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description opens with a precise verb and resource: 'Run one Notion write operation by name.' It immediately distinguishes itself from the sibling tools (notion_read, notion_describe) by framing itself as the write dispatcher, and the long operation enum makes the scope unmistakable.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
It gives explicit guidance on when to call notion_describe for complex shapes and warns which operations are destructive, requiring user confirmation. However, it never explicitly contrasts with notion_read for reads, so the when-not-to-use instruction is only implied by the tool name rather than stated.
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.
4 tool updates
v3.0.1- Changed
notion_describe2 fields changed- changed
Input schema / $schemaPrevious value: -"http://json-schema.org/draft-07/schema#"New value: +"https://json-schema.org/draft/2020-12/schema" - changed
Input schema / properties / operation / descriptionPrevious value: -"Operation name to describe."New value: +"Operation name to describe, as listed by notion_read / notion_write."
- Removed
notion_execute - Added
notion_read - Added
notion_write
15 tool updates
v1.0.1- Removed
append_block_children - Removed
archive_page - Removed
batch_append_block_children - Removed
batch_delete_blocks - Removed
batch_mixed_operations - Removed
batch_update_blocks - Removed
create_page - Removed
delete_block - Added
notion_describe - Added
notion_execute - Removed
restore_page - Removed
retrieve_block - Removed
retrieve_block_children - Removed
search_pages - Removed
update_block
13 tool updates
v1.0.0- First observed
append_block_children - First observed
archive_page - First observed
batch_append_block_children - First observed
batch_delete_blocks - First observed
batch_mixed_operations - First observed
batch_update_blocks - First observed
create_page - First observed
delete_block - First observed
restore_page - First observed
retrieve_block - First observed
retrieve_block_children - First observed
search_pages - First observed
update_block
TDQS
Scored across 3 tools
notion_read, notion_describe, and notion_write have clearly separated intents: execute a read, fetch schema guidance, or execute a write. There is no overlap between tool purposes, and the payload-level operations are cleanly scoped by tool.
All tool names follow the same notion_ prefix with a single lowercase verb: read, describe, and write. The naming pattern is uniform, predictable, and easy for an agent to reason about.
Three tools is well-scoped for a dispatcher-style server because each tool covers a broad category of Notion operations. The count is not too thin, and each tool earns its place in the set.
The read/write tools cover core Notion workflows including search, page retrieval, database queries, block children, page creation, block appending, updates, and delete/archive operations. The main gap is that the full set of supported operation names is not explicitly enumerated, relying on describe/errors for discovery.
Maintenance
Related MCP Connectors
An MCP server that integrates with Discord to provide AI-powered features.
- ZapierOAuthcom.zapier
Hosted MCP server connecting AI assistants to 9,000+ apps and 40,000+ actions via Zapier.
MCP server connecting AI agents to 100+ apps (Gmail, Slack, Notion, GitHub) via one-click OAuth.
MCP server for AI dialogue using various LLM models via AceDataCloud
Related MCP Servers
- AlicenseAqualityDmaintenanceA high-performance MCP server that integrates Notion into AI workflows, enabling interaction with Notion pages, databases, and comments through a standardized protocol.841 npm27Apache 2.0

Notion MCP Serverofficial
AlicenseBqualityDmaintenanceAn MCP server that enables AI assistants to interact with the Notion API, allowing them to search, read, comment on, and create content in Notion workspaces through natural language commands.19122,532 npm4,632MIT- -licenseNot gradedqualityNot gradedmaintenanceAn 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.122,532 npm-
- AlicenseAqualityCmaintenanceAn MCP server for Notion API with optimized token efficiency and full database property filtering, enabling AI assistants to manage pages, databases, and blocks.3210 npm1MIT