The trade is explicit: you carry two definitions instead of dozens, and in exchange the catalogue is not readable from the tool list — you find it through the describe tool, the operations resource, and the schema attached to an error. Batching is where the shape pays off, since one call can update a list of items with an idempotency key and a concurrency setting rather than issuing them one at a time. Before pointing it at a real workspace, decide what it may do: read-only is one environment variable, and the operation allow list is the finer-grained version of the same control.
A server that puts Notion behind two tools — an executor that takes an operation name and a payload, and a describe tool that returns the JSON Schema and a worked example for one operation. It authenticates with a token rather than OAuth, so it runs unattended.
- 43 operations across pages, blocks, databases, data sources, views, comments, users and files, invoked by name with a payload
- The same operation run across a list of items in one call, with atomicity, an idempotency key and a concurrency setting — a rename of fifty pages is one call rather than fifty
- Markdown accepted where blocks are expected, and a full round trip: read a page as markdown, edit it, write it back
- Database queries with a typed filter shorthand and rows flattened to name-to-value maps rather than Notion's raw property bag
- The JSON Schema and a working example for an operation, and the same pair attached to a validation error, so a malformed payload is corrected in the turn that made it
- Rate limiting and retry built in, honouring Notion's own limits and backing off on 429s
- Access control without a code change: one switch for read-only, and allow or block lists by operation name or by group — read, write, destructive, or a single family such as comments
- Notion content pulled in as MCP resources — a page as markdown, a data source schema as JSON — for clients that support attachment
A Notion token in NOTION_TOKEN: a personal access token, which sees everything you can see and expires a year after creation, or an internal integration secret, which only sees pages you have explicitly connected it to — most "could not find" errors are that second case. NOTION_PAGE_ID sets a default parent for new pages. It runs over stdio from npx or the published Docker image; an HTTP mode exists and is single-tenant, so everyone reaching it acts as that one token — bind it to loopback or put an auth token in front of it. MIT.
One command plus a key — npx -y notion-mcp-server, then supply credentials
