Skip to main content
Glama

notion-mcp

An MCP server for the Notion API: pages, databases (2025-09+ data-source model handled transparently), native markdown read/write, comments, users, and a full file pipeline — upload/replace/import/download with positional inserts, page icons/covers, multi-part upload for large files, and in-place PDF reading with optional OCR. Built on the Python MCP SDK (FastMCP); runs as a local stdio server or a containerized Streamable HTTP service with bearer auth. 44 tools.

Unofficial project, not affiliated with or endorsed by Notion.

Table of Contents

Related MCP server: Markdown-To-Notion

Quick Start

# Prerequisites: Python 3.12+, uv
uv sync

# Set your Notion integration token
export NOTION_TOKEN=ntn_...

# Run the server
uv run notion-mcp
# Server starts on http://0.0.0.0:8322/mcp

Create a Notion internal integration and share the pages/databases you want to access with it.


Tool Reference

Search & Discovery

notion_search_pages

Search for pages the integration can access.

Parameter

Type

Required

Default

Description

query

string

yes

Text to search for

limit

int

no

10

Max results (1-100)

in_trash

bool

no

false

Search trashed pages instead of active ones (2026-07 API addition; restore hits with notion_restore_page)

Returns page titles, IDs, and URLs. Use the page ID with any tool that accepts page_id.

Notion API: POST /search with filter.value = "page"

notion_search_databases

Search for databases the integration can access. Returns schema summaries.

Parameter

Type

Required

Default

Description

query

string

yes

Text to search for

limit

int

no

10

Max results

Returns database_id, data_source_id, property names and types. Use the database_id with database tools.

Notion API: POST /search with filter.value = "data_source" (changed from "database" in API 2025-09-03)

notion_list_files_on_page

List all file/image/pdf/video/audio blocks on a page.

Parameter

Type

Required

Description

page_id

string

yes

The page to scan

Returns block IDs, filenames, block types, and source types. Use the block_id with notion_download_file.

Notion API: GET /blocks/{id}/children, filtered to file-type blocks

notion_list_local_files

List files in either the server's local file store (/data/files inside the container) or the cross-MCP shared mount (/shared).

Parameter

Type

Required

Default

Description

filter

string

no

""

Substring filter on filename

location

string

no

"files"

"files" (the notion-mcp private store, FILES_DIR) or "shared" (the cross-MCP mount, SHARED_DIR, populated by google-accounts-mcp download_attachment(destination='shared'))

notion_purge_shared_files

Delete files from the cross-MCP shared mount (SHARED_DIR, /shared in the container) whose modification time is older than max_age_hours. Pair with a host-side cron/timer TTL sweep if you want automatic cleanup; this tool is mainly for agents that want to clean up proactively at the end of a workflow.

Parameter

Type

Required

Default

Description

max_age_hours

float

no

24.0

Minimum file age before deletion. Must be ≥ 0.

dry_run

bool

no

False

List victims without deleting

Regular files only — subdirectories are left alone on principle.


Reading Pages

There are two ways to read page content, optimized for different use cases.

notion_read_page

Read page content by fetching blocks and rendering them as markdown. Best for structured reading with optional block-level targeting.

Parameter

Type

Required

Default

Description

page_id

string

yes

The page to read

max_depth

int

no

3

Recursion depth for nested blocks (1-5)

include_block_ids

bool

no

false

Suffix each line with <!-- block:UUID -->

When include_block_ids is true, each rendered line includes the block's UUID in an HTML comment. Use these IDs with notion_update_block or notion_delete_block for surgical edits.

Notion API: GET /blocks/{id}/children (recursive). Blocks are converted to markdown by blocks.py.

notion_read_page_markdown

Read page content using Notion's native markdown API. Faster (single API call) and returns Notion-flavored markdown including special tags for embedded databases, files, and page links.

Parameter

Type

Required

Description

page_id

string

yes

The page to read

The output includes Notion-specific tags like <database url="...">, <file src="...">, and <page url="..."> that aren't standard markdown but are useful for understanding page structure.

Notion API: GET /pages/{id}/markdown (API 2025-09-03+)

When to use which:

  • notion_read_page — when you need block IDs for editing, or want clean standard markdown

  • notion_read_page_markdown — when you need speed, or want to see Notion-native structure (databases, synced blocks, etc.)


Creating & Writing Pages

notion_create_page

Create a new page with optional markdown body content.

Parameter

Type

Required

Default

Description

title

string

yes

Page title

content

string

no

""

Markdown body content

parent_page_id

string

no

""

Create as child of this page

database_id

string

no

""

Create as row in this database

properties_json

string

no

""

JSON object of Notion property values

icon

string

no

""

Emoji for page icon (e.g. "🚀")

Provide either parent_page_id (standalone page) or database_id (database row). The title property name is auto-detected from the database schema.

For database rows, pass additional properties via properties_json using Notion property value format:

{
  "Status": {"select": {"name": "In Progress"}},
  "Priority": {"number": 5},
  "Due": {"date": {"start": "2026-03-15"}}
}

The content parameter accepts markdown which is parsed into Notion blocks (see Markdown Format).

Notion API: POST /pages with children blocks

notion_append_content

Append markdown content to the end of an existing page.

Parameter

Type

Required

Description

page_id

string

yes

The page to append to

content

string

yes

Markdown text to append

Content is parsed into blocks and appended. Automatically batches in groups of 100 if the content produces more than 100 blocks (Notion API limit).

Notion API: PATCH /blocks/{id}/children


Editing Page Content

notion_update_page

Update a page's title, icon, or database properties. Does not modify page body content.

Parameter

Type

Required

Default

Description

page_id

string

yes

The page to update

title

string

no

""

New title

properties_json

string

no

""

JSON of properties to update

icon

string

no

""

Emoji, or "remove" to clear

For database row pages, properties follow the Notion property value format. Omitted properties are left unchanged.

Notion API: PATCH /pages/{id}

notion_update_page_content

Edit page body content using search-and-replace on the page's markdown. Use notion_read_page_markdown first to see the exact content, then provide the text to find and replace.

Parameter

Type

Required

Description

page_id

string

yes

The page to edit

old_text

string

yes

Exact text to find

new_text

string

yes

Replacement text (empty string to delete)

Notion API: PATCH /pages/{id}/markdown with type: "replace_content_range" (API 2025-09-03+)

notion_replace_page_content

Replace a page's entire body content with new markdown. Destructive — all existing content is overwritten.

Parameter

Type

Required

Description

page_id

string

yes

The page to overwrite

markdown

string

yes

New page content

Notion API: PATCH /pages/{id}/markdown with type: "replace_content" (API 2025-09-03+)


Page Lifecycle

notion_archive_page

Archive (soft-delete) a page. It moves to the Notion trash and can be restored within 30 days.

Parameter

Type

Required

Description

page_id

string

yes

The page to archive

Notion API: PATCH /pages/{id} with {"archived": true}

notion_restore_page

Restore an archived page from the Notion trash.

Parameter

Type

Required

Description

page_id

string

yes

The archived page to restore

Notion API: PATCH /pages/{id} with {"archived": false}

notion_move_page

Move a page to a new parent page or into a database.

Parameter

Type

Required

Default

Description

page_id

string

yes

The page to move

new_parent_page_id

string

no

""

Move under this page

new_parent_database_id

string

no

""

Move into this database

Provide exactly one of the parent parameters. When moving into a database, the server automatically resolves the data_source_id.

Notion API: POST /pages/{id}/move (API 2025-09-03+)


Block Operations

Use notion_read_page with include_block_ids=true to get block IDs for these tools.

notion_update_block

Edit an existing block's text content in place.

Parameter

Type

Required

Default

Description

block_id

string

yes

The block to edit

content

string

yes

New text (supports inline markdown)

block_type

string

no

auto-detect

Block type hint

If block_type is omitted, the server fetches the block to detect its type. Supported types: paragraph, heading_1, heading_2, heading_3, bulleted_list_item, numbered_list_item, to_do, quote, callout, toggle, code.

Notion API: PATCH /blocks/{id}

notion_delete_block

Permanently delete a block and all its children.

Parameter

Type

Required

Description

block_id

string

yes

The block to delete

Notion API: DELETE /blocks/{id}


Database Schema

notion_describe_database

Get a database's full property schema (column names and types).

Parameter

Type

Required

Description

database_id

string

yes

The database to describe

Returns the database_id, data_source_id, and all properties with their types. Use this to discover exact property names (case-sensitive) before querying or creating rows.

Notion API: GET /databases/{id} + GET /data_sources/{id}

notion_create_database

Create a new inline database under a page.

Parameter

Type

Required

Description

parent_page_id

string

yes

Page to create the database under

title

string

yes

Database title

schema_json

string

yes

JSON property schema

The schema_json maps property names to Notion property schema objects. A title property is required:

{
  "Name": {"title": {}},
  "Status": {"select": {"options": [
    {"name": "To Do", "color": "red"},
    {"name": "In Progress", "color": "yellow"},
    {"name": "Done", "color": "green"}
  ]}},
  "Due Date": {"date": {}},
  "Priority": {"number": {"format": "number"}},
  "Tags": {"multi_select": {"options": [
    {"name": "bug", "color": "red"},
    {"name": "feature", "color": "blue"}
  ]}},
  "Assignee": {"people": {}},
  "Done": {"checkbox": {}},
  "Notes": {"rich_text": {}},
  "Link": {"url": {}}
}

Notion API: POST /databases

notion_update_database

Modify a database's title, description, or property schema.

Parameter

Type

Required

Default

Description

database_id

string

yes

The database to update

title

string

no

""

New title

description

string

no

""

New description

properties_json

string

no

""

JSON of property changes

Property changes use Notion's property schema format:

// Add a column
{"New Column": {"rich_text": {}}}

// Remove a column
{"Old Column": null}

// Rename a column
{"Old Column": {"name": "New Name"}}

// Add select options
{"Status": {"select": {"options": [{"name": "Blocked", "color": "red"}]}}}

Multiple changes can be combined in one call.

Notion API: PATCH /data_sources/{id} (routed through data source in API 2025-09-03)


Database Queries

notion_query_database

Query database rows with optional filter and sort.

Parameter

Type

Required

Default

Description

database_id

string

yes

The database to query

filter_json

string

no

""

Notion filter object as JSON

sorts_json

string

no

""

Notion sort array as JSON

limit

int

no

50

Page size (1-100 — Notion's hard cap)

start_cursor

string

no

""

Opaque cursor from a previous call. Pass it back to fetch the next page.

properties

string

no

""

Comma-separated property names to include in the output (e.g. "Gmail Message ID, Invoice, Status"). The title column is always shown. Unknown names are ignored silently. Dramatically slims output when you only need a few columns.

Filter examples:

// Simple property filter
{"property": "Status", "select": {"equals": "Done"}}

// Number comparison
{"property": "Priority", "number": {"greater_than": 3}}

// Date filter
{"property": "Due", "date": {"before": "2026-04-01"}}

// Checkbox
{"property": "Done", "checkbox": {"equals": true}}

// Text contains
{"property": "Name", "title": {"contains": "meeting"}}

// Compound filter (AND)
{"and": [
  {"property": "Status", "select": {"equals": "Active"}},
  {"property": "Priority", "number": {"greater_than": 3}}
]}

// Compound filter (OR)
{"or": [
  {"property": "Status", "select": {"equals": "To Do"}},
  {"property": "Status", "select": {"equals": "In Progress"}}
]}

Sort examples:

// Single sort
[{"property": "Due", "direction": "ascending"}]

// Multiple sorts
[
  {"property": "Priority", "direction": "descending"},
  {"property": "Name", "direction": "ascending"}
]

// Sort by timestamp
[{"timestamp": "last_edited_time", "direction": "descending"}]

Results are returned one page at a time. Each response ends with a has_more: <bool> next_cursor: <token-or-(end)> line; when has_more is true, pass the next_cursor value back as start_cursor on the next call to walk the next page. This gives the LLM bounded- context pagination — walk a 10 000-row database 100 rows at a time without ever materialising the whole thing in the tool output.

When properties is set, only the listed columns are included in the per-row output (the title column is always shown). This is the fix for "the query returns the full schema for every row and blows the context budget" — set properties="Gmail Message ID,Invoice" and the response shrinks to just those columns per row.

Notion API: POST /data_sources/{id}/query (API 2025-09-03+; the server resolves database_id to data_source_id automatically)

notion_get_property

Retrieve a single property value with full pagination. Useful for large relation, rollup, or rich_text properties that get truncated in normal page responses.

Parameter

Type

Required

Default

Description

page_id

string

yes

The page (row) to read from

property_name

string

no

""

Human-readable property name

property_id

string

no

""

Notion property ID (takes precedence)

Notion API: GET /pages/{id}/properties/{property_id}


Database Rows & Files

notion_upload_file

Upload a file and attach it to a page as a new block.

Parameter

Type

Required

Default

Description

source

string

yes

File source (see below)

parent_page_id

string

yes

Page — or any block that supports children (toggle, column, callout, ...) — to attach the file to

filename

string

no

""

Override the source filename

caption

string

no

""

Caption shown below the file in Notion

position

string

no

"end"

Where the new block lands: "end", "start", or a block ID to insert directly after that block

The source parameter accepts four schemes:

  • local:<path> — file from the server's local store (/data/files)

  • shared:<filename> — file from the cross-MCP shared mount (/shared). Populated by google-accounts-mcp download_attachment(destination='shared'). Preferred path for gmail → notion attachment handoffs; no base64, no size limit.

  • drive:<name-or-id> — file from the shared Google Drive folder

  • base64:<data> — raw base64-encoded content (requires filename). Subject to MCP parameter size limits; prefer shared: or drive: for anything over ~20 KB.

The block type (image, pdf, video, audio, file) is auto-detected from the MIME type. Files over 20 MB are uploaded via Notion's multi-part protocol automatically (10 MiB parts + complete). The response includes the new block's ID and the sha256 of the uploaded bytes.

Notion API: POST /file_uploads (single-part or multi-part) + PATCH /blocks/{id}/children (with position)

notion_replace_file

Replace the file inside an existing file/image/pdf/video/audio block, in place — the block keeps its position on the page. Use this to update a document without re-arranging anything.

Parameter

Type

Required

Default

Description

block_id

string

yes

The file block to update (from notion_list_files_on_page)

source

string

yes

Same schemes as notion_upload_file

filename

string

no

""

Override the source filename

caption

string

no

""

Non-empty replaces the caption; empty leaves it untouched

The new file must map to the same block type as the existing block (you cannot replace an image block's content with a PDF — delete and re-upload instead).

Notion API: PATCH /blocks/{id} with a file_upload reference

notion_import_file_from_url

Import a file into Notion directly from a public HTTPS URL — the bytes go Notion-side, never through this server or MCP parameters. The import is asynchronous; the tool polls until done or wait_seconds elapses.

Parameter

Type

Required

Default

Description

url

string

yes

Public HTTPS URL

parent_page_id

string

yes

Page (or child-bearing block) to attach to

filename

string

yes

Must carry an extension Notion accepts; determines block type

caption

string

no

""

Caption

position

string

no

"end"

As in notion_upload_file

wait_seconds

int

no

60

Max time to poll for the async import

Size limits are plan-dependent (5 MiB free / 5 GiB paid workspaces).

Notion API: POST /file_uploads with mode=external_url + GET /file_uploads/{id} polling

notion_set_page_visual

Set or remove a page's icon or cover image.

Parameter

Type

Required

Default

Description

page_id

string

yes

Target page

target

string

yes

"icon" or "cover"

source

string

no

""

An image via the usual schemes

emoji

string

no

""

Single emoji character (icon only)

filename

string

no

""

Override for source

remove

bool

no

false

Clear the icon/cover entirely

Provide exactly one of source, emoji, or remove=True. Covers/icons from source must be images.

Notion API: PATCH /pages/{id} with icon / cover

notion_upload_file_to_database

Create a new database row with a file attached.

Parameter

Type

Required

Default

Description

database_id

string

yes

Target database

source

string

yes

File source (same schemes as above)

files_property

string

yes

Exact name of the files column (case-sensitive)

title

string

yes

Title for the new row

title_property

string

no

auto-detect

Override the title column name

filename

string

no

""

Override the source filename

Use notion_describe_database first to find the exact files_property name.

Notion API: POST /file_uploads + POST /pages

notion_add_file_to_row

Add a file to an existing database row's files column.

Parameter

Type

Required

Default

Description

page_id

string

yes

The row (page) ID

source

string

yes

File source

files_property

string

yes

Exact name of the files column

mode

string

no

"append"

"append" or "replace"

filename

string

no

""

Override the source filename

Notion API: POST /file_uploads + PATCH /pages/{id}

notion_batch_add_file_to_row

Attach files to many existing rows in one call.

Parameter

Type

Required

Default

Description

items_json

string

yes

JSON array of item objects (see below)

default_files_property

string

no

""

Default files_property for items that omit it

default_mode

string

no

"append"

Default mode for items that omit it

Each item in items_json takes the same fields as the single-shot tool: page_id, source, optional filename, optional files_property, optional mode. Defaults fill in any field left off at the item level.

[
  {"page_id": "p1", "source": "shared:m1_invoice.pdf"},
  {"page_id": "p2", "source": "shared:m2_invoice.pdf", "filename": "Renamed.pdf"},
  {"page_id": "p3", "source": "drive:<file_id>",
   "files_property": "Contracts", "mode": "replace"}
]

Items are processed with bounded concurrency — Notion's file upload API is not pipelined and rejects rapid parallel stage-2 calls. Per-item failures are reported in the output summary ([i] FAIL page=... : ...) but do not abort the batch, so you can retry individual failing items without re-running the whole set.

Typical pattern: stage N attachments via google-accounts-mcp/download_attachment(destination='shared', prefix=f"{msg}_"), call this tool once, then call notion_purge_file or notion_purge_shared_files to free the shared slots.

Notion API: same as notion_add_file_to_row, called N times under the hood.

notion_purge_file

Delete a single file from the cross-MCP shared mount by bare filename.

Parameter

Type

Required

Description

filename

string

yes

Bare filename (no path components)

Use after a successful upload to free the shared slot instead of waiting for the daily TTL sweep (or calling notion_purge_shared_files with max_age_hours=0, which would wipe any unrelated in-flight handoffs). Rejects any filename containing path separators.

File Downloads

notion_download_file

Download a file from a Notion file/image/pdf/video/audio block.

Parameter

Type

Required

Default

Description

block_id

string

yes

File block ID (from notion_list_files_on_page)

destination

string

no

"local"

"local", "shared", "drive", or "base64"

filename

string

no

""

Override the inferred filename (bare filename only for shared)

Destinations:

  • local — saves to the server's local file store

  • shared — saves to the cross-MCP mount (/shared), immediately attachable by google-accounts-mcp as attachments=['shared:<name>'] on draft_email/send_email. Collisions auto-rename atomically; 24h TTL.

  • drive — uploads to the shared Google Drive folder

  • base64 — returns the file content as base64 in the response

Every destination reports the sha256 of the downloaded bytes for end-to-end integrity verification.

Notion API: GET /blocks/{id} to get the signed S3 URL, then direct download


PDF Reading & OCR

Read PDFs in place — no download hop needed. Both tools accept any file source: notion:<block_id> (a file/pdf block on a page), shared:<name>, local:<path>, or drive:<name-or-id>. Semantics are identical to google-accounts-mcp's PDF tools (the pdf_read module is duplicated verbatim across the two repos — edit both copies together).

notion_extract_file_text

Extract text from a PDF, with OCR for scanned/image-only pages.

Parameter

Type

Required

Default

Description

source

string

yes

notion:<block_id>, shared:<name>, local:<path>, or drive:<name-or-id>

pages

string

no

all

pdftotext-style spec: "3", "1-5", "1,3,5", "1-3,7"

mode

string

no

"text"

text (flowing), layout (preserve columns), tables (markdown tables). Native extraction only

ocr

string

no

"auto"

auto — OCR pages with < 20 chars of native text; off; force — OCR every page; llm — vision model. OCR is delegated to an xberg server (XBERG_BASE_URL, default http://xberg:8000; classical backend tesseract; llm = xberg vlm backend → an OpenAI-compatible vision endpoint, needs XBERG_VLM_API_KEY)

Returns JSON {text, page_count, pages_returned, mode, truncated, filename, source} plus ocr_used/ocr_pages/ocr_engine when OCR ran (and ocr_error if auto-mode OCR was needed but the local stack failed — native text still returns). Response ceiling 200 KB, truncated on a page boundary with a marker naming the pages to fetch next.

notion_render_file_page

Render one PDF page as a viewable MCP image content block — the calling model sees the page directly (scans, charts, stamps — often no OCR needed).

Parameter

Type

Required

Default

Description

source

string

yes

Same schemes as notion_extract_file_text

page

int

no

1

1-indexed page number

max_width

int

no

1200

Target pixel width, 200..4000

format

string

no

"jpeg"

jpeg or png

quality

int

no

75

JPEG quality 1..100

return_base64

bool

no

false

true returns JSON {image_base64, mime_type, width, height, page, page_count, filename} instead of an image block

OCR is delegated to an xberg server — no OCR wheels or OS libs needed here. See google-accounts-mcp's README § PDF OCR for the full tier/env table (shared pdf_read module).


Comments

notion_get_comments

List all comments on a page.

Parameter

Type

Required

Description

page_id

string

yes

The page to read comments from

Returns comment text, author name, timestamp, and discussion_id for threading.

Notion API: GET /comments with block_id parameter

notion_add_comment

Add a comment to a page, or reply to an existing discussion thread.

Parameter

Type

Required

Default

Description

page_id

string

yes

The page to comment on

text

string

yes

Comment text (supports inline markdown)

discussion_id

string

no

""

Reply to this thread (from notion_get_comments)

text is sent as native API markdown (2026-04 addition): inline formatting, inline equations, and @mentions all work.

Notion API: POST /comments

notion_update_comment

Edit a comment this integration created (the API returns 404 for comments created by anyone else).

Parameter

Type

Required

Description

comment_id

string

yes

From notion_get_comments / notion_add_comment

text

string

yes

Replacement text (native API markdown)

Notion API: PATCH /comments/{id}

notion_delete_comment

Delete a comment this integration created (404 for anyone else's).

Parameter

Type

Required

Description

comment_id

string

yes

From notion_get_comments / notion_add_comment

Notion API: DELETE /comments/{id}


Views

The Views API (2026-03) exposes the saved views you see as tabs on a database — each carries its own filter, sorts, and layout.

notion_list_views

List a database's views: name, type, id, and whether each carries a saved filter/sorts. (The API's list endpoint returns identity-only objects, so each view is hydrated with an extra retrieve.)

Parameter

Type

Required

Description

database_id

string

yes

The database whose views to list

notion_query_view

Run a view's saved filter and sorts — no filter JSON needed. Use this instead of notion_query_database when a view already encodes the question ("Open bugs", "This week").

Parameter

Type

Required

Default

Description

view_id

string

yes

From notion_list_views

limit

int

no

20

Max rows (≤ 100)

Implementation note: the API's view queries are cached objects with a ~15-minute TTL; the client creates one, reads a page, and best-effort deletes it.

notion_create_view / notion_update_view / notion_delete_view

Parameter

Type

Required

Default

Description

database_id

string

create

Parent database

view_id

string

update/delete

Target view

name

string

create

"" (update)

Display name

view_type

string

no

"table"

table / board / list / calendar / timeline / gallery / chart

filter_json

string

no

""

Notion filter object (same shape as notion_query_database)

sorts_json

string

no

""

JSON array of sort objects

Gotcha (verified live): view creation requires both database_id and data_source_id — the client resolves the data source through the same cache the query path uses. Deleting a view never touches rows.


Users

notion_list_users

List all workspace users (people and bots).

Returns names, IDs, emails, and types. User IDs are needed for people properties and @mentions in comments.

Notion API: GET /users (paginated)

notion_get_user

Get details for a specific user.

Parameter

Type

Required

Description

user_id

string

yes

Notion user UUID

Returns name, email, avatar URL, type (person/bot), and owner info for bots.

Notion API: GET /users/{id}


Markdown Format

Writing (markdown to blocks)

When you pass content to notion_create_page or notion_append_content, the markdown is parsed into Notion blocks. Supported syntax:

Markdown

Notion Block

# Heading

heading_1

## Heading

heading_2

### Heading

heading_3

#### Heading

heading_4 (2026-03 API addition; deeper levels clamp to it)

Plain text

paragraph

- Item or * Item

bulleted_list_item

1. Item

numbered_list_item

- [x] Done

to_do (checked)

- [ ] Open

to_do (unchecked)

> Quote

quote (consecutive > lines merge)

```python ... ```

code (with language)

--- or *** or ___

divider

Inline formatting within any block:

Markdown

Rendering

**bold**

Bold

*italic*

Italic

***both***

Bold + Italic

`code`

Inline code

~~strike~~

Strikethrough

[text](url)

Link

Reading (blocks to markdown)

notion_read_page renders all common Notion block types:

  • Paragraphs, headings (1-3), bullet/numbered lists, to-do items

  • Quotes, callouts (with emoji icons), toggles

  • Code blocks (with language), dividers, equations

  • Images (![caption](url)), files, bookmarks, embeds

  • Tables (rendered as markdown tables)

  • Child pages and databases (shown as [Page: Title] / [Database: Title])

  • Nested blocks (indented, recursive up to max_depth)


Working with Databases

API 2025-09-03 Data Source Model

In the 2025-09-03 API, database properties and queries operate through data sources rather than directly on databases. This server handles the mapping transparently:

  1. You pass a database_id (from search results or a Notion URL)

  2. The server calls GET /databases/{id} to find the data_source_id

  3. Schema/query/update operations go through /data_sources/{data_source_id}

  4. The mapping is cached per session for performance

You never need to know or pass data source IDs directly.

Typical Database Workflow

1. notion_search_databases("project")     → find the database ID
2. notion_describe_database(db_id)        → see property names and types
3. notion_query_database(db_id, filter)   → read rows
4. notion_create_page(database_id=db_id)  → add a row
5. notion_update_page(row_id, props)      → update a row's properties

Property Types Reference

The Notion property value format for common types:

// Title (every database has exactly one)
{"Title Column": {"title": [{"text": {"content": "My Title"}}]}}

// Select
{"Status": {"select": {"name": "Done"}}}

// Multi-select
{"Tags": {"multi_select": [{"name": "bug"}, {"name": "urgent"}]}}

// Number
{"Priority": {"number": 5}}

// Date (single)
{"Due": {"date": {"start": "2026-03-15"}}}

// Date (range)
{"Sprint": {"date": {"start": "2026-03-01", "end": "2026-03-15"}}}

// Checkbox
{"Done": {"checkbox": true}}

// URL
{"Link": {"url": "https://example.com"}}

// Email
{"Contact": {"email": "user@example.com"}}

// Rich text
{"Notes": {"rich_text": [{"text": {"content": "Some notes"}}]}}

// People (requires user IDs from notion_list_users)
{"Assignee": {"people": [{"id": "user-uuid-here"}]}}

// Relation (requires page IDs)
{"Related": {"relation": [{"id": "page-uuid-here"}]}}

Working with Files

File Source Schemes

All upload tools accept a source parameter with one of five schemes:

Scheme

Format

Example

Notes

notion

notion:<block_id>

notion:21f8a834-…

A file/image/pdf/video/audio block already in the workspace (block IDs from notion_list_files_on_page). Lets the PDF tools read workspace files in place, and upload tools copy a file between pages.

local

local:<path>

local:reports/q3.pdf

Relative to /data/files in the container (FILES_DIR). List with notion_list_local_files().

shared

shared:<filename>

shared:invoice.pdf

Relative to /shared in the container (SHARED_DIR), the cross-MCP mount shared with the companion google-accounts-mcp server. Populated by google-accounts-mcp/download_attachment(destination='shared'). List with notion_list_local_files(location='shared'). Preferred path for gmail → notion handoffs — no base64, no size limit.

drive

drive:<name-or-id>

drive:budget.xlsx

From the shared Google Drive folder

base64

base64:<data>

base64:SGVsbG8=

Requires filename parameter. Subject to MCP parameter size limits; prefer shared: or drive: for anything larger than ~20 KB.

File Upload Protocol

Notion uses a 3-stage upload protocol:

  1. CreatePOST /file_uploads returns an upload_id

  2. SendPOST /file_uploads/{id}/send with multipart file data

  3. Reference — attach the upload_id in a block or page property (must be used within 1 hour)

This is handled automatically by all upload tools.

Cross-MCP File Handoff (shared: scheme)

Shared-store invariants: the mount is read/write from both MCP servers. Filename clashes during google-accounts-mcp download_attachment are resolved by auto-renaming (invoice.pdfinvoice-2.pdf), and the return value always reports the actual saved name to reference. Files are swept 24 hours after last modification by the host-side TTL sweep (if you run one) — stage to Notion within that window. For immediate cleanup after a workflow, call the notion_purge_shared_files tool (see the "Files" tool table).

notion-mcp and google-accounts-mcp share a host volume (~/.local/share/containers/data/mcp-shared/) mounted into both containers at /shared. Files dropped there by google-accounts-mcp download_attachment(destination='shared') are immediately readable via the shared: source scheme:

# From google-accounts-mcp:
download_attachment(message_id="...", filename="invoice.pdf",
                    destination="shared")
# From notion-mcp:
notion_add_file_to_row(page_id="...", source="shared:invoice.pdf",
                       files_property="Attachments")

The pipeline is bidirectional — the reverse direction stages a Notion file for google-accounts-mcp to attach:

# From notion-mcp:
notion_download_file(block_id="...", destination="shared")
# From google-accounts-mcp:
draft_email(to=["..."], subject="...", body="...",
            attachments=["shared:contract.pdf"])

The file bytes never traverse MCP tool parameters, so there is no size limit and the 2026-04 "Energy Locals 1.57 MB invoice" pipeline failure no longer applies. Use this path by default for file transfers between the two servers in either direction. All transfer tools report sha256 so the agent can verify integrity at each hop ( notion_list_local_files and gmail's list_shared_files include it per file too).

Google Drive Integration

The Drive integration is an optional companion feature: it reuses OAuth refresh tokens from google-accounts-mcp (mounted read-only at /google-data/tokens.db) using the same Google client ID. Without that companion server, skip the Drive env vars — every other file source keeps working, and drive: sources return a clear setup error. Use drive: when you want the file to persist in Drive; use shared: when you just need a one-shot handoff between the two MCP servers.


Common Workflows

Read a page and make a targeted edit

1. notion_read_page_markdown(page_id)           → see current content
2. notion_update_page_content(page_id,
     old_text="Draft version",
     new_text="**Final** version")               → search-replace edit

Create a meeting notes page

1. notion_create_page(
     title="Sprint Planning 2026-03-15",
     parent_page_id="meetings-page-id",
     icon="📋",
     content="# Agenda\n\n- [ ] Review backlog\n- [ ] Assign tasks\n\n# Notes\n\n")

Query a database and update a row

1. notion_query_database(db_id,
     filter_json='{"property": "Status", "select": {"equals": "In Progress"}}')
2. notion_update_page(row_id,
     properties_json='{"Status": {"select": {"name": "Done"}}}')

Move a page into a database

1. notion_move_page(page_id, new_parent_database_id=db_id)

Download a file from Notion to Drive

1. notion_list_files_on_page(page_id)            → get block_id
2. notion_download_file(block_id, destination="drive")

Email a file stored in Notion

1. notion_list_files_on_page(page_id)            → get block_id
2. notion_download_file(block_id, destination="shared")
3. google-accounts-mcp/draft_email(..., attachments=["shared:<filename>"])

Edit a specific block on a page

1. notion_read_page(page_id, include_block_ids=true)  → find block UUID
2. notion_update_block(block_id, "New **content**")    → edit in place

Configuration

All config is via environment variables. For container deployment, put secrets in .secrets.env:

Variable

Required

Default

Description

NOTION_TOKEN

Yes

Internal integration token (starts with ntn_)

MCP_BEARER_TOKEN

Yes

Bearer token for authenticating MCP clients

NOTION_API_VERSION

No

2026-03-11

Notion API version header

PORT

No

8322

Server listen port

FILES_DIR

No

/data/files

Local file store path

GOOGLE_CLIENT_ID

For Drive

OAuth client ID (same as google-accounts-mcp)

GOOGLE_CLIENT_SECRET

For Drive

OAuth client secret

GOOGLE_TOKEN_DB_PATH

For Drive

/google-data/tokens.db

Path to google-accounts-mcp's SQLite token DB (read-only; the legacy GMAIL_TOKEN_DB_PATH name is still honored)

DRIVE_ACCOUNT

For Drive

Google account email for Drive access

DRIVE_FOLDER_NAME

For Drive

mcp-google-accounts

Shared Drive folder name

Architecture

src/notion_mcp/
  server.py          # FastMCP tool definitions, HTTP server, bearer auth middleware
  notion_client.py   # Notion REST client — all API calls, pagination, caching
  blocks.py          # Bidirectional Notion blocks <-> markdown conversion
  drive_client.py    # Google Drive client (reuses google-accounts-mcp OAuth tokens)
  config.py          # Environment variable configuration

Key design decisions:

  • notion_client.py handles all HTTP, pagination, and the database → data_source mapping. Tools in server.py never call the Notion API directly.

  • blocks.py is a pure-function module with zero side effects — easy to test and reuse.

  • Data source IDs are cached in memory per NotionClient instance to avoid redundant lookups.

  • Block appends are auto-batched at 100 (Notion API limit).

  • The bearer auth middleware is pure ASGI, compatible with streamed responses.

  • Threadpool tool offload (server.py): the MCP SDK runs synchronous @mcp.tool() handlers inline on the event loop, so one blocking httpx call would freeze every other request (the cause of the "server unresponsive after a burst" stalls). mcp.tool is wrapped so each sync tool runs its body in a worker thread (anyio.to_thread.run_sync); the loop stays free. The wrapper preserves the tool signature (client schemas unchanged — no restart) and returns the original sync function as the module name (tool-to-tool calls and tests still work). Each call logs tool=… outcome=… duration_ms=… for Loki.

  • Rate-limit backoff (notion_client._request): retries 429/502/503/504 up to 4 attempts honouring Retry-After (capped 30s/attempt). The backoff sleep runs in the offload thread, never blocking the loop. send_file_upload timeout is capped at 180s — below the MCP client's 240s ceiling — so the server fails before the client does.

  • Container healthcheck: python -m notion_mcp.healthcheck does a full HTTP round-trip to /mcp; a wedged loop fails the probe and HealthOnFailure=kill + Restart=always restart the container.

Development

Requires Python 3.12+ and uv.

# Install all dependencies including test extras
uv sync --extra test

# Run the server locally
NOTION_TOKEN=ntn_... uv run notion-mcp

# Run unit tests (fast, no API calls needed)
uv run --extra test pytest tests/ --ignore=tests/test_integration.py -v

# Run all tests including live API
NOTION_TEST_TOKEN=ntn_... uv run --extra test pytest -v

Testing

Unit Tests (203 tests, ~2s)

No API access required. Seven test files (the PDF-tool tests mock the xberg HTTP calls — no network):

test_blocks.py (77 tests) — markdown conversion:

  • rich_text_to_md / md_to_rich_text: all inline formatting variants

  • blocks_to_markdown: every block type, nesting, include_block_ids, tables

  • markdown_to_blocks: headings, lists, quotes, code, dividers, mixed content

  • format_property_value: all 20+ Notion property types

  • format_page_properties: multi-property formatting

test_notion_client.py (50 tests) — client logic with mocked HTTP:

  • Pagination for list_children, query_data_source, list_comments, list_users

  • resolve_data_source_id caching and error handling

  • create_page child batching (>100 blocks)

  • Request body construction for markdown, move, comment endpoints

  • pick_block_type MIME mapping

  • find_title_property / get_page_title edge cases

test_server_tools.py (38 tests) — tool-layer logic with a mocked client (file source resolution, positional inserts, replace flows, purge tools).

test_sources.py (13 tests) — _read_source scheme parsing for local:/shared:/base64: (the drive: scheme lives in test_drive_client.py).

test_purge.py (6 tests) — notion_purge_shared_files filters.

test_pdf_tools.py (16 tests) — notion_extract_file_text / notion_render_file_page wiring, the notion: source scheme, image vs base64 return modes, OCR plumbing (xberg calls mocked; full OCR-tier semantics are covered in google-accounts-mcp's test_pdf_read.py, since pdf_read.py is duplicated verbatim across the two repos).

test_drive_client.py (3 tests) — Drive client; guards the read-only tokens-DB regression (sqlite must open with immutable=1).

uv run --extra test pytest tests/ --ignore=tests/test_integration.py -v

Integration Tests (17 tests, ~60s)

Hit the live Notion API against a sandbox page you provide. Require:

  • NOTION_TEST_TOKEN — an integration token with access to the sandbox

  • NOTION_TEST_PAGE_ID — a scratch page the tests may write to

  • NOTION_TEST_DB_ID — an inline database on that page

Tests create pages, exercise all endpoints, and clean up after themselves:

  • Connectivity (search, list users, retrieve page)

  • Page CRUD (create, read, archive, restore)

  • Blocks (append, read, update, delete)

  • Markdown API (read, replace, search-replace, insert)

  • Data sources (resolve, get schema, query, search)

  • Move page (create parent A/B, move child, verify)

  • Comments (create, list)

NOTION_TEST_TOKEN=ntn_... uv run --extra test pytest tests/test_integration.py -v

Run integration tests after:

  • Upgrading NOTION_API_VERSION in config.py

  • Changing endpoint paths or request body formats

  • Modifying pagination or error handling

Local stdio Mode

--stdio starts FastMCP's stdio transport: no uvicorn, no bearer token (the client owns the spawned process; there is no network surface). This is what most interactive MCP clients should use:

// e.g. Claude Desktop claude_desktop_config.json / Claude Code .mcp.json
{
  "mcpServers": {
    "notion": {
      "command": "uv",
      "args": ["run", "--project", "/path/to/notion-mcp",
               "notion-mcp", "--stdio"],
      "env": {
        "NOTION_TOKEN": "ntn_...",
        "FILES_DIR": "/home/you/.local/share/notion-mcp/files"
      }
    }
  }
}

Prefer an env file over inline values where your client supports it (uv run --env-file ...).

Container Deployment (HTTP)

podman build -t notion-mcp .   # or: docker build -t notion-mcp .
podman run -d --name notion-mcp -p 8322:8322 -v notion-data:/data \
  -e NOTION_TOKEN=ntn_... \
  -e MCP_BEARER_TOKEN=some-long-random-token \
  notion-mcp

HTTP mode refuses to start without MCP_BEARER_TOKEN; clients register with:

{
  "mcpServers": {
    "notion": {
      "url": "https://your-host:8322/mcp",
      "headers": {"Authorization": "Bearer <MCP_BEARER_TOKEN>"}
    }
  }
}

notion_mcp.healthcheck does a full HTTP round-trip to /mcp (the 401 counts as alive); wire it to your container healthcheck. Terminate TLS at a reverse proxy — the server itself speaks plain HTTP. For the optional Drive/shared-mount features, add the /google-data (read-only) and /shared mounts shared with a google-accounts-mcp container.

Notion API Version Notes

This server uses Notion API 2026-03-11. Version history:

2025-09-03 (from 2022-06-28)

Feature

Old (2022-06-28)

New (2025-09-03)

Database properties

On /databases/{id} response

Moved to /data_sources/{id}

Database query

POST /databases/{id}/query

POST /data_sources/{id}/query

Database search

filter.value = "database"

filter.value = "data_source"

Markdown read

Not available

GET /pages/{id}/markdown

Markdown write

Not available

PATCH /pages/{id}/markdown

Move pages

Not available

POST /pages/{id}/move

2026-03-11 (from 2025-09-03)

Feature

Old (2025-09-03)

New (2026-03-11)

Trash status field

"archived": true/false

"in_trash": true/false

Block append positioning

"after": "block-id"

"position": {"type": "after_block", "after_block": {"id": "..."}}

Transcription block type

"transcription"

"meeting_notes"

The position object also supports {"type": "start"} and {"type": "end"} for inserting at the beginning or end of a parent block.

The server handles the data source migration transparently — you always pass database_id and the server resolves the data_source_id internally (with caching).

If upgrading from an older version, run the integration test suite to verify nothing breaks:

NOTION_TEST_TOKEN=ntn_... uv run --extra test pytest tests/test_integration.py -v

License

MIT

A
license - permissive license
-
quality - not tested
C
maintenance

Maintenance

Maintainers
Response time
Release cycle
Releases (12mo)
Commit activity

Resources

Unclaimed servers have limited discoverability.

Looking for Admin?

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

Related MCP Servers

  • -
    license
    -
    quality
    -
    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.
    Last updated
    194,217
  • A
    license
    -
    quality
    D
    maintenance
    MCP server for the Notion API, enabling management of pages, blocks, databases, data sources, comments, and users through natural language.
    Last updated
    31
    2
    MIT

View all related MCP servers

Related MCP Connectors

  • Markdown-first MCP server for Notion API with 8 composite tools and 39 actions.

  • MCP-native open-source Notion alternative: read & write pages, databases and kanban boards.

  • A MCP server built for developers enabling Git based project management with project and personal…

View all MCP Connectors

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/snickery/notion-mcp'

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