notion-plus-mcp-server
A local MCP server for Notion that provides precise, validated, undoable read/write operations, database and view management, charts/reports, and automations/workflows.
Search and read pages, blocks, databases, schemas, comments, and templates.
Query, aggregate, and bulk create/update rows with dry-run previews and schema validation.
Edit content surgically: patch/insert/delete/copy/move blocks, find/replace, update page properties/icons/covers/moves, duplicate pages.
Create and edit databases: full schemas, relations, rollups, formulas, unique IDs, property/option changes.
Manage views (table, board, calendar, timeline, gallery, form, chart, map, dashboard) and generate chart images/reports/Mermaid diagrams.
Automate recurring rules and multi-step workflows via schedules, webhooks, approval steps, and external HTTP/Slack actions.
Undo changes with snapshots, change history, recovery from interrupted writes, and safety checks against concurrent edits.
Run diagnostics and capability checks, and manage local state/templates for repeatable pages.
Ships a GitHub Actions workflow that runs the server's Notion automation rules hourly (at minute 17) from the committed rules file, with a manual Run workflow button that accepts dry_run (on by default) and specific-rule inputs.
Provides tools for precise editing of Notion pages and databases: patching individual blocks by id, inserting markdown or structured blocks, find/replace that preserves formatting, deleting blocks, and trashing pages. For data it reads pages and schemas, queries rows with simple filters or raw Notion filters, creates pages, updates properties with schema validation and forgiving name matching, and performs filtered bulk updates with dry-run previews and rate limiting. It also manages schema (adding properties, adding select/multi-select options, renaming properties), records an undo journal so any prior write can be reverted, supports an optional expected-last-edited-time guard against stale overwrites, and can run scheduled rules that query a database and set/append/comment/trash matching rows.
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@notion-plus-mcp-server Bulk update all rows in Tasks where Status is Blocked to In Progress, preview first"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
notion-plus-mcp-server
A local MCP server for Notion built for precise edits. It changes exactly the block or field you point at, checks every value against the database schema before writing, previews risky changes, and records undo for the writes it supports (best effort; see Undo coverage).
Why this instead of the built-in Notion connector
Built-in connector | notion-plus | |
Editing page content | Rewrites content | Patches one block by id, inserts at an exact position, find/replace that keeps formatting and mentions |
Setting properties | Values passed through as-is | Validated against the schema; forgiving name and option matching; all errors reported at once |
Bulk changes | One call per row | Filtered bulk update with dry-run preview and rate limiting |
Mistakes | Manual cleanup | Writes return an |
Stale overwrites | Not detected | Optional |
Schema changes | Limited | Create databases with relations, rollups, formulas, unique IDs, and status groups; add, rename, retype, or delete properties; manage options and number formats. Deleted properties keep their values for undo |
Views | Not available | Create, edit, and delete table, board, list, calendar, timeline, gallery, form, chart, map, and dashboard views |
Related MCP server: Notion MCP Server
Setup
1. Create a Notion integration. Go to https://www.notion.so/profile/integrations, create an internal integration, enable read content, update content, insert content, and (if you want automations to comment) insert comments, then copy the secret. To set people properties by name or email, also enable "Read user information including email addresses". Personal access tokens can't look up users at all; with one, pass user ids.
2. Share pages with it. In Notion, open each top-level page or database you want Claude to reach, click ••• → Connections, and add the integration. Everything under a shared page is included.
3. Install. Two ways:
One-click bundle (Claude Desktop and Cowork). Download the
.mcpbfile for your computer (darwin-arm64for Apple silicon Macs,darwin-x64for Intel Macs,win32-x64,linux-x64) from the Releases page, double-click it (or drag it onto Claude Desktop's Settings → Extensions), and paste your integration secret when asked. You can also pick a folder uploads may come from and your time zone. Cowork runs inside Claude Desktop and uses the extensions installed there. Skip to step 5.From source (for Claude Code, or to run automations). Requires Node 22 or later.
git clone https://github.com/katekruger/notion-mcp.git
cd notion-mcp
npm ci
npm run buildTo update later: git pull && npm ci && npm run build, then restart Claude (or start a new Claude Code session).
To build a bundle yourself: npm run bundle writes bundles/notion-plus-<version>-<platform>-<arch>.mcpb. Bundles are per platform because chart images use a native renderer; pushing a v* tag builds all four in GitHub Actions and attaches them to a release.
4. Connect it to Claude (from source). Use the full path to your clone.
Claude Code:
claude mcp add notion-plus --env NOTION_TOKEN=ntn_your_secret_here -- node /ABSOLUTE/PATH/TO/notion-mcp/dist/index.js
claude mcp list # notion-plus should show as connectedClaude Desktop: open Settings → Developer → Edit Config, and add this to claude_desktop_config.json, then restart Claude Desktop:
{
"mcpServers": {
"notion-plus": {
"command": "node",
"args": ["/ABSOLUTE/PATH/TO/notion-mcp/dist/index.js"],
"env": { "NOTION_TOKEN": "ntn_your_secret_here" }
}
}
}Turn off the built-in Notion connector while using this one so Claude doesn't pick between two sets of Notion tools. Keep the token out of the repo: it belongs only in your Claude config (or a local .env, which is gitignored).
5. Check it works. Ask Claude to "run notion_doctor". It checks the settings, the token, the integration's capabilities, whether any pages are shared with it, the local state folder, upload folders, and the chart renderer, and says how to fix anything that fails. Then ask "search Notion for "; you should see notion_search results with ids. If a setting is invalid (a misspelled time zone, say), the server still starts and every tool answers with the exact setting to fix. To call tools by hand instead, run NOTION_TOKEN=ntn_... npm run inspect to open the MCP Inspector.
6. Optional: scheduled automations. See Automations and GitHub Actions below.
Tools
Every tool answers with the same JSON shape:
{
"status": "ok | partial | error",
"summary": "one line",
"data": { "...the tool's own result..." },
"warnings": ["things to know, such as what couldn't be copied"],
"undo": { "id": "a1b2c3d4", "coverage": "full | partial | none" },
"pagination": { "next_cursor": "…" },
"next_actions": ["what to do next, when there's something to do"]
}Only status, summary, and data are always there. Results that would be too large shrink their longest lists and say how many items were left out (truncated); they're always valid JSON. Results that carry page, block, comment, or row text include a warning that the text is untrusted data, and the server's instructions tell the model the same: content in Notion pages isn't a source of instructions.
Read
notion_search: find pages and databases by title.notion_get_page: properties plus a content outline with every block id, or (format: "markdown") the whole page as Notion-flavored markdown that can be written back.notion_get_blocks: read one section of a large page.notion_find_blocks: locate blocks by text or regex.notion_get_schema: property types, options, status groups, relations.notion_query: rows viawhere(equality, operators,in,or/and, relative dates) or raw Notion filters.notion_aggregate: counts, sums, averages, and more, grouped by any property or date bucket, without pulling rows into context.
Write content
notion_patch_block: edit one block's text, checkbox, code language or caption, color, toggle heading, callout icon, or a table row's cells.notion_insert_blocks: add markdown or structured blocks at the start, end, or after a specific block.notion_replace_text: find and replace across a page's text, table cells, captions, and title, keeping formatting (dry run by default).notion_delete_blocks: trash specific blocks.notion_copy_blocks: copy blocks with everything inside them to another spot, or move them (move: true, dry run by default).notion_comments: list, add, and reply to comments.
Pages
notion_update_page: title, icon, cover, lock, or move a page under another page or into a database.notion_duplicate_page: copy a page with its content, icon, cover, properties, sub-pages, and databases (schema and rows, or schema only withdatabases: "schema"). Sub-pages and databases keep their place in the page.notion_list_templates: a database's templates, fornotion_create_page'stemplate.
Write data
notion_update_properties: set row fields with validation.notion_create_page: new row or sub-page with content, an icon and cover, or from a database template.notion_bulk_update: set values on every matching row, or different values per row (dry run by default, up to 500 rows).notion_bulk_create: add up to 1000 rows from JSON or CSV (dry run by default; every row validated first).notion_trash_page: trash a page.
Databases
notion_create_database: a database with its full schema in one call: options (status options with groups), number formats, formulas, one- or two-way relations (including to itself), rollups, unique IDs, files, places.notion_schema: add, rename, or delete a property (delete previews first and undo restores the values), add select/multi-select/status options, change number formats and descriptions.
Views and visuals
notion_views: list, read, create, update, and delete views: table, board, list, calendar, timeline, gallery, form, map, dashboard, and Notion's native chart views (column, bar, line, donut, number, with stacking). A view can be a database tab, a linked view placed anywhere on a page, or a dashboard widget.notion_create_chart: render a chart image (bar, column, stacked, grouped, line, area, pie, donut, scatter) from inline data or a database query, and refresh it in place later.notion_build_report: a report page for a database: summary, KPI numbers, live and image charts, a table of key rows (such as overdue items), and a Mermaid Gantt chart.notion_workflow: multi-step workflows with schedule, manual, or webhook triggers (see Workflows).notion_template: build pages from reusable templates (see Templates): list, get, validate, preview (outline, counts, and a diff against an existing page), render, save, delete, export, import.
Safety
notion_history: changes and their undo ids, newest first, paged withcursor. Each says whether it's undoable, partly undoable, undone, or was interrupted part-way (with the tool's input, to check by hand).notion_doctor: check the setup and say how to fix what isn't working. Changes nothing.notion_capabilities: what the server can do with this integration (probed with read-only calls), the API version, and the limits that matter for planning.notion_undo: revert a change. Refuses if anything it would restore was edited afterward, and lists what; passforce: trueto overwrite.
Automations (see below)
notion_automation: list, add, update, validate, dry-run, enable, disable, delete, and run rules, and see recent runs.
API version
The server pins Notion API version 2026-03-11 (set NOTION_VERSION to override). That version renamed archived to in_trash, transcription blocks to meeting_notes, and replaced the flat after parameter of block appends with position; this server already used the new forms, and the full live suite passes on it.
Content formats
notion_insert_blocks and notion_create_page take markdown or structured blocks, and both can express every block type the API can create:
Content | Markdown | Structured block |
Headings 1–4, toggle headings |
|
|
Lists, to-dos, nesting |
|
|
Callouts |
|
|
Toggles |
|
|
Tables | pipe tables, or |
|
Columns |
|
|
Tabs |
|
|
Code, Mermaid diagrams |
|
|
Equations |
|
|
Images, files, PDF, video, audio |
|
|
Bookmarks, embeds |
|
|
Synced blocks |
|
|
Links to pages, TOC, breadcrumb |
|
|
Dividers |
|
|
Inline: **bold**, *italic*, `code`, ~~strike~~, <u>underline</u>, [link](url), $x^2$, <span color="red">…</span> (any color, or blue_bg for backgrounds), and mentions: <mention-page url="…"/>, <mention-user email="…"/>, <mention-date start="2026-10-01"/>. A block's color goes at the end of its line: Text {color="blue"}.
These are the same tags Notion's markdown export uses, so notion_get_page with format: "markdown" returns markdown you can edit and insert back; the live test suite checks that a full page survives the round trip line for line. Local files can be uploaded from the working directory and the temp folder; set NOTION_PLUS_UPLOAD_DIRS (separated by :, or ; on Windows) to allow other folders.
The undo journal is stored in ~/.notion-plus/workspaces/<integration id>/journal.json (last 500 changes; see Local state). Each write is journaled before it reaches Notion: if the server stops mid-write, notion_history shows the entry as interrupted with the tool's input, so you can check those objects by hand. Undo restores the snapshot taken at write time. Before writing, it checks every page, block, or database it would restore; if any was edited after the original change (by a person, or by a later change through this server, which it names), it writes nothing and lists them. The check is per object, so an edit to a different field of the same page also counts, and edits in the same minute as the original change can't be seen. Undoing added options deletes them, which also clears them from any rows that used them since. Schema changes are the exception to the edit check: a database's edit time moves with every schema change, so it can't tell whose change it was; schema undo only touches the property it names.
Visuals: which to use
Native Notion content for structure and diagrams: callouts, columns, tables, equations, and Mermaid diagrams (
```mermaidcode blocks: flowcharts, sequence, Gantt, pie, timeline). Mermaid is checked before writing so a typo doesn't leave an error box.Chart views (
notion_viewswithtype: "chart") when the data lives in a Notion database: they stay live, filter with the database, and people can click through. They can sit on any page as a linked view.Chart images (
notion_create_chart) for chart types Notion lacks, data from outside Notion, or a fixed snapshot. Types: bar, column, stacked and grouped columns, line, area, stacked area, pie, donut, scatter, and:histogram(one row per observation;bins),boxplot(x = group, y = observation),heatmap(x = column, series = row, y = value)waterfall(rows in order, y = change; a row with series"total"shows the running total),funnel(stages in order),bullet(actualandtargetrows per category)small_multiples(one panel per series),dual_axis(two series: columns on the left axis, a line on the right),treemap(x = item, y = size, series = group),sankey(x = from, series = to, y = amount; flows must only go forward),network(x and series are the two ends of a link, y its weight; nodes sit on a circle sized by how connected they are)
annotationsadd labeled reference lines ({at: "Q2", label: "Launch"}or{value: 100, label: "Target"}). Images use a colorblind-checked palette by default, thin marks, direct value labels, and a legend whenever there's more than one series; past eight series the smallest fold into "Other".paletteswitches tocool,warm,mono, or your own hex colors. Notion shows an image the same way in light and dark mode, so pick the surface withtheme:light(default),dark, ortransparent.format: "svg"uploads a vector image instead of a PNG.data_table: trueadds a "Chart data" toggle under the chart with a one-line description and the numbers, so the chart has a text alternative (the description is also returned asalt_text). For anything else, pass a whole Vega-Lite spec asvega_lite: data must be inline, and specs that load URLs, link out, or use image marks are refused;vega_lite_themepicks its surface the same way. The recipe of a regular chart is stored incharts.jsonin the local state folder, andrefresh_block_idredraws it from current data in the same block.Embeds (
embedblocks) for interactive charts hosted elsewhere, when neither of the above fits.
notion_build_report combines these: it uses a live chart view where Notion supports the chart type and renders an image otherwise.
Queries and aggregation
where in notion_query, notion_aggregate, notion_bulk_update, and automation rules accepts:
{
"Status": {"in": ["Done", "At Risk"]},
"Due": {"before": "today"},
"Estimate": {">": 500, "<=": 1000},
"Tags": "Q4",
"Owner": null,
"or": [{"Priority": "High"}, {"Task": {"contains": "urgent"}}],
"$last_edited": {"after": "-7d"}
}Operators: =, !=, >, >=, <, <=, contains, not_contains, starts_with, ends_with, in, not_in, is_empty, before, after, on_or_before, on_or_after, and date ranges such as past_week or next_month. Dates accept today, tomorrow, yesterday, and offsets like +7d, -2w, +1m (in NOTION_PLUS_TIMEZONE, default the system zone). Property names and option values are matched forgivingly and checked against the schema.
notion_aggregate takes group_by (a property, or {"property": "Due", "by": "month"}) and metrics such as ["count", "sum:Estimate", "avg:Estimate"]. Rows with several values (multi-select, people, relations) count once in each of their groups, and relation groups show the related rows' titles. It scans up to 10,000 rows by default (max_rows, up to 50,000).
Relation values in writes can be page ids, links, or the related row's exact title.
Undo coverage
Undo is best effort: it covers the writes below, from a snapshot taken at write time, and refuses when something it would restore was edited afterward.
Write | Undo |
Property values, page title/icon/cover/lock, moves | Restores the previous values |
Inserted, copied, or report blocks; created pages, rows, databases | Trashes them |
Patched or replaced blocks, find and replace | Restores the previous content |
Deleted blocks, trashed pages | Restores them from the trash |
Schema changes, including deleted properties | Restores the property and up to 2000 rows' values |
Comments added by this server | Deletes them |
Views created, changed, or deleted | Deletes, restores, or re-creates them |
Chart refreshes | Puts back the previous image (when it could be downloaded) |
Automation runs | Everything above that the run did, as one entry |
Results say coverage: "partial" when part of a write can't be reverted (the entry names what), and none when nothing can. Undo can't see edits in the same minute as the original change (Notion reports edit times to the minute), entries roll off after 500 changes, and comments by other integrations, re-created unique IDs, and Notion-hosted files that expired can't be restored. A write interrupted by a crash is journaled as interrupted before it starts, so it's visible but may have nothing to undo.
Templates
notion_template builds a page from a declarative template, so a recurring page (a weekly update, a brief, a launch plan) is one call with values rather than dozens of block calls. Five ship with the server: weekly-executive-report, content-brief, launch-plan, research-dossier, and database-report (the page notion_build_report makes, as a template you can copy and change); get shows any of them as a starting point for your own, and save keeps yours in templates.json in the home folder (a saved template with a built-in's name replaces it).
A template declares its variables (type, required, default), then lists blocks:
{
"version": 1,
"name": "weekly-update",
"variables": { "week_of": { "type": "date", "required": true }, "wins": { "type": "list", "default": [] } },
"title": "Week of {{week_of}}",
"blocks": [
{ "kpis": [{ "label": "Open tasks", "metric": { "database": "https://www.notion.so/…", "value": "count", "where": { "Status": { "not": "Done" } } } }] },
{ "if": "wins", "then": [{ "heading": "Wins" }, { "each": "wins", "as": "w", "blocks": [{ "markdown": "- {{w}}" }] }] },
{ "chart": { "spec": { "type": "line", "title": "Signups" }, "source": { "database": "https://www.notion.so/…", "x": { "property": "Created", "by": "week" }, "y": "count" } } },
{ "slot": "notes" }
]
}Blocks can be markdown, headings, callouts, toggles, columns, dividers, KPI numbers (fixed, or computed from a database), charts (any notion_create_chart type, from data or a database query), live database views (top level), tables (fixed rows or a database query), a database summary callout (summary: rows, completion, overdue), Mermaid Gantt charts (fixed tasks, or query for a database's dated rows), and raw block specs. Where a field takes an object or list (a where, a table's properties), it can also be a whole "{{variable}}" that holds one. each repeats blocks for every item of a list ({{item.field}}, {{item_index}}), if picks blocks by a condition ("var" for set and non-empty, or {var, equals | not | in | gt | lt | empty}), parts are named block lists a template reuses with {part, with}, and slots are filled at render time with markdown or blocks. Every variable is checked before anything is written, and a reference to a value that doesn't exist is an error rather than a blank in the page. preview shows the outline, the charts and views it would make, an estimate of the Notion requests, and, with compare_to, a line diff against an existing page; render creates the page under parent or appends to append_to, and notion_undo removes what it wrote.
Known Notion API limits
A block's type can't be changed in place; insert a new block and delete the old one.
Blocks can't be moved, so
notion_copy_blockswithmove: truecopies and then trashes the original: moved blocks get new ids and lose their comments. It refuses moves that would lose content the API can't recreate (databases, read-only blocks) or break synced-block references. Pages are moved natively.There's no API for duplicating a page or a database;
notion_duplicate_pagerebuilds both. Databases directly on the page are recreated in place with their schema and up to 500 rows per data source (values and content); relations inside the copied database point at the copied rows, while a two-way relation to a database outside the copy becomes one-way so nothing outside the copy changes. Views aren't copied (the copy gets a default table view), and linked database views can't be read through the API, so both are reported. Notion only creates pages and databases directly on a page, so sub-pages inside toggles or columns go to the end of the copy and databases there are skipped.Button blocks can't be created or read through the API. Breadcrumbs, bookmarks, and links to pages don't appear in Notion's markdown export; this server fills them in. Code captions aren't in the markdown export either.
A heading 4 can't be updated without resending its text;
notion_patch_blockhandles that.Notion's built-in database automations can't be created or edited through the API. Button blocks, AI blocks, and some embeds are read-only (the API returns them as
unsupported).Map views need a place property (
map_by). Dashboards can be created and given widgets, but the widget layout beyond "new row" or "existing row" is set in Notion.A chart image can be replaced in place, but Notion won't take an old file link back, so a refresh keeps a copy of the previous image in
~/.notion-plus/charts/for undo.Status options can be added, each in a group (To-do, In progress, Complete). Options sent without a group all land in To-do, so this server guesses the group from the option name ("Done" → Complete) and says so.
Last-edited times are rounded to the minute, so the freshness check catches edits made in an earlier minute.
Select and status option names and colors can't be changed through the API. Renames are accepted and silently ignored; color changes are rejected. Rename options in Notion, or add a new option, move rows with
notion_bulk_update, and delete the old option in Notion.Leaving an option out of a schema update deletes it and clears it from every row, so this server always sends the full list.
Verification properties can only be created in wikis; in a regular database Notion skips them without an error (
notion_create_databasereports it).There's no aggregation endpoint;
notion_aggregatereads the matching rows and summarizes them on your machine (about 100 rows per request). Rows with more than 25 relations or people are counted by their first 25.Deleting a property deletes its values.
notion_schemasaves up to 2000 rows' values first so undo can put them back; a re-created unique ID renumbers rows, and a re-created two-way relation re-creates its other side.One write can carry at most 100 relations or people, and at most 100 rich text segments per block or property (each segment up to 2000 characters). This server splits long text into segments and returns a clear error past those limits instead of truncating.
blocks.children.appendaccepts 2 levels of nesting per request; deeper content is added in follow-up requests automatically.Page reads include at most 25 items of a title, rich text, relation, or people value. Undo snapshots and before/after previews re-read the full value, so nothing is lost;
notion_get_pageand bulk dry-run previews may still show only the first 25.New databases take a few seconds to appear in
notion_search.Notion merges adjacent text segments with identical formatting when you read them back, and normalizes link URLs (for example adding a trailing
/).Undoing a large insert trashes blocks one request at a time (about 3 per second).
A comment can only be deleted by the integration that created it, so undo can remove comments this server added, but nothing else.
Requests time out after 30 seconds (
NOTION_TIMEOUT_MS). Reads are retried after timeouts and dropped connections; writes are not, since the first attempt may have landed. Notion's 429 and 5xx responses are retried by the client, honoringRetry-After.Results are capped at 25,000 characters. Long lists are shortened (with a count of what was left out) so the result stays valid JSON; narrow the request to see the rest.
Automations
Rules run on a schedule in GitHub Actions (or locally with npm run automations). Rules you manage from Claude are saved in rules.json in the local state folder, so they survive reinstalling or upgrading the server; the GitHub workflow runs the committed automations/rules.json (it sets NOTION_PLUS_RULES to it). Upgrading from 0.9 or earlier, rules already in automations/rules.json of your checkout are copied to the new location the first time. A rule picks rows with a condition, runs at set times, or both, and acts on what it finds. Manage rules from Claude with notion_automation ("add an automation that…"), which checks each rule against the live database and previews it before saving.
{
"version": 1,
"timezone": "America/New_York",
"rules": [
{
"id": "at-risk",
"schedule": "weekdays 09:00",
"database": "https://www.notion.so/…",
"when": { "where": { "Due": { "before": "today" }, "Status": { "not_in": ["Done", "At Risk"] } } },
"actions": [{ "set": { "Status": "At Risk" } }, { "comment": "Past due: {{page.Task}} was due {{page.Due}}" }]
},
{
"id": "stamp-completed",
"database": "https://www.notion.so/…",
"when": { "where": { "Status": "Done", "Completed Date": null } },
"actions": [{ "set": { "Completed Date": "{{today}}" } }],
"then": [{ "refresh_chart": "<chart image block id>" }]
},
{
"id": "weekly-report",
"schedule": "weekly mon 08:00",
"then": [{ "build_report": { "database": "https://www.notion.so/…", "parent": "https://www.notion.so/…", "title": "Week of {{today}}" } }]
}
]
}When a rule runs. With only when, every run (hourly in GitHub Actions) checks for matching rows. With a schedule ("hourly", "daily 09:00", "weekdays 09:00", "weekly mon 09:00", "monthly 1 09:00", or 5-field cron, in the file's timezone), the rule fires once per scheduled time, on the first run at or after it; with both, the condition is checked only at those times. Because the workflow runs at minute 17, "weekdays 09:00" fires at about 9:17.
Conditions (when): where and filter work exactly like notion_query, including operators and relative dates. relative takes {property, older_than_days | newer_than_days} for a date, created time, or last edited time property, or "$created" / "$last_edited".
Row actions (actions), applied to each matching row: {"set": {...}} (validated like notion_update_properties), {"append": "markdown"} or blocks, {"comment": "text"}, {"trash": true}.
Follow-up actions (then), run once per firing after the row actions (and, when the rule has a condition, only if it acted on at least one row):
{"refresh_chart": "<block id>"}: redraw a chart made bynotion_create_chartor a report. When you save the rule throughnotion_automation, the chart's recipe is copied into the rule so the GitHub runner can redraw it.{"build_report": {...}}: the same arguments asnotion_build_report.replace_previous(on by default) trashes the report this rule built last time.{"create_page": {"parent", "title", "template"?, "properties"?, "markdown"?}}.
Strings can use {{today}} (in the file's timezone), {{now}}, and in row actions {{page.<Property>}}, {{page.url}}, {{page.id}}.
Each rule acts once per row. A rule must stop matching a row after acting on it, or it would act again on every run. The runner refuses a rule unless it writes a value its condition no longer matches (for example Status: {"not_in": ["Done", "At Risk"]} then setting "At Risk"), trashes the row, or names a marker: a checkbox property the runner requires to be unchecked and then checks. Scheduled rules also remember which scheduled time they last fired for, so a time fires once even if runs overlap or repeat.
Order, retries, and partial failure. Each row's actions run as steps, and progress is saved after every step: first set values that keep the row matching, then append and comment, then the writes that take the row out of the condition (the marker, or set values for properties the condition checks), then trash. A row is only marked handled once everything before it landed. If a step fails, the row keeps matching, and the next run picks it up again from the step that failed, skipping the ones that succeeded. A scheduled occurrence only counts as fired when every row and every then action of it succeeded; otherwise the next run retries it (completed then actions aren't repeated), until it succeeds, a newer occurrence arrives, or you waive it. An edited rule starts unfinished rows over. Each rule's run reports succeeded, partial (some steps failed or were left for the next run), failed, or skipped (not due).
Other options: enabled (default true), limit (rows per run, default 50; the rest wait for the next run), allow_new_options, data_source_name, name.
Managing rules from Claude
notion_automation handles every step: list, get, add, update (merge fields), validate (check a rule and preview what it would do), dry_run (never writes; force: true ignores schedules), enable, disable, delete, run (writes; one undo id per rule; returned as an error unless every rule succeeded, with the details and undo ids kept), history (local runs, newest first, paged with limit and cursor), waive (give up on a rule's unfinished firing: the occurrence counts as fired and unfinished rows start over), export and import (the whole rules file as JSON; import_mode merge or replace), and deploy (the rules file with chart recipes embedded, plus the steps to run it in GitHub Actions). list and get return the file's revision; passing it back as expected_revision refuses the edit if the file changed in the meantime.
Running locally
npm run automations -- --dry-run # preview every enabled rule that's due
npm run automations -- --dry-run --force # preview as if every schedule were due
npm run automations -- --rule at-risk --force
npm run automations # applyOne run stops starting new rows once it reaches any of its limits: 200 rows (--max-rows, AUTOMATIONS_MAX_ROWS; the old --max-writes still works), 3000 Notion requests (--max-requests), 5000 appended blocks (--max-blocks), or 20 minutes (--max-minutes). Rows left over run next time, and the run reports partial. A failing rule is reported and the others still run. The command exits non-zero unless every rule succeeded, and logs one JSON line per event on stderr, each with the run's run_id (NOTION_PLUS_LOG=text for plain lines, off to silence). Each rule's run is one journal entry, so notion_undo <undo_id> reverts it, including deleting the comments it added and trashing pages it created. Runs are logged to automation-runs.jsonl, and schedule state is kept in automation-state.json, both in the local state folder.
GitHub Actions
.github/workflows/automations.yml runs every hour (at minute 17) from the committed automations/rules.json. It never lets two runs overlap. Schedule state (which occurrences fired, and unfinished rows and occurrences to resume) is committed after every run to the notion-automations-state branch, which only holds automation-state.json (no page content). Once that branch exists, a run whose state can't be restored stops instead of re-firing old occurrences. The undo journal and run log carry over in the Actions cache (best effort: an evicted cache only shortens undo history) and are uploaded as an artifact (notion-plus-journal-<run id>, kept 90 days). When a run fails or is partial, it opens an issue labeled notion-automations (creating the label if needed) or comments on the open one; if that isn't possible, the job summary says so. The manual Run workflow button takes dry_run (on by default), rule, and force.
One-time setup
Make a token for the job. Create a separate internal integration (step 1 of Setup) and share only the pages and databases your rules touch with it (including report parents). Enable insert comments if rules comment. This limits what the scheduled job can reach.
Add it as a repository secret. Open the repo on GitHub, click the repo's Settings tab, then Secrets and variables → Actions → New repository secret. Name it
NOTION_TOKENand paste the token.Test the workflow. Go to Actions → Notion automations → Run workflow, leave "dry run" checked, and run it. A green run whose summary lists your rules (or "No enabled rules.") means the token and workflow work.
Adding a rule
In Claude, ask for it in plain words, for example: "Every weekday at 9am, mark rows past their due date as At Risk and add a comment." Claude uses
notion_automation, which checks the rule against the live database, shows the rows it would act on, and saves it on your machine.Ask Claude to deploy the rules (
notion_automationdeploy), save the returned file asautomations/rules.jsonin your repository, and commit and push it. (If you work in a checkout withNOTION_PLUS_RULESpointing atautomations/rules.json, edits land there directly.)Run the workflow once by hand with "dry run" checked (and "force" to see a scheduled rule now), and read the summary. After that, the hourly runs apply it.
To pause a rule, disable it (or set "enabled": false) and push. To give up on an occurrence that keeps failing, waive it (locally, on a state file restored from the notion-automations-state branch) or fix the cause; it's retried every run until then. To stop everything, disable the workflow under Actions → Notion automations → ••• → Disable workflow.
Undoing a scheduled run
Download the run's notion-plus-journal-<run id> artifact, unzip it into a folder (it keeps the workspaces/<integration id>/ layout), point NOTION_PLUS_HOME at that folder (in your Claude config, or NOTION_TOKEN=… NOTION_PLUS_HOME=… npm run inspect), and call notion_undo with the undo id from the run summary.
GitHub may start scheduled runs a few minutes late, and turns off schedules in repos with no activity for 60 days.
What Notion itself can't do through the API
Notion's built-in database automations (the ⚡ button in a database) can't be created, read, or changed through the API. Rules here are the programmable alternative; they poll rather than react instantly.
Webhooks can push changes the moment they happen, but Notion needs a public HTTPS URL to deliver them, and a local server doesn't have one. A future option: a small hosted endpoint (or a tunnel such as Cloudflare Tunnel) that receives the webhook and triggers the workflow with
workflow_dispatch, so rules run within seconds instead of within the hour.
Modes
Mode | What runs it | Rules | State |
Interactive | Claude Desktop, Cowork, or Claude Code, through this server | Edited with | Local state folder |
Local scheduled |
| Same file as interactive | Same folder |
GitHub runner |
| The committed |
|
Webhook |
| Workflows in your home folder (or | The server's |
Workflows
Workflows are version 2 of automations: a trigger and a list of steps that run in order, with outputs that later steps use. Manage them with notion_workflow (save, run, runs, run_status, resume, cancel, convert, …); they're kept in workflows.json in the home folder (NOTION_PLUS_WORKFLOWS moves it; the GitHub workflow uses the repo's automations/workflows.json). Rules keep working, and convert turns a rule into an equivalent workflow (saved disabled, so you can check it first).
{
"version": 2,
"id": "launch-review",
"trigger": { "webhook": { "events": ["page.properties_updated"], "database": "https://www.notion.so/…" } },
"steps": [
{ "id": "ready", "query": { "database": "https://www.notion.so/…", "where": { "Status": "Ready for review" } } },
{ "id": "each", "foreach": { "items": "${steps.ready.rows}", "as": "row", "steps": [
{ "id": "ask", "approval": { "page": "${row}", "property": "Approved", "message": "Approve ${row.title} for launch?" } },
{ "id": "ship", "set": { "page": "${row}", "values": { "Status": "Launched", "Launched on": "${today}" } } },
{ "id": "tell", "slack": { "webhook": "${secret:launch_slack}", "text": "Launched: ${row.title} ${row.url}" }, "retry": { "attempts": 3 } }
] } }
],
"on_failure": [{ "comment": "https://www.notion.so/…ops-page" }]
}Triggers: manual (always available through run), schedule (same forms as rules, in the server's time zone), and webhook (Notion events, optionally only for one database).
Steps: each has an id, and may have if (a ${…} reference that must be set and not empty), retry (attempts, backoff_seconds), timeout_seconds, and continue_on_error. Actions:
Notion:
query(rows withid,url,title, and every property),set,append,comment,create_page,move_page,duplicate_page,trash,replace_text,export_markdown,render_template.Outside:
http(to hosts listed inNOTION_PLUS_HTTP_ALLOW, comma-separated,*.example.comallowed) andslack(an incoming-webhook URL, best kept as a secret).Control:
foreach(over a list,${item}or theasname),switch(cases by value),approval(pauses until a checkbox is checked or a comment containing a keyword is posted; optional request comment and expiry),delay(pauses for minutes), andrun_workflow(another workflow, with inputs).
References: ${steps.<id>.<path>}, ${trigger.*} (for webhooks: event, page_id, entity, data), ${inputs.*}, ${today}, ${now}, and ${secret:NAME}, read from the environment variable NOTION_PLUS_SECRET_NAME. Secret values are replaced with *** everywhere a run is stored or shown.
Durable runs. Every step's state is saved as it changes. A run that fails, pauses, or is interrupted continues after the last finished step (resume, or the next scheduler pass). A step that started but didn't finish is retried as a resume: creating steps first look for what the interrupted attempt already made (a page with the same title, a comment with the same text, appended blocks created since it started) and adopt or remove it, so retries don't duplicate content; HTTP steps send an Idempotency-Key header for the same reason. (Rules get the same protection for retried appends.) Each run records undo for what it wrote, and on_failure reports failed runs to Slack, an HTTP endpoint, or a page comment. Set NOTION_PLUS_NOTIFY (for example slack:ops_slack,comment:<page>) to be told about failed or partial rule runs too.
Scheduling. Each npm run automations (hourly in GitHub Actions) also starts scheduled workflows that are due and continues waiting runs whose approval or delay has come. Run state is saved on the notion-automations-state branch with the rules' state.
Webhook mode
npm run webhook (or the Dockerfile) runs a small server that reacts to Notion events within seconds instead of within the hour:
Run it somewhere Notion can reach over HTTPS (behind a reverse proxy, a tunnel such as Cloudflare Tunnel, or a platform that terminates TLS), with
NOTION_TOKEN, a persistentNOTION_PLUS_HOME(the container uses/data), and optionallyNOTION_PLUS_WEBHOOK_PORT(default 8787).In the integration's settings, add a webhook subscription to
https://<your host>/notion/webhook. Notion sends a verification token; the server logs it. Paste it into Notion to verify, and setNOTION_PLUS_WEBHOOK_TOKENto it (restart).Give workflows a
webhooktrigger.
Every delivery must carry a valid X-Notion-Signature (HMAC-SHA256 of the body with that token). Events are accepted once (by id, remembered for a week), only if they're less than 10 minutes old, and acknowledged right away; a worker then starts the matching workflows, retrying with backoff, and moves an event that fails five times to dead letters. notion_workflow events lists the queue, replay retries an event, and discard drops one. GET /healthz reports queued and dead events. The same worker continues waiting runs and starts scheduled workflows every minute, so hourly polling becomes a fallback.
Local state
Everything the server remembers lives under ~/.notion-plus (NOTION_PLUS_HOME moves it), in a folder per integration: workspaces/<integration id>/, with journal.json (undo history), charts.json (chart recipes), automation-state.json (which scheduled occurrences fired), and automation-runs.jsonl (run log). Two integrations sharing a machine never mix their history. NOTION_PLUS_WORKSPACE sets the folder name instead of looking up the integration; NOTION_PLUS_STATE moves just the automation state file. The first time a folder is created, state from older versions (kept directly in ~/.notion-plus) is copied into it.
These files are safe to share between several servers at once (Claude Desktop, Cowork, Claude Code, the scheduled runner): each change is made under a lock, written to a temporary file, flushed, and swapped in, with the previous version kept as <file>.bak. If a file can't be read (cut off by a crash or a full disk, edited by hand into invalid JSON, or not readable), the server stops with an error naming it instead of treating it as empty, since that would lose undo history or repeat scheduled automations. Fix the file, restore the .bak, or move it away to start fresh. A leftover <file>.lock from a process that died is cleared automatically.
Development
npm test # offline unit and property tests with vitest (no network)
npm run test:coverage # the same, failing if coverage drops below the thresholds in vitest.config.ts
npm run lint # ESLint (typescript-eslint strict)
npm run typecheck # src, scripts, and tests
npm run check # lint, typecheck, tests with coverage, build; CI runs this on Node 22 and 24
npm run check:version # package.json, manifest.json, package-lock.json, and CHANGELOG.md agree
npm run test:live # live integration suite against a real workspace
npm run bundle && node scripts/bundle-smoke.mjs # build this platform's bundle and install-test itCI also fails on known high-severity advisories in runtime dependencies, reviews new dependencies in pull requests (on public repos, or private ones with GitHub Code Security once the repository variable DEPENDENCY_REVIEW is true), and Dependabot proposes updates weekly. Every GitHub Action is pinned to a commit. A release builds a bundle on each platform, install-tests it (unpack, start, list every tool, render a chart), and publishes it with sha256sums.txt, an SBOM, and build provenance.
Live tests in CI. .github/workflows/live.yml runs the smoke and acceptance suites nightly and on release tags once two repository secrets exist: NOTION_TOKEN (an integration used only for testing, in a throwaway workspace) and NOTION_TEST_PAGE (a page shared with it). Until then it passes with a notice. It never runs for pull requests, so forks can't reach the secrets.
Evaluations and acceptance tests
evals/notion-plus.xmlholds 10 realistic, read-only questions (in the MCP evaluation format) that each need several tools to answer: aggregation, relations, schema, views, page content, and comments.npm run eval:seedbuilds the fixed dataset they're written against under your test page;npm run eval:verifyanswers every question through the tools and checks the expected answers. To measure how well a model picks tools, run the questions with the MCP evaluation harness against this server.npm run acceptanceruns the seven acceptance scenarios (project tracker, views, report page, find/replace with preview and undo, the At Risk and Completed Date automations, and a 300-row bulk update with undo) and leaves the results under an "Acceptance " page.
npm run test:live (also npm run smoke) needs NOTION_TOKEN and NOTION_TEST_PAGE (a page shared with the integration), from the environment or a local .env file (gitignored). It creates two throwaway databases under that page, runs every tool including dry runs and notion_undo for each write type, then moves them to the trash. It never writes outside the test page, and its undo journal goes to a temp folder. Set SMOKE_KEEP=1 to keep the databases for inspection.
Versions follow semver; see CHANGELOG.md and, for supported versions and deprecations, SUPPORT.md.
Roadmap
Built in phases, each ending with the build, unit tests, and the live suite passing:
Foundations: tests, lint, CI, timeouts, partial-failure reporting, safer undo. (done, 0.2.0)
Content: every creatable block type, rich text colors and mentions, markdown round trip, move/copy/duplicate, icons, covers, templates, comments. (done, 0.3.0)
Databases: create with full schemas, schema editing (including status options), every property type, aggregation, bulk create. (done, 0.4.0)
Views and visuals: views including native chart views, generated chart images, Mermaid, report pages. (done, 0.5.0)
Automations: schedules, run state, more actions, full management from Claude. (done, 0.6.0)
Distribution: MCP Bundle, tool evaluations, acceptance tests, API version 2026-03-11. (done, 0.7.0)
Next: hosted team mode (multi-user, OAuth, a shared database); see docs/hosted-design.md.
Available Tools
33 toolsnotion_aggregateAggregate RowsARead-onlyIdempotent
Answer "how many / how much by X" without reading every row into context. Filters with where/filter (same as notion_query), groups by any property (multi-select, people, and relations count a row in each of its groups; dates can be bucketed by day/week/month/quarter/year), and computes metrics: count, count_values, count_empty, distinct, sum, avg, min, max, median, checked, percent_checked (e.g. ["count", "sum:Points"]). Returns totals and the top groups.
Inputs: database (required), top, sort, where, +5 more.
notion_automationAutomationsADestructive
Manage and run automation rules (rules.json in the local state folder, or NOTION_PLUS_RULES). A rule has a when condition (same where/filter as notion_query, plus relative dates), a schedule ("weekdays 09:00", "daily 18:00", "hourly", cron), or both. Row actions apply to each matching row: {set: {...}}, {append: markdown}, {comment: text}, {trash: true}. then actions run once per firing: {refresh_chart: block_id}, {build_report: {...notion_build_report args}}, {create_page: {parent, title, …}}. Strings accept {{today}}, {{now}}, {{page.}}. A rule must stop matching a row after acting on it (write a value outside its condition, trash, or use marker). Actions: list, get, add, update (merge fields), validate (checks against the live schema and previews), dry_run (never writes), enable, disable, delete, run (writes; one undo_id per rule), history (recent runs, paged), waive (mark a rule's unfinished firing as handled), export / import (the rules file as JSON), deploy (the rules file plus steps to run it on a schedule in GitHub Actions). Each row's actions run in order with progress saved after each step: values that keep the row matching, then content and comments, then the marker or values that take it out of the condition, then trash. A failed row is retried next run from the step that failed, and a scheduled occurrence only counts as fired when all of it succeeded. run reports status succeeded, partial, or failed, and is an error unless every rule succeeded. list and get return the file's revision; pass it as expected_revision on edits so a change made elsewhere in the meantime isn't overwritten.
Inputs: action (required), rule, force, limit, +10 more.
notion_build_reportBuild Report PageA
Build a report page for a database in one call: a summary callout (row count, completion, overdue), KPI numbers side by side, charts (live Notion chart views where Notion supports the type, rendered images otherwise), a table of key rows (e.g. overdue items), and a Mermaid Gantt chart of dated work. Every section is optional; where scopes the whole report. Image charts can be refreshed later with notion_create_chart. notion_undo trashes the page.
Inputs: parent (required), database (required), kpis, gantt, +6 more.
notion_bulk_createBulk Create RowsA
Create up to 1000 database rows from JSON objects (rows) or CSV (csv text or csv_path). Column/key names match properties forgivingly; values follow notion_update_properties rules (relations accept related rows' titles). Empty CSV cells are left unset. Every row is validated before anything is written, and all problems are reported by row. Defaults to dry_run=true. Rows that fail while writing are reported; the rest stay. One notion_undo trashes every created row.
Inputs: database (required), csv, rows, dry_run, +3 more.
notion_bulk_updateBulk Update RowsADestructiveIdempotent
Update many rows at once, in one of two ways: set the same values on every row matching where/filter (same syntax as notion_query, including operators like {"Due": {"before": "today"}}), or give rows: [{page, set}] to set different values per row. Up to 500 rows. Defaults to dry_run=true, which returns the rows and their current values so you can confirm first. Values are validated before anything is written; every changed row is snapshotted, failures are reported per row, and the whole batch reverts with one notion_undo.
Inputs: set, rows, limit, where, +5 more.
notion_capabilitiesCapabilitiesARead-onlyIdempotent
What this server can do with the current integration: the areas it covers, the Notion API version, limits that matter for planning (rows copied, blocks per copy, run limits), and which integration capabilities are available (probed with read-only calls; write capabilities show as unknown until a write).
Inputs: no named parameters.
notion_commentsCommentsA
Read, add, or reply to comments. action=list returns a page's or block's open comments with discussion ids (Notion's API doesn't return resolved threads). action=add starts a discussion on a page or block; action=reply answers a discussion by discussion_id. text is markdown (bold, links, mentions such as ). Added comments can be removed with notion_undo; an integration can only delete its own comments.
Inputs: action (required), text, limit, target, +1 more.
notion_copy_blocksCopy or Move BlocksADestructive
Copy blocks (with everything nested inside them) to another spot: a page or block, at the start, end, or after a block. With move=true the originals are trashed afterward (the API has no move, so moved blocks get new ids and lose their comments). Sub-pages among the given blocks are moved with Notion's page move, or duplicated when copying; they land at the end of the destination page. A move is refused if it would lose something the API can't recreate (databases, read-only blocks, original synced blocks whose references would break). Copying an original synced block creates a reference to it. Moves default to dry_run=true. One notion_undo reverts everything.
Inputs: to (required), block_ids (required), move, dry_run, +2 more.
notion_create_chartCreate or Refresh Chart ImageA
Render a chart image (PNG, or SVG) and put it on a page, or refresh an existing chart image in place. Types: bar, column, stacked_bar, stacked_column, grouped_column, line, area, stacked_area, pie, donut, scatter, and histogram (one row per observation; y is the value (x is ignored). bins sets the number of bins.); heatmap (x = column, series = row, y = the cell's value.); boxplot (one row per observation; x = group, y = value.); waterfall (rows in order; y = change. A row with series "total" shows the running total instead.); funnel (rows in stage order; y = how many reached the stage.); bullet (per x: a row with series "actual" (or none) and a row with series "target".); small_multiples (x and y as for a line chart; one small panel per series.); dual_axis (exactly two series: the first as columns (left axis), the second as a line (right axis).); treemap (x = item, y = size; optional series groups items (one color per group).); sankey (one row per flow: x = from, series = to, y = amount. Flows must only go forward (no loops).); network (one row per link: x = one end, series = the other, y = weight (line thickness).). Data is inline data ([{x, y, series?}]) or a source query that groups a database (x, y like "count" or "sum:Estimate", optional series). annotations add labeled reference lines; palette picks colors (default is a validated colorblind-safe order). data_table adds a toggle under the chart with a description and the numbers, for readers who can't see the image. For anything the types don't cover, pass a full Vega-Lite spec as vega_lite (data inline only; nothing is fetched). The recipe is remembered, so refresh_block_id re-renders it from current data. Prefer notion_views chart views when a live Notion chart is enough. Reversible with notion_undo.
Inputs: data, chart, format, parent, +8 more.
notion_create_databaseCreate DatabaseA
Create a database under a page with its full schema in one call: select/multi-select/status options (status options can name their group: To-do, In progress, Complete), number formats, formulas, relations (one- or two-way, to another database or "self"), rollups over those relations, unique IDs with a prefix, people, dates, files, places, and more. If no title property is given, one called "Name" is added. Add rows with notion_bulk_create or notion_create_page. notion_undo trashes the database.
Inputs: title (required), parent (required), properties (required), icon, +3 more.
notion_create_pageCreate PageA
Create a page. If parent is a database, properties are validated against its schema (same rules as notion_update_properties) and title fills the title property; template applies one of the database's templates (see notion_list_templates). If parent is a page, a sub-page is created. Content can be markdown or structured blocks (same formats as notion_insert_blocks). icon is an emoji, image URL, or local image path; cover is an image URL or local path.
Inputs: parent (required), icon, cover, title, +7 more.
notion_delete_blocksDelete BlocksADestructive
Move specific blocks (by id) to the trash. Nothing else on the page is touched. Reversible with notion_undo.
Inputs: block_ids (required).
notion_doctorCheck SetupARead-onlyIdempotent
Check that the server is set up and working, and say how to fix what isn't: settings, the token, the integration's capabilities, whether any pages are shared with it, the local state folder (undo journal, chart recipes, automation state), upload folders, the chart renderer, automation rules, and unfinished automation runs. Changes nothing. Run it first when something doesn't work.
Inputs: offline.
notion_duplicate_pageDuplicate PageA
Copy a page with its icon, cover, content (every block type the API can create, with formatting), and, when it stays in the same database, all writable properties. Sub-pages and databases on the page are copied in place; databases get their schema and (by default) their rows with content, with relations inside the copy pointing at the copied rows. Views, linked database views, and read-only blocks are listed as skipped, and the result's status is "partial" when anything was. Database rows keep their own content, sub-pages, and databases. Uploaded files and images are re-uploaded so the copy doesn't depend on expiring links. dry_run shows what would be copied. notion_undo trashes the copy.
Inputs: page (required), to, title, dry_run, +5 more.
notion_find_blocksFind BlocksARead-onlyIdempotent
Find blocks on a page whose text matches a string or regex. Returns block id, type, text, and last_edited_time. Use this to locate exactly which block to edit instead of rewriting the page.
Inputs: page (required), query (required), regex, max_depth, +2 more.
notion_get_blocksGet Block TreeARead-onlyIdempotent
Read the children of a page or block as an outline with block ids. Use for large pages to read one section at a time.
Inputs: block (required), max_depth, max_blocks.
notion_get_pageGet PageARead-onlyIdempotent
Read a page: its properties as simple values, the database it belongs to (if any), and its content. format "outline" (default) lists every block with its id in ⟨…⟩, for surgical edits with notion_patch_block, notion_insert_blocks, notion_delete_blocks, notion_copy_blocks. format "markdown" returns the whole page as Notion-flavored markdown (colors, callouts, columns, tables, toggles, mentions), which notion_insert_blocks / notion_create_page accept back. last_edited_time can be passed to write tools as expected_last_edited_time to avoid overwriting someone else's edit.
Inputs: page (required), format, max_depth, max_blocks, +1 more.
notion_get_schemaGet Database SchemaARead-onlyIdempotent
Read a database's properties: names, types, select/status options (with status groups), relation targets, and which are read-only. Read this before writing properties so values match exactly.
Inputs: database (required), data_source_name.
notion_historyChange HistoryARead-onlyIdempotent
List changes made through this server, newest first, with their undo ids and how much of each undo can revert. Entries marked interrupted or failed stopped part-way; their input is included so the objects can be checked.
Inputs: limit, cursor.
notion_insert_blocksInsert BlocksA
Insert new content at an exact spot: at the start or end of a page/block, or directly after a specific block id. Existing content is never rewritten. markdown accepts headings (# to ####), lists and to-dos nested by indentation to any depth, quotes, code (mermaid for diagrams), $$ equations $$, pipe tables, > [!NOTE]/[!TIP]/[!WARNING] callouts, and Notion's markdown tags as returned by notion_get_page format=markdown (, , , , , , mentions, {color="…"}). Or give structured blocks (tables via rows, columns, tabs, images/files from a URL or local path, bookmarks, embeds, equations, synced blocks, link_to_page, table_of_contents, breadcrumb). Everything is validated and uploaded before the first write. Returns the new block ids.
Inputs: parent (required), blocks, markdown, position, +1 more.
notion_list_templatesList Database TemplatesARead-onlyIdempotent
List a database's page templates (name, id, which is the default). Use a name or id as notion_create_page's template.
Inputs: database (required), data_source_name.
notion_patch_blockPatch One BlockAIdempotent
Edit a single block in place by id without touching anything else on the page: replace its text (inline markdown, colors, mentions allowed), check/uncheck a to-do, set a code block's language or caption, change color, make a heading a toggle, change a callout's icon, or replace a table row's cells. Block type can't change (Notion API limit); to change type, insert a new block and delete the old one. Snapshots the old content for notion_undo.
Inputs: block_id (required), icon, text, cells, +6 more.
notion_queryQuery DatabaseARead-onlyIdempotent
Query database rows. where takes equality ({"Status": "Done"}), operators ({"Due": {"before": "today"}}, {"Estimate": {">": 500}}, {"Task": {"contains": "x"}}, {"Owner": null}), lists ({"Status": {"in": ["Done", "Blocked"]}}), {"or": [...]}, relative dates ("today", "+7d", "-2w"), and "$created"/"$last_edited"; names and options are matched forgivingly and validated. filter takes raw Notion filter JSON; both are combined with AND. Returns rows with simple values and page ids. For counts or totals use notion_aggregate instead of reading every row.
Inputs: database (required), limit, sorts, where, +4 more.
notion_replace_textFind and Replace TextA
Find and replace text across a page while keeping bold/italic/links/colors and mentions intact: every text block, table cell, and code caption, plus the page title (include_title). Supports regex with $1-style groups. Defaults to dry_run=true, which shows every change (before/after) so you can confirm. Matches that cross a mention or equation are skipped and reported. Sub-pages are separate documents and aren't searched. All changes revert together with notion_undo.
Inputs: find (required), page (required), replace (required), regex, +5 more.
notion_schemaEdit Database SchemaADestructive
Change a database's properties. action=add adds a property (same definition as notion_create_database). action=rename renames one. action=delete removes one: it defaults to a dry run showing how many rows have values, and snapshots up to 2000 rows' values so notion_undo can re-create the property and restore them. action=add_options adds select/multi-select/status options (status options take a group). action=set_number_format and action=set_description change those settings. Notion's API can't rename or recolor options (it ignores or rejects it); rename them in Notion, or add a new option, move rows with notion_bulk_update, and delete the old one in Notion.
Inputs: action (required), database (required), to, dry_run, +6 more.
notion_searchSearch NotionARead-onlyIdempotent
Search pages and databases shared with the integration by title. Returns id, type, title, url, and last edited time. Use this to find ids before reading or editing. Only content shared with the integration is searchable.
Inputs: query (required), type, limit.
notion_templatePage TemplatesA
Build pages from reusable templates instead of many block calls. Built-in templates: weekly-executive-report, content-brief, launch-plan, research-dossier; save your own. Actions: list, get (a template's full spec, a good starting point), validate (check a template, and with variables that it compiles), preview (the page outline, block count, charts, views, estimated requests, and, with compare_to, what differs from an existing page; writes nothing), render (create the page under parent, or append to append_to; charts are rendered and uploaded, live views placed; notion_undo removes it), save, delete, export (your saved templates), import. A template is {version: 1, name, description?, variables: {name: {type: string|number|boolean|date|list|object, required?, default?, description?}}, parts?: {name: [nodes]}, title, icon?, blocks: [nodes]}. Strings use {{variable}} (also {{item.field}} in loops, {{item_index}}, {{today}}, {{now}}); a string that is exactly one {{variable}} passes lists and objects through. Nodes: {markdown}, {heading, level?}, {callout, icon?, color?}, {toggle, children}, {columns: [[nodes], …]}, {divider: true}, {kpis: [{label, value} | {label, metric: {database, value: "count"|"sum:Prop", where?}}]}, {chart: {spec (as notion_create_chart), data | source, data_table?, format?}}, {view: {database, view (as notion_views)}} (top level only), {table: {header?, rows} | {query: {database, properties?, where?, limit?}}}, {gantt: {title?, tasks}}, {each: "list", as: "item", blocks}, {if: "var" | {var, equals|not|in|gt|lt|empty}, then, else?}, {part: name, with?}, {slot: name, default?} (filled by slots), {blocks: [raw block specs]}.
Inputs: action (required), name, slots, parent, +5 more.
notion_trash_pageMove Page to TrashADestructive
Move a page or database row to the Notion trash. Reversible with notion_undo or from Notion's trash.
Inputs: page (required).
notion_undoUndoADestructive
Revert a change made through this server. With no id, reverts the most recent change that hasn't been undone. Use notion_history to find ids. Undo restores the snapshot taken at write time. Before writing, it checks whether anything it would restore was edited later (by a person or a later change here) and refuses, listing those objects, unless force is true. Edits made within the same minute as the original write can't be detected (Notion rounds edit times to the minute).
Inputs: force, undo_id.
notion_update_pageUpdate PageAIdempotent
Change a page's title, icon, or cover, lock or unlock it, or move it under another page or into a database. Icons: an emoji, image URL, or local image path; covers: an image URL or local path; "none" removes either. For database row fields use notion_update_properties. Reversible with notion_undo (an icon or cover that was an uploaded file is uploaded again on undo).
Inputs: page (required), icon, cover, title, +4 more.
notion_update_propertiesUpdate Page PropertiesAIdempotent
Set properties on a database row using simple values: strings, numbers, booleans, arrays for multi-select/people/relations, ISO dates or {start,end}, null to clear. Property names and option values are matched forgivingly and validated against the schema before anything is written; all problems are reported together. Saves a snapshot so notion_undo can revert. Set dry_run to preview the before/after.
Inputs: page (required), properties (required), dry_run, allow_new_options, +1 more.
notion_viewsDatabase ViewsADestructive
List, read, create, update, or delete database views. Types: table, board, list, calendar, timeline, gallery, form, map, and chart (Notion's native, live charts: column, bar, line, donut, number, with stacking). A view can be a tab on the database (default), a linked view placed on any page (on: {page, after_block}), or a widget on a dashboard view (on: {dashboard}). Views take where/filter like notion_query, sorts, group_by (e.g. board by Status, dates by week), visible properties, and a chart spec like {type: "column", x: "Owner", y: "count"} or y: "sum:Estimate". Every change is reversible with notion_undo.
Inputs: action (required), on, view, limit, +4 more.
notion_workflowWorkflowsADestructive
Multi-step workflows (version 2 of automations): a trigger (manual, schedule, or Notion webhook events) and typed steps run in order. Step actions: query, set, append, comment, create_page, move_page, duplicate_page, trash, replace_text, export_markdown, render_template, http, slack, foreach, switch, approval, delay, run_workflow. Each step has an id; may have if (a ${…} reference), retry {attempts, backoff_seconds}, timeout_seconds, and continue_on_error. Steps read earlier outputs with ${steps..} (query returns rows with id, url, title, and each property), the trigger with ${trigger.}, inputs with ${inputs.}, loop items with ${item} (or the foreach as name), and secrets with ${secret:NAME} (environment variable NOTION_PLUS_SECRET_NAME; never stored or shown). approval pauses until a checkbox is checked or a comment with a keyword is posted; delay pauses for minutes; both resume on the next scheduler pass. Runs are durable: a crash, failure, or pause resumes after the last finished step, and creating steps check what an interrupted attempt already made, so nothing is created twice. http and slack steps only reach hosts in NOTION_PLUS_HTTP_ALLOW (Slack's hook host is always allowed for slack). on_failure notifies Slack, an HTTP endpoint, or a page comment. Actions: list, get, save, delete, validate, run (now, with inputs), runs (history), run_status, resume (continue a waiting or failed run), cancel, convert (a v1 rule into a workflow, saved disabled), events / replay / discard (the webhook queue and its dead letters). Webhook events: page.created, page.content_updated, page.properties_updated, page.moved, page.deleted, page.undeleted, comment.created, data_source.content_updated, data_source.schema_updated.
Inputs: action (required), id, limit, inputs, +4 more.
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
Loading tool schema changelog…
TDQS
Scored across 33 tools
Most tools have distinct resource+action boundaries, and descriptions actively steer the agent (e.g. prefer notion_views for live charts, use notion_aggregate for counts). However, a few areas overlap: notion_automation vs notion_workflow, notion_template vs notion_list_templates, and notion_doctor vs notion_capabilities. These are clarified by descriptions but still require careful reading.
All tools use snake_case with a consistent notion_ prefix, which is predictable and readable. Most names are verb_noun (get_page, create_page, update_properties), but several are noun-only or action-packed (comments, views, schema, undo, history, doctor, capabilities, automation, workflow, template), and singular/plural use varies slightly.
33 tools is above the 25+ threshold considered too many for a well-scoped server. While Notion is broad and many tools earn their place, the surface is heavy and includes advanced features (automation, workflows, templates, charts, reports) that could be consolidated or exposed as sub-actions. This increases selection overhead and the risk of misselection.
The server covers an unusually full Notion lifecycle: search, page/block read and write, database schema and row CRUD, bulk operations, comments, views, charts, templates, automations, and undo/history. Minor gaps remain: no user-directory tool for people properties, no restore for pages trashed outside this server, and comment deletion is only available indirectly via notion_undo. These are workaroundable but notable.
Maintenance
Related MCP Connectors
MCP-native open-source Notion alternative: read & write pages, databases and kanban boards.
Hosted Notion MCP server for Claude, ChatGPT and Cursor, with each action switched per account.
Shared tables and Markdown docs for teams and AI-built apps, with scoped access and audited changes.
Build and change Discord bots by describing them: slash commands, events, embeds, modals and data.
Related MCP Servers
- AlicenseNot gradedqualityAmaintenanceGives AI agents token-authenticated read and write access to Notion, letting them create and edit pages, query databases, append blocks, apply templates, comment, and upload files in plain language. It runs headless for CI and background agents while keeping tool schemas and responses small to conserve context.5,643 npmMIT
- AlicenseNot gradedqualityAmaintenanceEnables AI agents and MCP clients to read and write a Notion workspace through a single token, covering pages, databases, blocks, comments, templates, and file uploads. It keeps the model's context small by exposing three tools and fetching operation schemas on demand, while supporting batched, idempotent mutations and headless use in CI or self-hosted deployments.5,643 npmMIT
- AlicenseNot gradedqualityAmaintenanceGives MCP clients such as Claude, Cursor, VS Code and Codex token-authenticated read/write access to a Notion workspace, so agents can search, create and edit pages, query databases, manage blocks and comments, and upload files in plain language. It is built for unattended automation with batched, idempotent writes, markdown round-tripping, retry on rate limits, and a low context footprint.5,643 npmMIT
- AlicenseNot gradedqualityAmaintenanceConnects MCP clients such as Claude, Cursor, VS Code and Codex to Notion with a single token so agents can search, create and edit pages, query databases, manage blocks and comments, and upload or read files in plain language. Designed for unattended agents and CI, it offers batched idempotent writes, markdown round-tripping, rate-limit retries and low context cost.5,643 npmMIT