Notion
Proxy for the Notion REST API through the managed gateway: search, database schema + queries, page read/write, and page content (blocks). All operations are gated by PBAC policy.
The Notion-Version header
Every Notion API call requires a fixed Notion-Version header alongside
Authorization, or Notion rejects the request. Notion's own OAuth
passthrough model has no way to attach that extra header per call, so this
connector doesn't offer idp_passthrough — Notion's per-user integration
tokens (see below) already give the same "act as this specific identity"
result without needing a separate IdP.
Finding a database's real id
A "database" in the Notion UI is often a linked view of a source
database that lives elsewhere — connecting the integration on a linked view
does not grant access to the source it points at, and get_database/
query_database will 400 against the view's own id ("does not contain any
data sources accessible by this API bot"). Likewise, an inline database
embedded in a page has its own id, distinct from the page's id — calling
get_database with a page's id 400s with "is a page, not a database."
If search with filter_type: "database" doesn't find what you're looking
for, use get_block_children on a page you know is shared: an inline
database shows up in the results as a child_database block, whose id is
the real database id to use. A newly-shared or newly-created database can
also take a short while to appear in search results even once access is
confirmed — direct get_database/query_database calls by id aren't
affected by that lag.
Authentication
Notion authenticates REST calls with an integration token. This connector supports two auth modes, chosen per managed resource at install time via the Authentication field:
- Shared integration token (
static, default) — every call uses one operator-held token from thetoken_envsecret, withNotion-Versionsent alongside it automatically. This matches how Notion access actually works: an integration is shared onto specific pages/databases from each page's ••• menu → Connections, regardless of which person is calling through it, and creating an integration in the first place typically needs workspace-admin permission — not something every individual user can self-serve the way a GitHub or Gitea token is. - Per-user integration token (
user_secret) — each user links their own Notion integration token once, through a PolicyArc-hosted browser page reached by MCP URL elicitation on the first call. Use this when per-person attribution matters more than the shared-token default, and when each person can create their own integration.
Either way, PolicyArc enforces per-user authorization at the gateway via
policy before the request reaches Notion. See
Per-user upstream credentials for how the user_secret
collection flow works.
Setup
upstream_auth.type—static(default) oruser_secret.token_env— static mode only — a secret holding a Notion internal integration token. Hidden whenuser_secretis selected.
Creating a Notion integration token
notion.so/my-integrations → New integration → give it the capabilities
this connector needs (read content, update content, insert content), then
copy its token. Then, on each page or database it should reach, open the
page's ••• menu → Connections → connect the integration — an
integration with no pages shared to it can authenticate but sees nothing. In
user_secret mode each user does this for themselves (their own integration,
shared onto whatever they want it to reach) on the collection page; in
static mode the operator provisions the shared integration once.
Manifest reference
- ID:
identos.notion - Version:
1.0.0 - Resource type:
urn:connector:identos:notion - Capabilities:
mcp
Supported auth modes
| Type | Details |
|---|---|
static | scheme bearer; setup fields: token_env |
user_secret | — |
Setup fields
| ID | Label | Default | Secret? | Notes |
|---|---|---|---|---|
upstream_auth.type | Authentication | static | no | Notion's own sharing model is workspace-level, not per-user: one integration is shared onto specific pages/databases regardless of who calls it, and creating an integration itself typically needs workspace-admin permission. static (default) matches that with one shared integration token. user_secret instead has each user link their own integration token, for when you want per-person attribution and each person can create their own integration. |
token_env | Integration token | — | yes | Used only for the static method. A secret holding a Notion internal integration token, created at notion.so/my-integrations and shared onto the pages/databases it should reach. / shown when upstream_auth.type == 'static' |
Scopes
| Scope |
|---|
notion:read |
notion:write |
Routes
| Method | Pattern | Scope | Resource template |
|---|---|---|---|
POST | /search | notion:read | — |
GET | /databases/{database_id} | notion:read | notion://databases/{{database_id}} |
POST | /databases/{database_id}/query | notion:read | notion://databases/{{database_id}} |
GET | /pages/{page_id} | notion:read | notion://pages/{{page_id}} |
POST | /pages | notion:write | — |
PATCH | /pages/{page_id} | notion:write | notion://pages/{{page_id}} |
GET | /blocks/{block_id}/children | notion:read | notion://blocks/{{block_id}} |
PATCH | /blocks/{block_id}/children | notion:write | notion://blocks/{{block_id}} |
MCP tools
| Name | Scope | Description |
|---|---|---|
search | notion:read | Search pages and databases the integration has been shared with, by title. |
get_database | notion:read | Get a database's schema (properties/columns) and metadata. |
query_database | notion:read | Query a database's rows (pages), with optional filter and sort. |
get_page | notion:read | Get a page's properties (not its body content — use get_block_children for that). |
create_page | notion:write | Create a page, either as a database row (parent.database_id) or as a subpage of another page (parent.page_id). |
update_page | notion:write | Update a page's properties, or archive/restore it by setting archived. |
get_block_children | notion:read | Get a page's or block's body content as a list of blocks. Pass a page_id as block_id to read a page's top-level content. |
append_block_children | notion:write | Append new content blocks to the end of a page or block. Pass a page_id as block_id to add to a page's top-level content. |
Send feedback about this page