Skip to main content
Glama

Notion Remote MCP Server (OAuth + PKCE)

Remote MCP server that connects to Notion via OAuth and exposes a practical, enterprise-friendly tool surface. It implements Streamable HTTP transport over POST /mcp, MCP-compatible OAuth endpoints, PKCE, token refresh, and encrypted token storage.

Quick Start (5 minutes)

  1. Create a Notion integration

  • Create a public integration in Notion.

  • Add the OAuth redirect URL: http://localhost:8787/oauth/callback

  • Enable capabilities (least-privilege):

    • Read content

    • Update content

    • Insert content

    • Read user info

  1. Configure env

cp .env.example .env

Generate an encryption key and HMAC secret:

node -e "console.log(require('crypto').randomBytes(32).toString('base64'))"

Set:

  • TOKEN_ENC_KEY to the generated base64 key

  • STATE_SIGNING_KEY to another random secret

  • (Optional) TOKEN_ENC_KEY_FILE / STATE_SIGNING_KEY_FILE for file-based secrets (defaults under ./data/)

  • NOTION_CLIENT_ID / NOTION_CLIENT_SECRET

  • BASE_URL (if not localhost)

  • ALLOWED_REDIRECT_URIS for your MCP client

  • NOTION_VERSION (default: 2025-09-03)

  1. Run

npm install && npm run start

Server: http://localhost:8787

OAuth for MCP Clients

This server is an OAuth 2.1 Authorization Server for MCP clients and uses Notion OAuth behind the scenes.

Authorization URL:

GET /authorize?response_type=code&client_id=mcp-cli&redirect_uri=http://localhost:3000/callback&scope=notion.read%20notion.write&state=xyz&code_challenge=...&code_challenge_method=S256

Token endpoint:

POST /token (application/x-www-form-urlencoded)

Supported scopes:

  • notion.read

  • notion.write

  • notion.admin

Token refresh is supported via grant_type=refresh_token.

Dynamic client registration example:

curl -s http://localhost:8787/register \
  -H "Content-Type: application/json" \
  -d '{"client_name":"my-mcp-client","redirect_uris":["http://localhost:3000/callback"],"scope":"notion.read notion.write"}'

MCP Endpoint

  • POST /mcp (Streamable HTTP)

  • GET /mcp returns 405 (only POST is supported)

Headers:

  • Authorization: Bearer <access_token>

  • MCP-Protocol-Version: 2025-11-25 (optional; supported: 2025-11-25, 2025-06-18, 2025-03-26)

  • Accept: application/json, text/event-stream

OAuth metadata:

  • /.well-known/oauth-protected-resource

  • /.well-known/oauth-authorization-server

  • POST /register (dynamic client registration)

Tool Surface

All tools validate inputs with JSON Schema and return JSON-encoded results.

Tool

Scope

Purpose

notion.search

notion.read

Search pages/databases

notion.get_page

notion.read

Retrieve a page

notion.get_database

notion.read

Retrieve a database/data source

notion.query_database

notion.read

Query database/data source rows

notion.create_page

notion.write

Create a page

notion.update_page

notion.write

Update page properties

notion.append_block

notion.write

Append blocks

notion.list_users

notion.admin

Governance: list users

notion.whoami

notion.admin

Governance: integration identity

JSON Schemas

notion.search

Input:

{
  "type": "object",
  "properties": {
    "query": { "type": "string" },
    "filter": { "type": "object", "properties": { "object": { "type": "string", "enum": ["page", "database"] } }, "additionalProperties": false },
    "sort": { "type": "object", "properties": { "direction": { "type": "string", "enum": ["ascending", "descending"] }, "timestamp": { "type": "string", "enum": ["last_edited_time", "created_time"] } }, "additionalProperties": false },
    "page_size": { "type": "integer", "minimum": 1, "maximum": 100 },
    "start_cursor": { "type": "string" }
  },
  "additionalProperties": false
}

Output:

{
  "type": "object",
  "properties": {
    "results": {
      "type": "array",
      "items": {
        "type": "object",
        "properties": {
          "id": { "type": "string" },
          "object": { "type": "string" },
          "url": { "type": "string" },
          "title": { "type": "string" },
          "last_edited_time": { "type": "string" }
        },
        "required": ["id", "object", "url"]
      }
    },
    "next_cursor": { "type": ["string", "null"] },
    "has_more": { "type": "boolean" }
  },
  "required": ["results", "has_more"]
}

notion.get_page

Input:

{
  "type": "object",
  "properties": {
    "page_id": { "type": "string" },
    "include_properties": { "type": "boolean", "default": false }
  },
  "required": ["page_id"],
  "additionalProperties": false
}

Output:

{
  "type": "object",
  "properties": {
    "id": { "type": "string" },
    "url": { "type": "string" },
    "created_time": { "type": "string" },
    "last_edited_time": { "type": "string" },
    "archived": { "type": "boolean" },
    "title": { "type": "string" },
    "properties": { "type": "object" }
  },
  "required": ["id", "url"]
}

notion.get_database

Input:

{
  "type": "object",
  "properties": { "database_id": { "type": "string" }, "data_source_id": { "type": "string" } },
  "anyOf": [{ "required": ["database_id"] }, { "required": ["data_source_id"] }],
  "additionalProperties": false
}

Output:

{
  "type": "object",
  "properties": {
    "id": { "type": "string" },
    "title": { "type": "string" },
    "url": { "type": ["string", "null"] },
    "properties": { "type": "object" }
  },
  "required": ["id", "url"]
}

notion.query_database

Input:

{
  "type": "object",
  "properties": {
    "database_id": { "type": "string" },
    "data_source_id": { "type": "string" },
    "filter": { "type": "object" },
    "sorts": { "type": "array" },
    "page_size": { "type": "integer", "minimum": 1, "maximum": 100 },
    "start_cursor": { "type": "string" }
  },
  "anyOf": [{ "required": ["database_id"] }, { "required": ["data_source_id"] }],
  "additionalProperties": false
}

Output:

{
  "type": "object",
  "properties": {
    "results": { "type": "array" },
    "next_cursor": { "type": ["string", "null"] },
    "has_more": { "type": "boolean" }
  },
  "required": ["results", "has_more"]
}

notion.create_page

Input:

{
  "type": "object",
  "properties": {
    "parent": { "type": "object", "properties": { "database_id": { "type": "string" }, "data_source_id": { "type": "string" }, "page_id": { "type": "string" } }, "additionalProperties": false },
    "properties": { "type": "object" },
    "children": { "type": "array" }
  },
  "required": ["parent", "properties"],
  "additionalProperties": false
}

Output:

{
  "type": "object",
  "properties": {
    "id": { "type": "string" },
    "url": { "type": "string" },
    "created_time": { "type": "string" }
  },
  "required": ["id", "url"]
}

notion.update_page

Input:

{
  "type": "object",
  "properties": {
    "page_id": { "type": "string" },
    "properties": { "type": "object" },
    "archived": { "type": "boolean" }
  },
  "required": ["page_id"],
  "additionalProperties": false
}

Output:

{
  "type": "object",
  "properties": {
    "id": { "type": "string" },
    "url": { "type": "string" },
    "archived": { "type": "boolean" }
  },
  "required": ["id", "url"]
}

notion.append_block

Input:

{
  "type": "object",
  "properties": {
    "block_id": { "type": "string" },
    "children": { "type": "array" }
  },
  "required": ["block_id", "children"],
  "additionalProperties": false
}

Output:

{
  "type": "object",
  "properties": {
    "id": { "type": "string" },
    "has_more": { "type": "boolean" }
  },
  "required": ["id"]
}

notion.list_users

Input:

{
  "type": "object",
  "properties": {
    "page_size": { "type": "integer", "minimum": 1, "maximum": 100 },
    "start_cursor": { "type": "string" }
  },
  "additionalProperties": false
}

Output:

{
  "type": "object",
  "properties": {
    "results": { "type": "array" },
    "next_cursor": { "type": ["string", "null"] },
    "has_more": { "type": "boolean" }
  },
  "required": ["results", "has_more"]
}

notion.whoami

Input:

{
  "type": "object",
  "properties": {},
  "additionalProperties": false
}

Output:

{
  "type": "object",
  "properties": {
    "bot_id": { "type": "string" },
    "workspace_id": { "type": "string" },
    "owner": { "type": "object" }
  },
  "required": ["bot_id"]
}

Examples

List tools:

curl -s http://localhost:8787/mcp \
  -H "Authorization: Bearer $MCP_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'

Search pages:

curl -s http://localhost:8787/mcp \
  -H "Authorization: Bearer $MCP_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"notion.search","arguments":{"query":"Roadmap","page_size":5}}}'

Create a page in a database:

curl -s http://localhost:8787/mcp \
  -H "Authorization: Bearer $MCP_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"notion.create_page","arguments":{"parent":{"database_id":"YOUR_DB_ID"},"properties":{"Name":{"title":[{"text":{"content":"Q1 Plan"}}]}}}}}'

Docker

docker build -t notion-mcp .
docker run --env-file .env -p 8787:8787 notion-mcp

Security Notes

  • OAuth 2.1 + PKCE enforced for MCP clients.

  • Token storage is AES-256-GCM encrypted via TOKEN_ENC_KEY.

  • Access tokens are short-lived; refresh tokens rotate access tokens.

  • Origin allowlist for browser clients via ALLOWED_ORIGINS.

  • Rate limiting: defaults are 120 requests/min for /mcp and 30 requests/min for auth endpoints.

  • If encryption/state keys are not set, they are auto-generated and stored under ./data/ for local dev.

Trade-offs / Next Steps

  • Notion does not document PKCE support for its own OAuth; PKCE is enforced for MCP clients, and upstream Notion OAuth uses standard code exchange.

  • Dynamic client registration stores client metadata in the encrypted store; could add client secrets and approval workflows for stricter control.

  • Tool output is returned as JSON text; could add structured content types once supported.

  • Add rate limiting, audit logs, and per-tenant encryption keys for stronger governance.

  • For Notion API version 2025-09-03, prefer data_source_id for database-like operations; database_id is kept for backward compatibility.

-
security - not tested
F
license - not found
-
quality - not tested

Resources

Unclaimed servers have limited discoverability.

Looking for Admin?

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

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/edenking0621-png/MCP'

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