#
tokens: 5816/50000 6/6 files
lines: off (toggle) GitHub
raw markdown copy
# Directory Structure

```
├── .env.example
├── .gitignore
├── .npmignore
├── package-lock.json
├── package.json
├── README.md
└── server.js
```

# Files

--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------

```
.aider*
.env
```

--------------------------------------------------------------------------------
/.env.example:
--------------------------------------------------------------------------------

```
# Notion API Key
NOTION_API_KEY=your_notion_api_key_here

```

--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------

```
.aider*
.env
# Dependency directories
node_modules/

# Environment variables
.env

# Logs
logs
*.log
npm-debug.log*

# Runtime data
pids
*.pid
*.seed
*.pid.lock

# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov

# Coverage directory used by tools like istanbul
coverage

# nyc test coverage
.nyc_output

# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release

# Dependency directories
node_modules/
jspm_packages/

# TypeScript cache
*.tsbuildinfo

# Optional npm cache directory
.npm

# Optional eslint cache
.eslintcache

# Optional REPL history
.node_repl_history

# Output of 'npm pack'
*.tgz

# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local

# Mac files
.DS_Store

```

--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------

```markdown
# Notion MCP Server

A Model Context Protocol (MCP) server that connects Claude and other AI assistants to your Notion workspace. This integration allows AI assistants to interact with your Notion databases, pages, and blocks.

## What is this?

This tool acts as a bridge between AI assistants (like Claude) and your Notion workspace. It allows the AI to:
- View and search your Notion databases
- Create and update pages
- Manage content blocks
- And much more!

## Step-by-Step Setup Guide

### Prerequisites
- [Node.js](https://nodejs.org/) (version 14 or higher)
- A Notion account
- Claude Desktop app (if using with Claude)

### 1. Getting Your Notion API Key

1. Go to [https://www.notion.so/my-integrations](https://www.notion.so/my-integrations)
2. Click the blue **"+ New integration"** button
3. Fill in the details:
   - **Name**: Choose a name like "Claude Assistant" or "AI Helper"
   - **Logo**: Optional
   - **Associated workspace**: Select your Notion workspace
4. Click **"Submit"**
5. On the next page, find the **"Internal Integration Token"** section
6. Click **"Show"** and copy the token (it starts with `secret_`)

## 2. Setting Up This Server

### Download the Repository

**Option A: Download as ZIP (Recommended for beginners)**
1. Go to the GitHub repository: https://github.com/Sjotie/notionMCP/
2. Click the green "Code" button at the top right
3. Select "Download ZIP"
4. Once downloaded, extract the ZIP file to a location on your computer
   - Windows: Right-click the ZIP file and select "Extract All"
   - Mac: Double-click the ZIP file to extract

**Option B: Clone with Git (For users familiar with Git)**
1. Open a command prompt or terminal
   - Windows: Press `Win+R`, type `cmd`, and press Enter
   - Mac: Open Terminal from Applications > Utilities
2. Navigate to where you want to store the repository
   ```
   cd path/to/desired/location
   ```
3. Clone the repository
   ```
   git clone https://github.com/Sjotie/notionMCP/
   ```

### Navigate to the Project Directory

After downloading or cloning, you need to navigate to the project folder using the `cd` (change directory) command:

**If you downloaded the ZIP (Option A):**
1. Open a command prompt or terminal
2. Use the `cd` command to navigate to where you extracted the ZIP file:
   ```
   cd path/to/extracted/folder/notionMCP
   ```
   
   For example:
   - On Windows: `cd C:\Users\YourName\Downloads\notionMCP`
   - On Mac: `cd /Users/YourName/Downloads/notionMCP`

**If you cloned with Git (Option B):**
1. The repository should have been cloned into a folder named "notionMCP"
2. If you're still in the same terminal window after cloning, simply type:
   ```
   cd notionMCP
   ```

**How to know you're in the right directory:**
- After using the `cd` command, you can check your current location:
  - On Windows: Type `dir` and press Enter - you should see files like `server.js`
  - On Mac: Type `ls` and press Enter - you should see files like `server.js`

### Install Dependencies

Once you're in the notionMCP directory, install the required dependencies:

```
npm install
```

This will install all the necessary Node.js packages. You should see a progress bar and eventually a message indicating the installation is finished. It might say something along the lines of "X Packages are looking for funding" - this is completely normal and means it worked.

### 3. Connecting to Notion Pages

For security, Notion requires you to explicitly grant access to each page or database:

1. Open Notion and navigate to a page or database you want the AI to access
2. Click the **"•••"** (three dots) in the top-right corner
3. Select **"Add connections"**
4. Find and select the integration you created earlier
5. Repeat for any other pages or databases you want to make accessible

### 4. Connecting to Claude Desktop

1. Locate your Claude Desktop configuration file:
   - Windows: `%APPDATA%\Claude\claude_desktop_config.json`
     (Type this path in File Explorer address bar)
   - Mac: `~/Library/Application Support/Claude/claude_desktop_config.json`
     (In Finder, press Cmd+Shift+G and paste this path)

2. Open the file in a text editor. If it doesn't exist, create it with the following content:
   ```json
   {
     "mcpServers": {
       "notion": {
         "command": "node",
         "args": [
           "C:\\path\\to\\notion-mcp-server\\server.js"
         ],
         "env": {
           "NOTION_API_KEY": "your_notion_api_key_here"
         }
       }
     }
   }
   ```

3. Replace:
   - `C:\\path\\to\\notion-mcp-server\\server.js` with the actual path to the server.js file
     - Windows: Use double backslashes (\\\\) in the path
     - Mac: Use forward slashes (/)
   - `your_notion_api_key_here` with your Notion API key

4. Save the file and restart Claude Desktop

### 5. Testing the Connection

1. Start a new conversation in Claude
2. Ask Claude to interact with your Notion workspace, for example:
   - "Show me a list of my Notion databases"
   - "Create a new page in my Tasks database with title 'Test Task'"

## Available Tools

The server provides these tools to AI assistants:

- **list-databases**: View all accessible databases
- **query-database**: Get entries from a database
- **create-page**: Add a new page to a database
- **update-page**: Modify an existing page
- **create-database**: Create a new database
- **update-database**: Modify a database structure
- **get-page**: View a specific page
- **get-block-children**: View content blocks
- **append-block-children**: Add content to a page
- **update-block**: Edit content blocks
- **get-block**: View a specific block
- **search**: Find content across your workspace

## Troubleshooting

### Common Issues:

1. **"Connection failed" in Claude**
   - Make sure the server path in claude_desktop_config.json is correct
   - Check that your Notion API key is valid
   - Ensure Node.js is installed

2. **"Access denied" when accessing Notion content**
   - Make sure you've shared the page/database with your integration
   - Check that your API key has the necessary permissions

3. **Server won't start**
   - Ensure all dependencies are installed (`npm install`)
   - Check that the .env file exists with your API key

### Getting Help

If you encounter issues not covered here, please:
- Check the console output for error messages
- Ensure your Notion API key is valid
- Verify that your integration has access to the pages/databases

## License

MIT

```

--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------

```json
{
  "name": "@sjotie/notion-mcp-server",
  "version": "1.0.3",
  "description": "MCP server for Notion integration",
  "type": "module",
  "main": "server.js",
  "scripts": {
    "start": "node server.js",
    "dev": "nodemon server.js"
  },
  "keywords": [
    "notion",
    "mcp",
    "model-context-protocol"
  ],
  "author": "Sjotie",
  "license": "MIT",
  "bin": {
    "notion-mcp-server": "./server.js"
  },
  "dependencies": {
    "@modelcontextprotocol/sdk": "^0.7.0",
    "@notionhq/client": "^2.2.16",
    "dotenv": "^16.4.7",
    "zod": "^3.24.2"
  },
  "devDependencies": {
    "nodemon": "^3.0.2"
  },
  "repository": {
    "type": "git",
    "url": "git+https://github.com/Sjotie/notionMCP.git"
  },
  "bugs": {
    "url": "https://github.com/Sjotie/notionMCP/issues"
  },
  "homepage": "https://github.com/Sjotie/notionMCP#readme"
}

```

--------------------------------------------------------------------------------
/server.js:
--------------------------------------------------------------------------------

```javascript
#!/usr/bin/env node

import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
import { Client } from "@notionhq/client";
import dotenv from "dotenv";

// Load environment variables
dotenv.config();

// Initialize Notion client
const notion = new Client({
  auth: process.env.NOTION_API_KEY,
});

// Create MCP server
const server = new Server({
  name: "notion-mcp",
  version: "1.0.0",
}, {
  capabilities: {
    tools: {}
  }
});

// Add a request interceptor for debugging
server.setRequestHandler(z.object({
  method: z.string(),
  params: z.any().optional()
}), async (request) => {
  console.error("Received request:", JSON.stringify(request, null, 2));
  
  // Let the request continue to be handled by other handlers
  return undefined;
}, { priority: -1 });

// List databases tool
server.setRequestHandler(z.object({
  method: z.literal("tools/list")
}), async () => {
  return {
    tools: [
      {
        name: "list-databases",
        description: "List all databases the integration has access to",
        inputSchema: {
          type: "object",
          properties: {}
        }
      },
      {
        name: "query-database",
        description: "Query a database",
        inputSchema: {
          type: "object",
          properties: {
            database_id: {
              type: "string",
              description: "ID of the database to query"
            },
            filter: {
              type: "object",
              description: "Optional filter criteria"
            },
            sorts: {
              type: "array",
              description: "Optional sort criteria"
            },
            start_cursor: {
              type: "string",
              description: "Optional cursor for pagination"
            },
            page_size: {
              type: "number",
              description: "Number of results per page",
              default: 100
            }
          },
          required: ["database_id"]
        }
      },
      {
        name: "create-page",
        description: "Create a new page in a database",
        inputSchema: {
          type: "object",
          properties: {
            parent_id: {
              type: "string",
              description: "ID of the parent database"
            },
            properties: {
              type: "object",
              description: "Page properties"
            },
            children: {
              type: "array",
              description: "Optional content blocks"
            }
          },
          required: ["parent_id", "properties"]
        }
      },
      {
        name: "update-page",
        description: "Update an existing page",
        inputSchema: {
          type: "object",
          properties: {
            page_id: {
              type: "string",
              description: "ID of the page to update"
            },
            properties: {
              type: "object",
              description: "Updated page properties"
            },
            archived: {
              type: "boolean",
              description: "Whether to archive the page"
            }
          },
          required: ["page_id", "properties"]
        }
      },
      {
        name: "create-database",
        description: "Create a new database",
        inputSchema: {
          type: "object",
          properties: {
            parent_id: {
              type: "string",
              description: "ID of the parent page"
            },
            title: {
              type: "array",
              description: "Database title as rich text array"
            },
            properties: {
              type: "object",
              description: "Database properties schema"
            },
            icon: {
              type: "object",
              description: "Optional icon for the database"
            },
            cover: {
              type: "object",
              description: "Optional cover for the database"
            }
          },
          required: ["parent_id", "title", "properties"]
        }
      },
      {
        name: "update-database",
        description: "Update an existing database",
        inputSchema: {
          type: "object",
          properties: {
            database_id: {
              type: "string",
              description: "ID of the database to update"
            },
            title: {
              type: "array",
              description: "Optional new title as rich text array"
            },
            description: {
              type: "array",
              description: "Optional new description as rich text array"
            },
            properties: {
              type: "object",
              description: "Optional updated properties schema"
            }
          },
          required: ["database_id"]
        }
      },
      {
        name: "get-page",
        description: "Retrieve a page by its ID",
        inputSchema: {
          type: "object",
          properties: {
            page_id: {
              type: "string",
              description: "ID of the page to retrieve"
            }
          },
          required: ["page_id"]
        }
      },
      {
        name: "get-block-children",
        description: "Retrieve the children blocks of a block",
        inputSchema: {
          type: "object",
          properties: {
            block_id: {
              type: "string",
              description: "ID of the block (page or block)"
            },
            start_cursor: {
              type: "string",
              description: "Cursor for pagination"
            },
            page_size: {
              type: "number",
              description: "Number of results per page",
              default: 100
            }
          },
          required: ["block_id"]
        }
      },
      {
        name: "append-block-children",
        description: "Append blocks to a parent block",
        inputSchema: {
          type: "object",
          properties: {
            block_id: {
              type: "string",
              description: "ID of the parent block (page or block)"
            },
            children: {
              type: "array",
              description: "List of block objects to append"
            },
            after: {
              type: "string",
              description: "Optional ID of an existing block to append after"
            }
          },
          required: ["block_id", "children"]
        }
      },
      {
        name: "update-block",
        description: "Update a block's content or archive status",
        inputSchema: {
          type: "object",
          properties: {
            block_id: {
              type: "string",
              description: "ID of the block to update"
            },
            block_type: {
              type: "string",
              description: "The type of block (paragraph, heading_1, to_do, etc.)"
            },
            content: {
              type: "object",
              description: "The content for the block based on its type"
            },
            archived: {
              type: "boolean",
              description: "Whether to archive (true) or restore (false) the block"
            }
          },
          required: ["block_id", "block_type", "content"]
        }
      },
      {
        name: "get-block",
        description: "Retrieve a block by its ID",
        inputSchema: {
          type: "object",
          properties: {
            block_id: {
              type: "string",
              description: "ID of the block to retrieve"
            }
          },
          required: ["block_id"]
        }
      },
      {
        name: "search",
        description: "Search Notion for pages or databases",
        inputSchema: {
          type: "object",
          properties: {
            query: {
              type: "string",
              description: "Search query string",
              default: ""
            },
            filter: {
              type: "object",
              description: "Optional filter criteria"
            },
            sort: {
              type: "object",
              description: "Optional sort criteria"
            },
            start_cursor: {
              type: "string",
              description: "Cursor for pagination"
            },
            page_size: {
              type: "number",
              description: "Number of results per page",
              default: 100
            }
          }
        }
      }
    ]
  };
});

// Define a single CallToolRequestSchema handler for all tools
server.setRequestHandler(z.object({
  method: z.literal("tools/call"),
  params: z.object({
    name: z.string(),
    arguments: z.any()
  })
}), async (request) => {
  const { name, arguments: args } = request.params;
  
  try {
    // Handle each tool based on name
    if (name === "list-databases") {
      const response = await notion.search({
        filter: {
          property: "object",
          value: "database",
        },
        page_size: 100,
        sort: {
          direction: "descending",
          timestamp: "last_edited_time",
        },
      });

      return {
        content: [
          {
            type: "text",
            text: JSON.stringify(response.results, null, 2),
          },
        ],
      };
    }

    else if (name === "query-database") {
      console.error("Query database handler called with:", JSON.stringify(args, null, 2));
      const { database_id, filter, sorts, start_cursor, page_size } = args;
      
      const queryParams = {
        database_id,
        page_size: page_size || 100,
      };

      if (filter) queryParams.filter = filter;
      if (sorts) queryParams.sorts = sorts;
      if (start_cursor) queryParams.start_cursor = start_cursor;

      const response = await notion.databases.query(queryParams);

      return {
        content: [
          {
            type: "text",
            text: JSON.stringify(response, null, 2),
          },
        ],
      };
    }

    else if (name === "create-page") {
      const { parent_id, properties, children } = args;
      
      const pageParams = {
        parent: { database_id: parent_id },
        properties,
      };

      if (children) {
        pageParams.children = children;
      }

      const response = await notion.pages.create(pageParams);

      return {
        content: [
          {
            type: "text",
            text: JSON.stringify(response, null, 2),
          },
        ],
      };
    }

    else if (name === "update-page") {
      const { page_id, properties, archived } = args;
      
      const updateParams = {
        page_id,
        properties,
      };

      if (archived !== undefined) {
        updateParams.archived = archived;
      }

      const response = await notion.pages.update(updateParams);

      return {
        content: [
          {
            type: "text",
            text: JSON.stringify(response, null, 2),
          },
        ],
      };
    }

    else if (name === "create-database") {
      let { parent_id, title, properties, icon, cover } = args;
      
      // Remove dashes if present in parent_id
      parent_id = parent_id.replace(/-/g, "");

      const databaseParams = {
        parent: {
          type: "page_id",
          page_id: parent_id,
        },
        title,
        properties,
      };

      // Set default emoji if icon is specified but emoji is empty
      if (icon && icon.type === "emoji" && !icon.emoji) {
        icon.emoji = "📄"; // Default document emoji
        databaseParams.icon = icon;
      } else if (icon) {
        databaseParams.icon = icon;
      }

      if (cover) {
        databaseParams.cover = cover;
      }

      const response = await notion.databases.create(databaseParams);

      return {
        content: [
          {
            type: "text",
            text: JSON.stringify(response, null, 2),
          },
        ],
      };
    }

    else if (name === "update-database") {
      const { database_id, title, description, properties } = args;
      
      const updateParams = {
        database_id,
      };

      if (title !== undefined) {
        updateParams.title = title;
      }

      if (description !== undefined) {
        updateParams.description = description;
      }

      if (properties !== undefined) {
        updateParams.properties = properties;
      }

      const response = await notion.databases.update(updateParams);

      return {
        content: [
          {
            type: "text",
            text: JSON.stringify(response, null, 2),
          },
        ],
      };
    }

    else if (name === "get-page") {
      let { page_id } = args;
      
      // Remove dashes if present in page_id
      page_id = page_id.replace(/-/g, "");

      const response = await notion.pages.retrieve({ page_id });

      return {
        content: [
          {
            type: "text",
            text: JSON.stringify(response, null, 2),
          },
        ],
      };
    }

    else if (name === "get-block-children") {
      let { block_id, start_cursor, page_size } = args;
      
      // Remove dashes if present in block_id
      block_id = block_id.replace(/-/g, "");

      const params = {
        block_id,
        page_size: page_size || 100,
      };

      if (start_cursor) {
        params.start_cursor = start_cursor;
      }

      const response = await notion.blocks.children.list(params);

      return {
        content: [
          {
            type: "text",
            text: JSON.stringify(response, null, 2),
          },
        ],
      };
    }

    else if (name === "append-block-children") {
      let { block_id, children, after } = args;
      
      // Remove dashes if present in block_id
      block_id = block_id.replace(/-/g, "");

      const params = {
        block_id,
        children,
      };

      if (after) {
        params.after = after.replace(/-/g, ""); // Ensure after ID is properly formatted
      }

      const response = await notion.blocks.children.append(params);

      return {
        content: [
          {
            type: "text",
            text: JSON.stringify(response, null, 2),
          },
        ],
      };
    }

    else if (name === "update-block") {
      let { block_id, block_type, content, archived } = args;
      
      // Remove dashes if present in block_id
      block_id = block_id.replace(/-/g, "");

      const updateParams = {
        block_id,
        [block_type]: content,
      };

      if (archived !== undefined) {
        updateParams.archived = archived;
      }

      const response = await notion.blocks.update(updateParams);

      return {
        content: [
          {
            type: "text",
            text: JSON.stringify(response, null, 2),
          },
        ],
      };
    }

    else if (name === "get-block") {
      let { block_id } = args;
      
      // Remove dashes if present in block_id
      block_id = block_id.replace(/-/g, "");

      const response = await notion.blocks.retrieve({ block_id });

      return {
        content: [
          {
            type: "text",
            text: JSON.stringify(response, null, 2),
          },
        ],
      };
    }

    else if (name === "search") {
      const { query, filter, sort, start_cursor, page_size } = args;
      
      const searchParams = {
        query: query || "",
        page_size: page_size || 100,
      };

      if (filter) {
        searchParams.filter = filter;
      }

      if (sort) {
        searchParams.sort = sort;
      }

      if (start_cursor) {
        searchParams.start_cursor = start_cursor;
      }

      const response = await notion.search(searchParams);

      return {
        content: [
          {
            type: "text",
            text: JSON.stringify(response, null, 2),
          },
        ],
      };
    }
    
    // If we get here, the tool name wasn't recognized
    return {
      isError: true,
      content: [
        {
          type: "text",
          text: `Unknown tool: ${name}`,
        },
      ],
    };
  } catch (error) {
    return {
      isError: true,
      content: [
        {
          type: "text",
          text: `Error executing ${name}: ${error.message}`,
        },
      ],
    };
  }
});

// Start the server
async function main() {
  const transport = new StdioServerTransport();
  await server.connect(transport);
  console.error("Notion MCP Server running on stdio");
}

// Add error handling for unhandled rejections
process.on('unhandledRejection', (reason, promise) => {
  console.error('Unhandled Rejection at:', promise, 'reason:', reason);
});

main().catch((error) => {
  console.error("Fatal error in main():", error);
  process.exit(1);
});

```