SuperAPI AI Directory

easy-notion-mcp-integration

Agents & models

View repository

Updated 10-16 02:13

About this skill

Easy Notion MCP Integration

Skill by ara.so — MCP Skills collection.

Overview

easy-notion-mcp is a production-grade Model Context Protocol (MCP) server that bridges AI agents to Notion using GitHub-flavored markdown instead of raw Notion API JSON. It provides 42 MCP tools for reading and writing Notion content with round-trip fidelity across 24 block types including toggles, columns, callouts, tables, and file uploads.

Key Features

  • Markdown-first I/O: Agents work with markdown; server handles all Notion block JSON conversion
  • Database ergonomics: Write simple property objects like { "Status": "Done" } with automatic schema mapping
  • Dual transport: Stdio (API token) or HTTP (OAuth/bearer token)
  • Optional Redis: Shared schema cache and OAuth persistence for multi-instance deployments
  • Security defaults: Content sanitization, URL validation, workspace-root file containment

Installation

Quick Start with npx (Recommended)

Configure in your MCP client (Claude Desktop, Cursor, etc.):

{
  "mcpServers": {
    "notion": {
      "command": "npx",
      "args": ["-y", "easy-notion-mcp"],
      "env": {
        "NOTION_TOKEN": "YOUR_NOTION_INTEGRATION_TOKEN"
      }
    }
  }
}

From Source

git clone https://github.com/Grey-Iris/easy-notion-mcp.git
cd easy-notion-mcp
npm install
npm run build

MCP client configuration:

{
  "mcpServers": {
    "notion": {
      "command": "node",
      "args": ["/absolute/path/to/easy-notion-mcp/dist/index.js"],
      "env": {
        "NOTION_TOKEN": "YOUR_NOTION_INTEGRATION_TOKEN",
        "NOTION_ROOT_PAGE_ID": "optional_default_parent_page_id"
      }
    }
  }
}

Getting a Notion Integration Token

  1. Visit https://www.notion.so/my-integrations
  2. Click + New integration
  3. Name your integration and select capabilities
  4. Copy the Internal Integration Token (starts with ntn_)
  5. Share target pages/databases with your integration

Configuration

Environment Variables

Core Settings:

# Required for stdio transport
NOTION_TOKEN=ntn_your_integration_token_here

# Optional: default parent for create_page
NOTION_ROOT_PAGE_ID=abc123def456

# Disable content-notice prefix on reads (default: false)
NOTION_TRUST_CONTENT=false

# Root directory for file:// uploads (default: cwd)
NOTION_MCP_WORKSPACE_ROOT=/path/to/workspace

# Logging verbosity: debug, info, warn, error (default: info)
LOG_LEVEL=info

HTTP Transport (OAuth or Static Bearer):

# Static bearer token for /mcp endpoint
NOTION_MCP_BEARER=your_random_secret_token

# OAuth configuration
NOTION_OAUTH_CLIENT_ID=your_oauth_client_id
NOTION_OAUTH_CLIENT_SECRET=your_oauth_client_secret

# HTTP server settings
PORT=3333
NOTION_MCP_BIND_HOST=127.0.0.1

Redis (Optional Multi-Instance Persistence):

# Enable Redis (auto-enabled if REDIS_URL is set)
REDIS_ENABLED=true
REDIS_URL=redis://127.0.0.1:6379/0

# Or configure individually:
REDIS_HOST=127.0.0.1
REDIS_PORT=6379
REDIS_PASSWORD=your_redis_password
REDIS_DB=0
REDIS_KEY_PREFIX=easy-notion:
REDIS_CONNECT_TIMEOUT_MS=10000
REDIS_MAX_RETRIES=3

.env File Setup

cp .env.example .env
# Edit .env with your configuration

MCP Tools Reference

Page Operations

read_page

Read a Notion page as markdown.

// Tool call
{
  "name": "read_page",
  "arguments": {
    "page_id": "abc123def456"
  }
}

// Response includes markdown with metadata
{
  "markdown": "# Page Title\n\nContent here...",
  "warnings": []
}

create_page

Create a new page with markdown content.

{
  "name": "create_page",
  "arguments": {
    "title": "My New Page",
    "markdown": "# Heading\n\nSome **bold** text.",
    "parent_page_id": "parent123" // optional, uses NOTION_ROOT_PAGE_ID if not set
  }
}

// Response
{
  "id": "new_page_id",
  "url": "https://notion.so/...",
  "title": "My New Page"
}

update_page

Update page content with markdown.

{
  "name": "update_page",
  "arguments": {
    "page_id": "abc123",
    "markdown": "# Updated Content\n\nNew paragraph.",
    "mode": "replace" // or "append"
  }
}

append_to_page

Append markdown to existing page.

{
  "name": "append_to_page",
  "arguments": {
    "page_id": "abc123",
    "markdown": "\n## New Section\n\nAppended content."
  }
}

delete_page

Move page to trash.

{
  "name": "delete_page",
  "arguments": {
    "page_id": "abc123"
  }
}

Database Operations

query_database

Query database with filters and sorts.

{
  "name": "query_database",
  "arguments": {
    "database_id": "db123",
    "filter": {
      "property": "Status",
      "select": { "equals": "In Progress" }
    },
    "sorts": [
      { "property": "Created", "direction": "descending" }
    ],
    "page_size": 50
  }
}

// Returns array of entries with markdown content
{
  "results": [
    {
      "id": "entry123",
      "properties": { "Status": "In Progress", "Name": "Task 1" },
      "markdown": "Task content..."
    }
  ],
  "has_more": false
}

get_database

Get database schema and properties.

{
  "name": "get_database",
  "arguments": {
    "database_id": "db123"
  }
}

// Returns schema information
{
  "id": "db123",
  "title": "Tasks",
  "properties": {
    "Name": { "type": "title" },
    "Status": { 
      "type": "select",
      "options": ["To Do", "In Progress", "Done"]
    },
    "Due Date": { "type": "date" }
  }
}

add_database_entry

Create new database entry with automatic schema conversion.

{
  "name": "add_database_entry",
  "arguments": {
    "database_id": "db123",
    "properties": {
      "Name": "New Task",
      "Status": "To Do",
      "Due Date": "2026-12-31",
      "Tags": ["urgent", "bug"]
    },
    "markdown": "## Task Details\n\nDetailed description here."
  }
}

// Response
{
  "id": "new_entry_id",
  "url": "https://notion.so/...",
  "properties": { /* converted properties */ }
}

update_database_entry

Update database entry properties and/or content.

{
  "name": "update_database_entry",
  "arguments": {
    "page_id": "entry123",
    "properties": {
      "Status": "Done",
      "Completed": true
    },
    "markdown": "Updated task description."
  }
}

Search Operations

search_notion

Full-text search across workspace.

{
  "name": "search_notion",
  "arguments": {
    "query": "project planning",
    "filter": { "property": "object", "value": "page" },
    "sort": { "direction": "descending", "timestamp": "last_edited_time" },
    "page_size": 20
  }
}

// Returns matching pages/databases
{
  "results": [
    {
      "id": "page123",
      "title": "Q4 Project Planning",
      "url": "https://notion.so/...",
      "last_edited_time": "2026-07-03T10:30:00Z"
    }
  ],
  "has_more": false
}

File Operations

upload_file

Upload local file to a page (stdio transport only).

{
  "name": "upload_file",
  "arguments": {
    "page_id": "abc123",
    "file_path": "/workspace/documents/report.pdf",
    "caption": "Q3 Financial Report"
  }
}

// Security: file_path must be within NOTION_MCP_WORKSPACE_ROOT

add_image

Add image from URL to page.

{
  "name": "add_image",
  "arguments": {
    "page_id": "abc123",
    "url": "https://example.com/image.png",
    "caption": "Architecture Diagram"
  }
}

Block Operations

create_block

Create specific block types.

// Callout block
{
  "name": "create_block",
  "arguments": {
    "parent_id": "page123",
    "type": "callout",
    "content": {
      "icon": "💡",
      "text": "Important note here",
      "color": "blue_background"
    }
  }
}

// Toggle block
{
  "name": "create_block",
  "arguments": {
    "parent_id": "page123",
    "type": "toggle",
    "content": {
      "text": "Click to expand",
      "children": [
        { "type": "paragraph", "text": "Hidden content" }
      ]
    }
  }
}

Common Patterns

Reading and Updating a Page

// 1. Read existing page
const readResponse = await callTool("read_page", {
  page_id: "abc123def456"
});

// 2. Process markdown
const existingMarkdown = readResponse.markdown;
const updatedMarkdown = existingMarkdown + "\n## New Section\n\nAdded content.";

// 3. Update page
await callTool("update_page", {
  page_id: "abc123def456",
  markdown: updatedMarkdown,
  mode: "replace"
});

Creating a Task in Database

// 1. Get database schema (optional but recommended for validation)
const schema = await callTool("get_database", {
  database_id: "tasks_db_id"
});

// 2. Add entry with automatic schema conversion
const newTask = await callTool("add_database_entry", {
  database_id: "tasks_db_id",
  properties: {
    "Task Name": "Implement feature X",
    "Status": "In Progress",
    "Priority": "High",
    "Assignee": "john@example.com",
    "Due Date": "2026-08-15",
    "Tags": ["backend", "api"]
  },
  markdown: `## Description

Implement the new feature X as specified in RFC-123.

### Acceptance Criteria

- [ ] API endpoints created
- [ ] Tests written
- [ ] Documentation updated`
});

console.log(`Created task: ${newTask.url}`);

Querying and Processing Database Entries

// Query active tasks
const results = await callTool("query_database", {
  database_id: "tasks_db_id",
  filter: {
    "and": [
      {
        "property": "Status",
        "select": { "does_not_equal": "Done" }
      },
      {
        "property": "Priority",
        "select": { "equals": "High" }
      }
    ]
  },
  sorts: [
    { "property": "Due Date", "direction": "ascending" }
  ]
});

// Process each task
for (const entry of results.results) {
  console.log(`Task: ${entry.properties["Task Name"]}`);
  console.log(`Due: ${entry.properties["Due Date"]}`);
  console.log(`Content: ${entry.markdown}`);
  
  // Update if needed
  if (shouldUpdate(entry)) {
    await callTool("update_database_entry", {
      page_id: entry.id,
      properties: {
        "Status": "In Review"
      }
    });
  }
}

Creating a Page with Rich Content

const richContent = `# Project Documentation

## Overview

This project implements **critical features** for Q4 delivery.

## Architecture

![System Diagram](https://example.com/diagram.png)

## Tasks

- [x] Initial setup
- [ ] Core implementation
- [ ] Testing

## Notes

> 💡 Remember to update the API docs after deployment.

\`\`\`typescript
// Example usage
const client = new NotionClient({ token: process.env.NOTION_TOKEN });
const page = await client.getPage("page_id");
\`\`\`

## Next Steps

1. Review code
2. Deploy to staging
3. Monitor metrics
`;

const page = await callTool("create_page", {
  title: "Q4 Project Documentation",
  markdown: richContent,
  parent_page_id: process.env.NOTION_ROOT_PAGE_ID
});

Searching and Organizing Content

// Search for all planning documents
const searchResults = await callTool("search_notion", {
  query: "planning",
  filter: { "property": "object", "value": "page" },
  sort: { 
    "direction": "descending", 
    "timestamp": "last_edited_time" 
  }
});

// Create index page with links
const indexMarkdown = searchResults.results
  .map(r => `- [${r.title}](${r.url})`)
  .join("\n");

await callTool("create_page", {
  title: "Planning Documents Index",
  markdown: `# Planning Documents\n\n${indexMarkdown}`,
  parent_page_id: process.env.NOTION_ROOT_PAGE_ID
});

CLI Usage

The project includes a CLI for low-context Notion access:

# Readonly mode
npx easy-notion-mcp --profile readonly

# Readwrite mode
npx easy-notion-mcp --profile readwrite

# With custom config
NOTION_TOKEN=ntn_xxx npx easy-notion-mcp --profile readwrite

Development Workflow

Local Development

# Install dependencies
npm install

# Watch mode for development
npm run dev

# Run stdio server locally
NOTION_TOKEN=ntn_xxx npm start

# Run HTTP server
NOTION_TOKEN=ntn_xxx NOTION_MCP_BEARER=$(openssl rand -hex 32) npm run start:http

Testing

# Run all tests
npm test

# Watch mode
npm run test:watch

# Type checking
npm run typecheck

# Linting
npm run lint

# Build
npm run build

Integration Tests

# Requires dedicated test workspace
cp .env.example .env
# Set E2E_ROOT_PAGE_ID to a test page
npm run test:e2e

HTTP Transport Setup

Static Bearer Token

# Generate secure token
export NOTION_MCP_BEARER=$(openssl rand -hex 32)

# Start server
NOTION_TOKEN=ntn_xxx NOTION_MCP_BEARER=$NOTION_MCP_BEARER npm run start:http

Configure HTTP client:

{
  "mcpServers": {
    "notion-http": {
      "url": "http://127.0.0.1:3333/mcp",
      "transport": "http",
      "headers": {
        "Authorization": "Bearer YOUR_BEARER_TOKEN"
      }
    }
  }
}

OAuth Flow

# Configure OAuth application in Notion
NOTION_OAUTH_CLIENT_ID=your_client_id \
NOTION_OAUTH_CLIENT_SECRET=your_client_secret \
npm run start:http

# Navigate to http://localhost:3333/auth/login
# Complete OAuth flow
# Tokens stored in Redis or encrypted files

Troubleshooting

Common Issues

NOTION_TOKEN is required error:

# Ensure token is set in MCP config env block
{
  "env": {
    "NOTION_TOKEN": "ntn_your_token"
  }
}

401 invalid_token (HTTP mode):

# Verify bearer token matches on client and server
# Server: NOTION_MCP_BEARER=secret_token
# Client: Authorization: Bearer secret_token

outside the allowed workspace root error:

# Set workspace root or use absolute paths inside it
NOTION_MCP_WORKSPACE_ROOT=/path/to/workspace

# Or use URLs instead of file:// paths

Redis connection warnings:

# Start Redis or disable
redis-server

# Or explicitly disable
REDIS_ENABLED=false npm start

Unknown property name errors:

# Call get_database first to fetch current schema
# Property names are case-sensitive
# Example: "Status" not "status"

Stale schema cache:

# Clear Redis cache
redis-cli KEYS "easy-notion:schema:*" | xargs redis-cli DEL

# Or restart with fresh in-memory cache

Enable Debug Logging

LOG_LEVEL=debug npm start

Debug logs include:

  • Full Notion API request/response bodies
  • Schema cache hits/misses
  • Block conversion warnings
  • Redis operation details

Testing Connectivity

// Simple page read test
const result = await callTool("read_page", {
  page_id: "known_shared_page_id"
});
console.log(result.markdown);

// Search test
const search = await callTool("search_notion", {
  query: "test",
  page_size: 1
});
console.log(search.results);

Security Considerations

Content Sanitization

By default, all read operations include a content-notice prefix warning agents about potentially untrusted content:

# Disable for trusted workspaces only
NOTION_TRUST_CONTENT=true

File Upload Restrictions

  • File uploads via file:// URLs are stdio transport only
  • All paths must be within NOTION_MCP_WORKSPACE_ROOT
  • Symlinks are validated to prevent escape
  • Use HTTPS URLs for HTTP transport

Workspace Isolation

# Restrict file operations to specific directory
NOTION_MCP_WORKSPACE_ROOT=/workspace/notion-files

# Attempts to access /etc/passwd will fail

Bearer Token Security

# Generate cryptographically secure tokens
openssl rand -hex 32

# Store securely, never commit to version control
# Use environment variables or secret management

Advanced Configuration

Redis for Multi-Instance Deployments

# Shared schema cache across instances
REDIS_ENABLED=true
REDIS_URL=redis://redis.internal:6379/0
REDIS_KEY_PREFIX=prod-notion:

# Start multiple HTTP instances
for i in {1..3}; do
  PORT=$((3333 + i)) npm run start:http &
done

Custom Cache TTL

Schema cache TTL is not directly configurable but can be adjusted in code:

// src/persistence/cache-store.ts
const DEFAULT_TTL = 3600; // 1 hour in seconds

Property Type Mapping

The server automatically converts between markdown-friendly values and Notion property types:

  • Title/Rich Text: Plain string → Notion rich text
  • Number: Numeric string → Number
  • Date: ISO string → Date object
  • Select/Multi-select: String/Array → Option references
  • People: Email string/Array → Person references
  • Checkbox: Boolean → Boolean
  • URL: String → URL (validated)
  • Email: String → Email (validated)
  • Phone: String → Phone number
  • Files: URL string/Array → External file references

Resources

License

MIT License - See project LICENSE file for details.