·

Notion MCP With Claude Code CLI and VS Code

Set up Notion MCP in Claude Code CLI and VS Code so your AI agent can read and write pages and databases right from your editor.

Claude Code has the most complete MCP tooling story of the clients covered in this course — first-class claude mcp subcommands, project-scoped config that's shareable via git, and (as of recent releases) native support for both stdio and remote HTTP/SSE transports. That makes it the natural place to build a real documentation workflow: pull context from a codebase, write structured pages to Notion, and read database state back into the terminal without touching a browser.

This topic walks through wiring the server into both the CLI and the VS Code extension (they share config), then builds out three concrete workflows: doc generation from code, database read/write from the terminal, and prompt patterns that keep the output looking hand-written instead of AI-flattened.


Installing and Connecting Notion MCP to Claude Code

Claude Code reads MCP server definitions from three scopes: user (~/.claude.json, global), project (.mcp.json in the repo root, shareable and git-committable), and local (project-specific but not committed). For a Notion integration you'll almost always want project scope, so the config travels with the repo and teammates get it for free.

claude mcp add notion --scope project \
  -- npx -y @notionhq/notion-mcp-server

export NOTION_TOKEN="ntn_your_internal_integration_secret_here"

This produces a .mcp.json like:

{
  "mcpServers": {
    "notion": {
      "command": "npx",
      "args": ["-y", "@notionhq/notion-mcp-server"],
      "env": {
        "NOTION_TOKEN": "${NOTION_TOKEN}"
      }
    }
  }
}

Using ${NOTION_TOKEN} as a variable reference (rather than a literal string) means the committed file has no secret in it — Claude Code resolves it from the shell environment at launch. Teammates just need the token in their own .env or shell profile.

If you'd rather use Notion's hosted remote server (OAuth, no token to manage):

claude mcp add notion --transport http --scope user \
  https://mcp.notion.com/mcp

This opens a browser-based OAuth consent flow on first use. It's a better fit for individual developer setups; the token-based local server is a better fit for CI or headless agents where there's no browser to complete an OAuth handshake.

VS Code. The Claude Code extension for VS Code reads the same .mcp.json — no separate config needed. Open the repo in VS Code with the extension installed, open the Claude Code panel, and run /mcp to confirm the notion server shows as connected. If you're using GitHub Copilot's native Agent mode instead, VS Code has its own MCP config at .vscode/mcp.json with an equivalent but distinct schema (servers instead of mcpServers) — the two are not interchangeable, so pick one client per project or duplicate the config.

Verify the connection works before building anything on top of it:

claude "List the Notion MCP tools you have access to, then call API-get-self
to confirm which integration is authenticated."

Tips
- Run claude mcp list after adding the server — a server that fails to start (bad token, npx network issue) shows up there before you waste time debugging a "tool not found" error mid-task.
- Keep the token in .env plus a .env.example placeholder, and add .env to .gitignore — the classic mistake is committing .mcp.json with the literal secret inlined instead of a variable reference.
- npx -y @notionhq/notion-mcp-server re-resolves the package on every launch unless you pin a version (@notionhq/notion-mcp-server@1.x) — pin it for CI reproducibility.


Generating Technical Docs and Requirements Pages from Code and Specs

The highest-value everyday use of Notion MCP in Claude Code is turning something that already exists — a PR diff, a set of route handlers, a design doc draft in specs/ — into a properly structured Notion page, without you doing the formatting by hand.

Prompt used against a real feature branch:

"Read the diff between main and this branch. Then create a Notion page titled
'RFC: Async Webhook Delivery' as a child of the page at
https://notion.so/eng-wiki/Architecture-abc123, structured as:
- Problem statement (2-3 sentences)
- Proposed approach
- API changes (as a code block)
- Rollout plan
- Open questions (as a bulleted list)

Match the heading style of the existing 'RFC: Rate Limiting' page in the same
wiki section — pull it first so you can match formatting."

Two things make this reliable: giving the agent a real reference page to match formatting against (Claude Code will call API-retrieve-a-block on it before writing), and specifying block-level structure explicitly rather than leaving it to infer. Without the reference page, output style drifts from doc to doc even within the same session.

For requirements docs generated from a spec file already in the repo:

claude "Read specs/003-webhook-retries/spec.md. Convert it into a Notion page
under the 'Product Specs' database, setting these database properties:
- Title: use the spec's H1
- Status: 'Draft'
- Owner: me (use API-get-self to fill this)
- Target Quarter: 'Q3 2026'

Then append the spec body as page content, converting the markdown tables
into real Notion table blocks, not plain text."

Watch the table conversion specifically — this is where models most often cut corners, emitting a plain-text approximation of a table (pipe characters and all) instead of a proper table block with table_row children. If the output looks like Markdown pasted into Notion rather than native blocks, call it out and ask for a redo with real block types.

Tips
- Always give the agent an existing page to use as a style reference when generating docs for a wiki with established conventions — "figure out the house style yourself" produces inconsistent results across sessions.
- Ask for database property values explicitly rather than letting the model guess a "Status" or "Owner" default — a wrong initial status is an easy miss to make in a busy database.
- Generated docs read noticeably better when you ask for specific block types by name (table, callout, toggle) instead of just describing the shape you want in prose.


Reading and Updating Notion Databases from the Terminal

This is where Notion MCP earns its keep for day-to-day engineering work — querying a sprint board or bug tracker without leaving the terminal, and updating rows as part of a scripted or semi-scripted workflow.

Prompt: "Query the 'Sprint 42' database. Filter to rows where Status is
'In Review' and Assignee includes me. Show me the results as a table with
Title, Priority, and Age (days since created)."

Under the hood this becomes an API-post-database-query call with a filter body like:

{
  "filter": {
    "and": [
      { "property": "Status", "status": { "equals": "In Review" } },
      { "property": "Assignee", "people": { "contains": "user_id_here" } }
    ]
  },
  "sorts": [
    { "property": "Priority", "direction": "descending" }
  ]
}

Claude Code resolves "me" to a user ID via API-get-self first, then plugs it into the people.contains filter — you don't need to hand it the raw ID, but it's worth knowing that's what's happening when a filter unexpectedly returns zero rows (usually because the bot user and your human user are different IDs, and the agent queried for the wrong one).

Updating rows from the terminal, tied to a real event:

claude "The PR for JIRA-4821 just merged. Find the matching row in the
'Engineering Tasks' database (match by the ticket ID in the Title property)
and set Status to 'Done' and Shipped Date to today."

This maps to a two-step tool sequence: API-post-database-query with a title.contains filter to find the row, then API-patch-page with the property update:

{
  "properties": {
    "Status": { "status": { "name": "Done" } },
    "Shipped Date": { "date": { "start": "2026-08-21" } }
  }
}

Note the property shape difference between status (Notion's newer status type, with grouped options) and select (older, flat options) — they look similar in the UI but use different JSON shapes in the API. If a property update silently no-ops, check whether you're sending a select payload against a status property or vice versa.

Tips
- Have the agent read a database's schema (API-retrieve-a-database) before writing filters against it — property names are case- and space-sensitive, and guessing "Assignee" when the real property is "Owner" fails silently with zero results, not an error.
- Batch database updates in groups of 5-10 and review before continuing on anything destructive (status changes affecting visible dashboards) — a bad filter that matches too broadly is easy to write and easy to miss.
- status vs select property types are a recurring source of "why didn't this update" bugs — always confirm the property's actual type before scripting updates against it.


Prompting Patterns for Clean Notion Formatting and Structure

The gap between "technically correct blocks" and "looks like a human wrote it" comes down to a handful of repeatable prompt patterns.

Give explicit block-type instructions, not just content. "Write three bullet points" is ambiguous between a bulleted_list_item and a numbered one. "Use bulleted_list_item blocks for the criteria, and a callout block with a warning icon for the caveat" removes the guesswork.

"Structure the page like this:
- H2 heading blocks for each section
- callout block (⚠️ icon) for the 'Breaking Change' note
- code block (language: typescript) for the interface definition
- toggle block titled 'Migration steps' containing a numbered list"

Ask for a dry-run block plan before writing, on anything non-trivial. For a multi-section page, have the agent list the block sequence it intends to create first, so you can catch a wrong structure before 20 API calls happen:

"Before creating the page, list the block types and headings you plan to use,
in order. Wait for my confirmation before calling any write tools."

Anchor tone with a real example, every time. Models default to a generically "helpful AI" register — hedged, over-explained, littered with "It's important to note that." Notion docs written by senior engineers tend to be terser and more opinionated. Pasting one paragraph of an existing well-written page into the prompt as a tone anchor fixes this more reliably than describing the tone in the abstract ("be concise" barely moves the needle; a concrete example does).

Separate structure requests from content requests. Asking for both simultaneously ("write a well-formatted RFC about X") tends to produce serviceable content in mediocre structure. Splitting it — first agree on the outline/block plan, then fill in content per section — consistently produces cleaner pages, at the cost of one extra round trip.

Tips
- Naming exact Notion block types in the prompt (callout, toggle, table) produces far more reliable structure than describing the visual layout in prose.
- For anything going into a shared wiki, paste a short excerpt of existing house-style content into the prompt — it's the single highest-leverage thing you can do for tone consistency.
- On complex multi-block pages, ask for a block plan first and confirm it before the agent starts writing — cheap insurance against a 15-block page you have to manually clean up.


Tips

Claude Code's project-scoped .mcp.json plus its native claude mcp tooling make Notion the easiest of the four clients in this module to set up well — the main risk is secret handling (committing a real token) and property-type mismatches when writing to databases with mixed select/status fields.

Tips
- Commit .mcp.json with variable references only, never literal tokens — treat it exactly like you'd treat a docker-compose.yml with a database password.
- Prefer the OAuth-based hosted server for individual, interactive use; keep the token-based local server for anything scripted, scheduled, or CI-driven.
- Build a small library of "known good" prompts for your team's common doc types (RFC, postmortem, onboarding page) — the structural consistency compounds across a wiki much faster than iterating prompts fresh each time.