Notion has quietly become the default source of truth for a huge share of engineering orgs — PRDs, RFCs, sprint boards, onboarding wikis, incident postmortems, API references. The problem was never that Notion lacked structure; it was that the structure lived behind a REST API most developers never touched directly. Model Context Protocol changes that math. Notion MCP puts a thin, tool-shaped layer over Notion's existing API so an agent like Claude Code, Cursor, or Gemini CLI can read a spec, walk a database, and write updates back without you copy-pasting text into a browser tab all day.
This topic covers what the server actually exposes, how the auth model differs from a typical API key setup, what realistic automation looks like, and — the part most tutorials skip — how Notion's block-based content model constrains and shapes what an AI can safely write.
Core Notion MCP Tools: Pages, Databases, Blocks, and Search
There are two Notion MCP servers you'll run into in practice, and conflating them causes half the confusion developers hit in week one.
@notionhq/notion-mcp-server— the open-source, self-hosted server Notion maintains on GitHub (makenotion/notion-mcp-server). It's a fairly direct wrapper around the Notion REST API: tool names mirror API operation IDs, so you get things likeAPI-post-search,API-retrieve-a-page,API-patch-page,API-post-page,API-retrieve-a-database,API-post-database-query,API-patch-block-children,API-retrieve-a-block,API-create-a-comment, andAPI-get-users. You run it locally vianpxor Docker, and it talks toapi.notion.comusing your own integration token.- The hosted remote server at
https://mcp.notion.com/mcp— Notion's own managed MCP endpoint, authenticated via OAuth instead of a static token. It exposes a smaller, higher-level tool set:search,fetch,notion-create-pages,notion-update-page,notion-move-pages,notion-duplicate-page,notion-create-database,notion-update-database,notion-create-comment,notion-get-comments,notion-get-users, andnotion-get-self. It also does something the raw API doesn't: it converts blocks to and from Markdown automatically, so the model reads and writes Markdown instead of raw block JSON.
For this course we'll set up and use the self-hosted server as the primary path (it's what "integration token" auth refers to, and it's what most CI/headless setups use), and call out the hosted variant where the trade-off actually matters — mainly in the Cursor and OpenCode topics.
Functionally, both servers cover four capability groups:
- Pages — retrieve a page's properties and metadata, create new pages (as children of a page or a database), update page properties (title, select values, dates, checkboxes), archive/restore pages.
- Databases — retrieve a database's schema (its property definitions), query rows with filters and sorts, create new databases, add or modify properties on an existing database.
- Blocks — read a block and its children (the actual page body: paragraphs, headings, lists, code blocks, tables, callouts), append new blocks, update or delete existing blocks.
- Search — full-text search across every page and database the integration has access to, optionally filtered by object type (
pagevsdatabase).
Comments are a fifth, smaller surface (API-create-a-comment, API-retrieve-a-comment) that's easy to forget but genuinely useful for agent-to-human handoff — more on that in the real-world workflow topic.
// Example: what the tool list looks like once notion-mcp-server is connected
// (abbreviated; actual list has ~19 tools mapped 1:1 to Notion API operations)
[
"API-post-search",
"API-retrieve-a-page",
"API-patch-page",
"API-post-page",
"API-retrieve-a-database",
"API-post-database-query",
"API-create-a-database",
"API-update-a-database",
"API-retrieve-a-block",
"API-patch-block-children",
"API-delete-a-block",
"API-create-a-comment",
"API-retrieve-a-comment",
"API-get-users",
"API-get-self"
]
Tips
- If your client shows a huge, unfiltered tool list and the model keeps picking the wrong one, restrict the server to a tool subset —notion-mcp-serversupports an--allowed-toolsflag for exactly this.
-API-post-searchis a lexical search, not semantic — it matches title and content substrings. Don't expect it to find a page by paraphrased meaning alone.
- Treat the hostedmcp.notion.comtool names (notion-create-pages, etc.) and the self-hostedAPI-*names as two different contracts. Prompts and few-shot examples written for one won't transfer cleanly to the other.
Notion MCP Authentication: Integration Token and Page Sharing Permissions
This is the step that trips up almost everyone the first time, because it has two parts and most people only do one of them.
Part 1 — create the integration and get a token.
- Go to
https://www.notion.so/my-integrationswhile logged into the target workspace. - Click + New integration, pick the workspace, give it a name that reflects its purpose (
MCP Agent - Docs Bot, nottest), and select capabilities: at minimum Read content, plus Update content and Insert content if the agent will write. Add Read comments / Insert comments if you want the comment tools to work. - Submit, then copy the Internal Integration Secret. Newer integrations issue tokens prefixed
ntn_; integrations created before late 2024 may still show the oldersecret_prefix — both work identically against the API.
Part 2 — share pages with the integration. This is the part people forget. Creating an integration grants it zero access to anything by default. You have to explicitly connect it to each page or database (connections cascade to child pages):
- Open the top-level page or database in Notion.
- Click the "···" menu → Connections → search for and select your integration.
- Confirm. Any pages nested under that page inherit the connection.
Skip this and every tool call returns a clean 404 — not a 403 — because the Notion API deliberately doesn't reveal that a page exists to an integration that hasn't been shared with it. If you're debugging "the agent says the page doesn't exist" and you're sure the ID is right, this is almost always the cause.
export NOTION_TOKEN="ntn_your_internal_integration_secret_here"
export OPENAPI_MCP_HEADERS='{"Authorization": "Bearer ntn_xxx", "Notion-Version": "2022-06-28"}'
The Notion-Version header matters more than it looks. Notion's API is versioned by date string, not semver, and 2022-06-28 has been the stable version for years — but if you ever see a payload shape in Notion's changelog that doesn't match what your tool calls return, check this header first.
Tips
- Rotate integration tokens the same way you'd rotate any API secret — Notion has no built-in expiry, so a leaked token stays valid indefinitely until you revoke it from the integration settings page.
- Give each project or team its own integration rather than reusing one company-wide token. It makes the "Connections" list on each page a legible audit trail of what has access to what.
- The Notion API rate limit is roughly 3 requests/second average per integration with short bursts allowed. A "generate 40 pages" batch job will hit429s — build in retry/backoff, or ask the agent to paginate work into smaller batches.
What AI Can Automate: Doc Generation, Database Updates, and Summaries
The realistic automation wins fall into three buckets, roughly in order of how reliable they are today.
1. Doc generation from code and conversation. Point Claude Code at a diff, a set of API route handlers, or a Slack thread pasted into context, and ask it to produce a structured Notion page — a technical spec, an RFC, an onboarding doc. This works well because the model is generating content it's good at generating (structured prose and code blocks) and just needs to format it as Notion blocks on the way out.
2. Database updates driven by external signal. Update a "Status" property from In Progress to Done when a PR merges, log a new row in a "Bugs" database when an agent triages an error report, flip a checklist item after a deploy. This is the most production-ready category because it's narrow, idempotent, and easy to verify — you're changing one property on one known row, not generating open-ended prose.
3. Summaries and rollups across many pages. "Summarize every page in the Q3 Planning database that's still Not Started" or "pull open action items from the last 5 retro pages." This is genuinely useful but the least deterministic — it depends on search recall, and Notion's search is lexical, so pages using different terminology for the same concept can get missed.
Example prompt used against Claude Code with Notion MCP connected:
"Search the Engineering wiki for pages tagged 'incident-postmortem' from the last
30 days. For each one, extract the root cause and the follow-up action items,
then create a new page called 'Q3 Incident Rollup' summarizing all of them in a
table with columns: Incident, Root Cause, Owner, Status."
What this produces in practice: a API-post-search call filtered to pages, then 5-10 API-retrieve-a-block calls to pull body content, then one API-post-page call with a table block. It's slow — expect 20-40 seconds of tool-call round trips for a rollup like this — but it replaces what would otherwise be 45 minutes of manual page-hopping.
Tips
- Doc generation quality is directly proportional to how much structure you give the model up front — "write a spec" produces mush; "write a spec with these five headings, matching the format of [linked existing page]" produces something usable.
- For database updates, always have the agent read the current row before writing — a blindAPI-patch-pagerisks clobbering a property another teammate just changed.
- Summarization tasks degrade badly past ~15-20 source pages in one prompt. Batch large rollups into smaller passes instead of one giant search-and-summarize call.
Understanding Notion's Block Model and Its Impact on AI Edits
Every piece of content in Notion — a paragraph, a heading, a to-do, a code snippet, an entire table — is a block, and every block is its own object with an id, a type, and a type-specific payload. A page isn't a blob of text; it's an ordered tree of block objects. This is the single most important thing to understand before you let an agent write to Notion, because it explains both what's easy and what's fragile.
// A paragraph block
{
"object": "block",
"type": "paragraph",
"paragraph": {
"rich_text": [
{ "type": "text", "text": { "content": "MCP servers expose tools over JSON-RPC." } }
]
}
}
// A heading and a to-do block, as they'd appear in a page's children array
{
"object": "block",
"type": "heading_2",
"heading_2": {
"rich_text": [{ "type": "text", "text": { "content": "Acceptance Criteria" } }]
}
}
{
"object": "block",
"type": "to_do",
"to_do": {
"rich_text": [{ "type": "text", "text": { "content": "Support pagination on the search endpoint" } }],
"checked": false
}
}
Two consequences follow directly from this model, and both matter for how you prompt an agent:
Appending is cheap and safe; rewriting is not. API-patch-block-children appends new blocks to the end of a page or under a parent block — it's additive by design. There's no single "replace this page's body" call. To truly rewrite a section, the agent has to enumerate existing child block IDs, delete the ones it wants gone, and append replacements — three round trips minimum, and a bug in step one (deleting the wrong range) is hard to undo since Notion's trash restore via API is clunky. Most well-behaved agent workflows lean toward appending new sections (a "Update — Aug 2026" heading followed by new content) rather than editing in place, precisely because it's safer.
Rich text formatting lives inside each block, not as page-level markup. Bold, italics, links, and inline code are annotations on spans of rich_text, not something wrapping the whole block. When an agent generates a Markdown-formatted string and hands it to a tool that expects Notion's rich text array, something in the pipeline has to do that translation — the hosted mcp.notion.com server does this conversion for you; the raw notion-mcp-server does not, so you either write blocks natively in your prompt guidance or lean on the model's ability to produce correctly-shaped JSON directly (which it does reasonably well, but not perfectly — nested formatting like a bolded link inside a bulleted item is where it slips).
Database rows are pages too — a database "row" is a page object whose parent is the database and whose properties match the database's schema. That's why the same API-patch-page tool updates both a wiki page's title and a task's "Status" field.
Tips
- Ask the agent to append rather than overwrite whenever you're not 100% sure what's already on the page — it's the difference between a messy-but-recoverable page and permanently gone content.
- When a page looks "broken" after an AI edit (missing formatting, flattened lists), the cause is almost always a block-type mismatch — the model emitted aparagraphwhere abulleted_list_itemwas needed. Ask it to re-check block types against the source content's structure.
- Nested/child blocks (a toggle containing a bulleted list) require a follow-up call to attach children under the newly created block's ID — a singleAPI-patch-block-childrencall can't create arbitrarily deep nesting in one shot.
Tips
Notion MCP is genuinely one of the more mature MCP integrations available today because it maps to a REST API that's been stable for years — but it inherits every quirk of that API, including the block model's append-first bias and the page-sharing gotcha that silently returns 404s instead of 403s.
Tips
- Start every new integration by sharing exactly one test page with it, and confirm a simpleAPI-retrieve-a-pagecall works before wiring up anything that writes.
- Default to the self-hostednotion-mcp-serverfor anything scripted or CI-driven (static token, predictable tool names); reach for the hostedmcp.notion.comOAuth server when a human is in the loop and Markdown-in/Markdown-out matters more than raw control.
- Keep a "scratch" Notion page or workspace for testing new prompts before pointing an agent at anything that matters — block-level mistakes are recoverable but annoying to clean up by hand.