Skip to main content
Glama
notion-full-mcp
by cphoskins

notion-full-mcp

Notion MCP server with deep page reading, surgical block editing, safe snapshot/restore, file uploads (Notion-hosted images and PDFs), table row manipulation, page move and copy, and page restore from trash.

What's new in v0.5.1

  • Tool annotations. Every tool declares whether it is read-only, additive or destructive, so MCP clients can decide what to run without asking.

  • MCP Registry. The server is published to the official MCP Registry as io.github.cphoskins/notion-full-mcp.

Related MCP server: notion-private-api-mcp

What's new in v0.5.0

  • Restore keeps the page in order. Subpages, inline databases and Notion-hosted files that are still on the page stay untouched, and the restored content is written around them in snapshot order.

  • Restore brings items back from trash. A subpage, database or hosted file deleted after the snapshot is restored from Notion's trash with its ID, comments and history intact.

  • Snapshots can carry files. snapshot_page with include_files downloads Notion-hosted images and files beside the JSON, and restore uploads them again in place.

  • Fix: inserting more than 100 blocks after a block now keeps them in order.

What's new in v0.4.0

  • Restore can no longer lose content. restore_page_from_snapshot writes the snapshot first and removes the old blocks only after that succeeds. A failed restore rolls back and leaves the page as it was. Subpages, inline databases and Notion-hosted files are never deleted.

  • move_page works. It uses Notion's move endpoint, so the page keeps its ID, content, comments and subpages.

  • copy_page_to_parent reports what it could not copy. Uncopyable block types are listed in skipped, and the original is archived only when everything was copied.

  • Large and deep content. Lists nested more than two levels, tables over 100 rows and text over 2,000 characters are split across requests automatically.

  • Mentions and equations survive edits. Find/replace, restore and copy keep user, page, database and date mentions and inline equations.

  • Better errors and rate-limit handling. Errors carry Notion's own code and message. Rate-limited requests are retried using Retry-After.

  • Database rows have titles. Search, page info and create return the title whatever the title column is called.

  • More block types. Equation, bookmark, embed and externally hosted media blocks round-trip through snapshot/restore and copy.

  • Optional file boundary. Set NOTION_MCP_FILE_ROOT to confine uploads, downloads and snapshots to one directory.

What's new in v0.3.0

  • Page restore from trash. restore_page explicitly unarchives a page by sending archived=False. This is the counterpart to delete_block on a page and works around the intentional safety guard in update_page that blocks archived=False. Cascaded child archives are restored automatically when the parent is restored.

  • Destructive page copy across parents. copy_page_to_parent is the workaround for Notion's public API not supporting parent changes. Recreates the page at the target parent with the same title, icon, and content, then archives the original. Handles paragraphs, headings, lists, tables, dividers, code blocks, and image/file blocks. Refuses to run if the source has nested child_page descendants (copy those manually).

  • move_page stopped pretending. Notion's PATCH /pages/{id} endpoint silently ignores parent changes, so the tool returned an error instead. (v0.4.0 implements a real move.)

What's new in v0.2.0

  • Notion file uploads. Upload images and files directly to Notion's own workspace storage instead of relying on an external image host. Works for PNG, JPEG, SVG, GIF, PDF, and any other Notion-supported attachment type.

  • Image block helpers. upload_image_as_block uploads a file and inserts it at a position in one call. replace_image_block swaps an existing image's content while preserving its position in the parent. download_image_block pulls any image block's file to disk.

  • Table row manipulation. insert_table_row adds a row to an existing table with automatic width validation. update_table_cell replaces the text of a single cell. No more rebuild-the-whole-table for small edits.

Install

pip install notion-full-mcp
# or
uv pip install notion-full-mcp

Quick Start

export NOTION_API_TOKEN="your-token-here"
notion-mcp

Claude Code Integration

Add to ~/.claude.json under mcpServers:

{
  "notion-full": {
    "type": "stdio",
    "command": "uvx",
    "args": ["notion-full-mcp"],
    "env": {
      "NOTION_API_TOKEN": "your-token-here"
    }
  }
}

Or for local development:

{
  "notion-full": {
    "type": "stdio",
    "command": "uv",
    "args": ["run", "--directory", "/path/to/notion-full-mcp", "notion-mcp"],
    "env": {
      "NOTION_API_TOKEN": "your-token-here"
    }
  }
}

Tools (33)

Search & Pages

Tool

Description

search_pages

Search pages by title

search

Search pages and/or databases

get_page_info

Get page metadata (title, parent, dates, URL)

create_page

Create a new page under a parent page or database

update_page

Update a page's title, icon, or archive status

move_page

Move a page under a new parent page, keeping its ID and content

restore_page

Restore a page (and its cascade-archived children) from trash

copy_page_to_parent

Recreate a page under a new parent page or database. Archives the original only when every block was copied

Reading

Tool

Description

read_page_deep

Read page with all nested children (recursive)

list_block_children

List direct children of a block or page

get_block

Get a single block by ID

Editing

Tool

Description

update_block_text

Replace all text in a block

find_replace_in_block

Find/replace text preserving formatting

update_block_rich_text

Update with full rich text specification

Inserting

Tool

Description

insert_text_after

Insert a text block after a specific block (paragraph, heading_1/2/3, bulleted_list_item, numbered_list_item, quote, callout, toggle, to_do, code, divider)

insert_blocks_after

Insert multiple blocks after a specific block

append_blocks_to_page

Append blocks to the end of a page

Deleting

Tool

Description

delete_block

Delete a block by ID

Comments

Tool

Description

create_comment

Add a comment to a page

list_comments

List all comments on a page or block

Databases

Tool

Description

get_database

Get database metadata and schema

query_database

Query a database with filters and sorts

Users

Tool

Description

get_self

Get the current authenticated user/bot

list_users

List all workspace users

Snapshot / Restore

Tool

Description

snapshot_page

Save complete page structure as JSON. include_files also saves Notion-hosted images and files

restore_page_from_snapshot

Restore page content from a snapshot. Writes first, rolls back on failure, keeps subpages and databases in place, restores deleted ones from trash and re-uploads saved files

Media & Files

Tool

Description

upload_file

Upload a file to Notion's own workspace storage (returns file_upload_id)

upload_image_as_block

Upload an image and insert it as a new image block in one step (after a block or appended to a page)

replace_image_block

Replace an existing image block with a newly uploaded file, preserving its position

download_image_block

Download the image file from an image block to disk — works for external URLs, Notion-hosted files, and file_upload references

upload_file_as_block

Upload any file (PDF, attachment) and insert it as a file block

Tables

Tool

Description

insert_table_row

Insert a new row into an existing table with automatic width validation

update_table_cell

Replace the text of a single cell in a table row

Tool annotations

Every tool carries MCP annotations so a client can decide what is safe to run without asking:

  • Read-only (11): search, page and block reads, comments list, database reads and user lookups.

  • Adds without loss (13): inserts, appends, uploads, page and comment creation, move_page, restore_page, and the two tools that write local files (snapshot_page, download_image_block).

  • Marked destructive (9): update_block_text, update_block_rich_text, find_replace_in_block, update_table_cell, replace_image_block, delete_block, update_page, copy_page_to_parent and restore_page_from_snapshot. They overwrite, delete or archive existing content.

Block support and limits

  • Round-trip (snapshot/restore and copy): paragraph, headings (including toggleable), bulleted and numbered lists, to-do, quote, callout, toggle, code, divider, table, columns, table of contents, equation, bookmark, embed, and externally hosted image, file, video, PDF and audio blocks.

  • Never recreated from scratch: subpages, inline databases and synced blocks. Copy reports them as skipped.

  • Restore of those blocks, and of Notion-hosted files:

    • Still on the page: left untouched, with restored content written around them in snapshot order (preserved). One that was dragged out of snapshot order stays where it is (out_of_order).

    • File saved with the snapshot: uploaded again and recreated in place (uploaded). Files over 20 MB, or over the workspace upload limit, are not uploaded.

    • In Notion's trash: brought back as the same block (restored_from_trash). Notion returns it to the end of the page, so a block that is still on the page and came later in the snapshot ends up above it and is reported as out_of_order.

    • Gone from trash, with no saved file: cannot be restored (skipped).

    • Why out_of_order blocks are not moved (by design): the Notion API has no way to reposition a block. The only way to move a subpage or database is to trash it and restore it, which sends it to the end of the page and, if it failed midway, would leave it in trash. Restore never deletes something it cannot recreate, so it reports the block and leaves it for you to drag into place.

  • Snapshots: record subpages and inline databases as blocks, without their own content. Notion-hosted files are saved only with include_files; otherwise the snapshot holds links that expire within the hour.

  • Queries: query_database returns at most 500 rows and sets truncated when more matched.

  • Find/replace: matches within one formatting run. A phrase that spans a bold or link boundary is reported as not found.

  • API version: requests use Notion API version 2022-06-28.

Configuration

Variable

Required

Purpose

NOTION_API_TOKEN

Yes

Integration secret

NOTION_MCP_FILE_ROOT

No

When set, uploads, downloads and snapshot files must be inside this directory

Development

uv run pytest --cov=notion_mcp      # unit tests, no credentials needed

A live check runs every tool against a real workspace. It works inside one scratch page under the parent you name and moves it to trash afterwards:

export NOTION_API_TOKEN="your-token-here"
export NOTION_TEST_PARENT_PAGE="page-id-shared-with-the-integration"
uv run python scripts/live_check.py

Notion API Token

  1. Go to https://www.notion.so/my-integrations

  2. Create a new integration

  3. Copy the token (starts with ntn_)

  4. Share the pages/databases you want to access with the integration

License

MIT

Available Tools

33 tools
append_blocks_to_pageA

Append blocks to the end of a page.

blocks_json should be a JSON array of block objects.

Inputs: page_id (required), blocks_json (required). Returns: result

copy_page_to_parentA
Destructive

Recreate a page under a new parent. Prefer move_page for plain moves.

Creates a new page at the target with the same title, emoji icon and content, then archives the original. Use it to duplicate a page or to place a page in a database.

LIMITATIONS (read carefully): * Block IDs of the new page differ from the original, so links to the original blocks will point at the archived page. * Notion-hosted images and files are copied as external references to signed URLs that expire within about an hour. Re-upload the source files for a permanent copy. * Nested subpages are not copied; the tool refuses to run if any are present. * Block types that cannot be recreated are listed in skipped. When anything was skipped, the original is left in place. * Comments, page history, permissions, and synced blocks are not copied.

Args: page_id: The source page to copy. new_parent_id: The target parent page or database ID. parent_type: "page_id" (default) or "database_id". archive_original: If True (default), archive the source page after the copy completes. Set False to keep both copies side-by-side.

Inputs: page_id (required), new_parent_id (required), parent_type, archive_original. Returns: result

create_commentC

Add a comment to a Notion page.

Inputs: text (required), page_id (required). Returns: result

create_pageA

Create a new Notion page under a parent page or database.

Args: parent_id: ID of the parent page or database. title: Title for the new page. parent_type: "page_id" (default) or "database_id". icon_emoji: Optional emoji character to use as the page icon. children_json: Optional JSON array of block objects for initial content. properties_json: Optional JSON object of additional database properties to set. Example for setting Status: {"Status": {"status": {"name": "Done"}}} Example for setting Date: {"Due date": {"date": {"start": "2026-04-15"}}}

Inputs: title (required), parent_id (required), icon_emoji, parent_type, +2 more. Returns: result

delete_blockC
DestructiveIdempotent

Delete a specific block by ID.

Inputs: block_id (required). Returns: result

download_image_blockA

Download the image file from an image block to a local path.

Works for all three image source types: external URL, Notion-hosted file, and file_upload references. Notion-hosted URLs are signed and short-lived, so this tool re-fetches them at call time.

Args: block_id: The image block to download from. output_path: Local path to write the image to.

Inputs: block_id (required), output_path (required). Returns: result

find_replace_in_blockA
Destructive

Find and replace text within a block, preserving all formatting.

Replaces occurrences of old_text with new_text in each rich text segment while keeping bold, italic, links, and other annotations intact.

Returns an error (not a silent no-op) if old_text does not appear in the block. The error message includes the block's current plain text so the caller can diagnose Unicode / whitespace / formatting mismatches (e.g., en-dash U+2013 vs hyphen-minus U+002D, curly vs straight quotes, non-breaking space U+00A0 vs regular space). Use update_block_text or update_table_cell for full-text replacement when find/replace doesn't match because of character mismatches.

The success response includes a replacements field with the total number of occurrences of old_text that were replaced.

Inputs: block_id (required), new_text (required), old_text (required). Returns: result

get_blockB
Read-only

Get a single block by ID, including its type, content, and metadata.

Inputs: block_id (required). Returns: result

get_databaseB
Read-only

Get metadata and schema for a Notion database.

Inputs: database_id (required). Returns: result

get_page_infoB
Read-only

Get metadata for a Notion page (title, parent, dates, URL).

Inputs: page_id (required). Returns: result

get_selfA
Read-only

Get the current authenticated Notion user or bot info.

Inputs: no named parameters. Returns: result

insert_blocks_afterA

Insert new blocks after a specific block.

blocks_json should be a JSON array of block objects. Use make_block format: [{"type":"paragraph","paragraph":{"rich_text":[{"type":"text","text":{"content":"Hello"}}]}}]

For simple cases, use insert_text_after instead.

Inputs: block_id (required), blocks_json (required). Returns: result

insert_table_rowA

Insert a new row into an existing table.

The number of cells must match the table's existing width.

Args: table_id: The table block ID. cells_json: JSON array. Each element is either: - a plain string (single text segment) - a list of strings (multi-segment plain text in one cell) Example for a 3-column table: '["Widget", "In stock", "42"]' after_row_id: Optional row ID to insert after. If empty, append to end.

Inputs: table_id (required), cells_json (required), after_row_id. Returns: result

insert_text_afterB

Insert a simple text block after a specific block.

Args: block_id: The block to insert after. text: The text content. block_type: Block type (paragraph, heading_1, heading_2, heading_3, bulleted_list_item, numbered_list_item, quote, etc.)

Inputs: text (required), block_id (required), block_type. Returns: result

list_block_childrenA
Read-only

List direct children of a block or page (shallow, no recursion).

Returns block IDs, types, and optionally their text content. Useful for finding specific block IDs before surgical edits.

Inputs: block_id (required), include_text. Returns: result

list_commentsB
Read-only

List all comments on a page or block.

Inputs: block_id (required). Returns: result

list_usersA
Read-only

List all users in the workspace.

Inputs: no named parameters. Returns: result

move_pageA
Idempotent

Move a page under a new parent page.

The page keeps its ID, content, comments and subpages. Only page parents are supported; to place a page in a database use copy_page_to_parent.

Args: page_id: The page to move. new_parent_id: The page that becomes the new parent. parent_type: Must be "page_id".

Inputs: page_id (required), new_parent_id (required), parent_type. Returns: result

query_databaseA
Read-only

Query a Notion database with optional filters and sorts.

Args: database_id: The database to query. filter_json: JSON string for a Notion filter object. Leave empty for no filter. sorts_json: JSON string for a Notion sorts array. Leave empty for no sorting. page_size: Maximum rows per page fetch (max 100).

Returns at most 500 rows. truncated is true when more rows matched; narrow the filter to see the rest.

Inputs: database_id (required), page_size, sorts_json, filter_json. Returns: result

read_page_deepA
Read-only

Read a Notion page with all nested children recursively.

Args: page_id: The Notion page ID. max_depth: Maximum nesting depth to fetch (default 5). format: Output format - 'markdown' for readable text, 'json' for full block tree.

Returns full document content including nested blocks like sub-lists, toggle contents, and table rows.

Inputs: page_id (required), format, max_depth. Returns: result

replace_image_blockA
Destructive

Replace an existing image block's content with a newly uploaded image.

Preserves the block's position in the parent: the new image is inserted where the old one was and the old block is deleted. The block ID changes.

Args: block_id: The existing image block to replace. file_path: Local path to the new image. caption: Optional caption for the new image (omit to drop the old one).

Inputs: block_id (required), file_path (required), caption. Returns: result

restore_pageA
Idempotent

Restore a page (and its children) from trash.

This is the explicit counterpart to delete_block on a page. update_page has a safety guard that blocks archived=False; this dedicated tool is the supported way to unarchive.

When called on a page that was archived via a cascade (for example, a section container that was deleted along with its subpages), restoring the parent container automatically restores the cascaded descendants.

Args: page_id: The page to restore from trash.

Inputs: page_id (required). Returns: result

restore_page_from_snapshotA
Destructive

Restore a page from a previously saved snapshot.

Replaces the page's content with the snapshot. The snapshot is written first and the previous blocks are removed only after that succeeds, so a failed restore leaves the page as it was.

Blocks the API cannot recreate (subpages, inline databases, Notion-hosted files) are never deleted. For each one in the snapshot:

  • Still on the page: left untouched, with the restored content written around it in snapshot order (preserved). If it was moved so the order cannot be kept, it stays put (out_of_order).

  • Saved with the snapshot (include_files): uploaded again in place (uploaded).

  • In Notion's trash (about 30 days): brought back, at the end of the page (restored_from_trash).

  • Otherwise it cannot be restored (skipped).

Args: page_id: The page to restore into. snapshot_path: Path to the snapshot JSON file.

Inputs: page_id (required), snapshot_path (required). Returns: result

search_pagesB
Read-only

Search Notion pages by title. Returns page IDs, titles, and URLs.

Inputs: query (required), max_results. Returns: result

snapshot_pageA

Create a complete snapshot of a page with all nested children.

Saves to a JSON file if output_path is provided. The snapshot includes the full block tree with recursive children, suitable for restore. Subpages and inline databases are recorded as blocks but their own content is not captured.

Set include_files to also download Notion-hosted images and files into a <snapshot name>.files folder beside the JSON. A restore can then upload them again even after they have left Notion's trash. Without it, the snapshot holds only links that expire within the hour.

Default path: ~/notion-snapshots/<page_id>_.json

Inputs: page_id (required), output_path, include_files. Returns: result

update_block_rich_textA
DestructiveIdempotent

Update a block's rich text with fully specified formatting.

rich_text_json should be a JSON array of rich text objects, e.g.: [{"type":"text","text":{"content":"Bold text"},"annotations":{"bold":true}}]

Inputs: block_id (required), rich_text_json (required). Returns: result

update_block_textA
DestructiveIdempotent

Replace all text in a specific block with new plain text.

Preserves the block type but replaces all rich text content. Use find_replace_in_block for targeted text changes that preserve formatting.

Inputs: block_id (required), new_text (required). Returns: result

update_pageA
DestructiveIdempotent

Update a page's title, icon, or archive status.

Only fields with non-default values are sent in the PATCH body. This tool can archive a page (archived=True) but never unarchives one; use restore_page to bring a page back from trash.

Args: page_id: The page to update. title: New title text. Omit or pass "" to leave unchanged. icon_emoji: New icon emoji. Omit or pass "" to leave unchanged. archived: Pass True to archive the page. Defaults to False (no change sent). properties_json: Optional JSON object of database properties to set. Example: {"Status": {"status": {"name": "Done"}}} Example: {"Due date": {"date": {"start": "2026-04-15"}}} Can be combined with title — both will be applied.

Inputs: page_id (required), title, archived, icon_emoji, +1 more. Returns: result

update_table_cellA
DestructiveIdempotent

Replace the text of a single cell in a table row.

Args: row_id: The table_row block ID. cell_index: Zero-based index of the cell within the row. new_text: New plain-text content for the cell. Overwrites all existing rich text segments in that cell.

Inputs: row_id (required), new_text (required), cell_index (required). Returns: result

upload_fileA

Upload a file to Notion's own storage.

Returns a file_upload_id that can be referenced in image, file, pdf, or video blocks via {"type": "file_upload", "file_upload": {"id": ...}}. For the common case of uploading an image and creating a block in one step, use upload_image_as_block instead.

Args: file_path: Local path to the file (PNG, JPEG, GIF, SVG, PDF, etc.), up to 20 MiB.

Inputs: file_path (required). Returns: result

upload_file_as_blockA

Upload a file (PDF, doc, any attachment) to Notion and insert as a file block.

Analogous to upload_image_as_block but for non-image files. Use this for PDFs, spreadsheets, archives, etc.

Args: file_path: Local path to the file. after_block_id: If provided, insert after this block (siblings). parent_id: If after_block_id is empty, append to this page/block. caption: Optional caption. name: Optional display name override (defaults to the filename).

Inputs: file_path (required), name, caption, parent_id, +1 more. Returns: result

upload_image_as_blockA

Upload an image file to Notion and insert it as a new image block.

The image is stored in Notion's own workspace storage, so no external image host is needed.

Args: file_path: Local path to the image (PNG, JPEG, GIF, SVG, WEBP, etc.) after_block_id: If provided, insert immediately after this block (siblings). parent_id: If after_block_id is empty, append to this page/block instead. caption: Optional caption text for the image.

Exactly one of after_block_id or parent_id must be provided.

Inputs: file_path (required), caption, parent_id, after_block_id. Returns: result

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections.

Loading tool schema changelog…

    TDQS

    A3.5/5.0

    Scored across 33 tools

    Disambiguation3/5

    Several tool pairs overlap: search vs search_pages (the former is a strict superset), update_block_text vs find_replace_in_block vs update_block_rich_text all mutate block text, insert_text_after is a convenience wrapper over insert_blocks_after, and upload_file vs upload_image_as_block/upload_file_as_block share a purpose. The descriptions mitigate this well by explicitly cross-referencing ('use upload_image_as_block instead', 'prefer move_page'), but the agent still has to read the fine print to pick correctly.

    Naming Consistency5/5

    Every tool is snake_case with a clear verb_noun shape (get_page_info, update_table_cell, insert_table_row, query_database, restore_page_from_snapshot). The only deviations are the bare noun 'search' and a couple of noun-ish names, which are idiomatic and readable rather than inconsistent.

    Tool Count3/5

    33 tools is heavy for a Notion surface, though the domain (blocks, pages, tables, files, snapshots, databases, comments, users) legitimately demands breadth. Still, a handful of mergeable pairs (search_pages into search, insert_text_after into insert_blocks_after, upload_file into the two higher-level uploaders) would trim the surface without losing capability.

    Completeness4/5

    Coverage is strong: full page lifecycle (create/update/move/copy/restore/archive via delete_block), deep block read and multiple write paths, table cells/rows, file and image upload/download/replace, comments, database query, and snapshot/restore. Gaps are minor but real — no comment deletion or comment retrieval by block, and no database schema creation/update, only get and query.

    Maintenance

    ActivityMaintained
    ResponsivenessOutdated data

    Related MCP Connectors

    Related MCP Servers

    • A
      license
      A
      quality
      C
      maintenance
      Unofficial Notion MCP server built on Notion's private API (token_v2 cookie). Gives LLM agents full read/write access to the entire workspace — no integration token and no per-page sharing.
      11
      18 npm
      4
      MIT
    • A
      license
      Not graded
      quality
      D
      maintenance
      Comprehensive MCP server for the Notion API. Provides 22 tools for full CRUD operations on pages, databases, blocks, users, and comments.
      8 npm
      MIT