# Directory Structure ``` ├── .env.example ├── .gitignore ├── .npmignore ├── package-lock.json ├── package.json ├── README.md └── server.js ``` # Files -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- ``` 1 | .aider* 2 | .env ``` -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- ``` 1 | # Notion API Key 2 | NOTION_API_KEY=your_notion_api_key_here 3 | ``` -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- ``` 1 | .aider* 2 | .env 3 | # Dependency directories 4 | node_modules/ 5 | 6 | # Environment variables 7 | .env 8 | 9 | # Logs 10 | logs 11 | *.log 12 | npm-debug.log* 13 | 14 | # Runtime data 15 | pids 16 | *.pid 17 | *.seed 18 | *.pid.lock 19 | 20 | # Directory for instrumented libs generated by jscoverage/JSCover 21 | lib-cov 22 | 23 | # Coverage directory used by tools like istanbul 24 | coverage 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Compiled binary addons (https://nodejs.org/api/addons.html) 30 | build/Release 31 | 32 | # Dependency directories 33 | node_modules/ 34 | jspm_packages/ 35 | 36 | # TypeScript cache 37 | *.tsbuildinfo 38 | 39 | # Optional npm cache directory 40 | .npm 41 | 42 | # Optional eslint cache 43 | .eslintcache 44 | 45 | # Optional REPL history 46 | .node_repl_history 47 | 48 | # Output of 'npm pack' 49 | *.tgz 50 | 51 | # dotenv environment variable files 52 | .env 53 | .env.development.local 54 | .env.test.local 55 | .env.production.local 56 | .env.local 57 | 58 | # Mac files 59 | .DS_Store 60 | ``` -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- ```markdown 1 | # Notion MCP Server 2 | 3 | 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. 4 | 5 | ## What is this? 6 | 7 | This tool acts as a bridge between AI assistants (like Claude) and your Notion workspace. It allows the AI to: 8 | - View and search your Notion databases 9 | - Create and update pages 10 | - Manage content blocks 11 | - And much more! 12 | 13 | ## Step-by-Step Setup Guide 14 | 15 | ### Prerequisites 16 | - [Node.js](https://nodejs.org/) (version 14 or higher) 17 | - A Notion account 18 | - Claude Desktop app (if using with Claude) 19 | 20 | ### 1. Getting Your Notion API Key 21 | 22 | 1. Go to [https://www.notion.so/my-integrations](https://www.notion.so/my-integrations) 23 | 2. Click the blue **"+ New integration"** button 24 | 3. Fill in the details: 25 | - **Name**: Choose a name like "Claude Assistant" or "AI Helper" 26 | - **Logo**: Optional 27 | - **Associated workspace**: Select your Notion workspace 28 | 4. Click **"Submit"** 29 | 5. On the next page, find the **"Internal Integration Token"** section 30 | 6. Click **"Show"** and copy the token (it starts with `secret_`) 31 | 32 | ## 2. Setting Up This Server 33 | 34 | ### Download the Repository 35 | 36 | **Option A: Download as ZIP (Recommended for beginners)** 37 | 1. Go to the GitHub repository: https://github.com/Sjotie/notionMCP/ 38 | 2. Click the green "Code" button at the top right 39 | 3. Select "Download ZIP" 40 | 4. Once downloaded, extract the ZIP file to a location on your computer 41 | - Windows: Right-click the ZIP file and select "Extract All" 42 | - Mac: Double-click the ZIP file to extract 43 | 44 | **Option B: Clone with Git (For users familiar with Git)** 45 | 1. Open a command prompt or terminal 46 | - Windows: Press `Win+R`, type `cmd`, and press Enter 47 | - Mac: Open Terminal from Applications > Utilities 48 | 2. Navigate to where you want to store the repository 49 | ``` 50 | cd path/to/desired/location 51 | ``` 52 | 3. Clone the repository 53 | ``` 54 | git clone https://github.com/Sjotie/notionMCP/ 55 | ``` 56 | 57 | ### Navigate to the Project Directory 58 | 59 | After downloading or cloning, you need to navigate to the project folder using the `cd` (change directory) command: 60 | 61 | **If you downloaded the ZIP (Option A):** 62 | 1. Open a command prompt or terminal 63 | 2. Use the `cd` command to navigate to where you extracted the ZIP file: 64 | ``` 65 | cd path/to/extracted/folder/notionMCP 66 | ``` 67 | 68 | For example: 69 | - On Windows: `cd C:\Users\YourName\Downloads\notionMCP` 70 | - On Mac: `cd /Users/YourName/Downloads/notionMCP` 71 | 72 | **If you cloned with Git (Option B):** 73 | 1. The repository should have been cloned into a folder named "notionMCP" 74 | 2. If you're still in the same terminal window after cloning, simply type: 75 | ``` 76 | cd notionMCP 77 | ``` 78 | 79 | **How to know you're in the right directory:** 80 | - After using the `cd` command, you can check your current location: 81 | - On Windows: Type `dir` and press Enter - you should see files like `server.js` 82 | - On Mac: Type `ls` and press Enter - you should see files like `server.js` 83 | 84 | ### Install Dependencies 85 | 86 | Once you're in the notionMCP directory, install the required dependencies: 87 | 88 | ``` 89 | npm install 90 | ``` 91 | 92 | 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. 93 | 94 | ### 3. Connecting to Notion Pages 95 | 96 | For security, Notion requires you to explicitly grant access to each page or database: 97 | 98 | 1. Open Notion and navigate to a page or database you want the AI to access 99 | 2. Click the **"•••"** (three dots) in the top-right corner 100 | 3. Select **"Add connections"** 101 | 4. Find and select the integration you created earlier 102 | 5. Repeat for any other pages or databases you want to make accessible 103 | 104 | ### 4. Connecting to Claude Desktop 105 | 106 | 1. Locate your Claude Desktop configuration file: 107 | - Windows: `%APPDATA%\Claude\claude_desktop_config.json` 108 | (Type this path in File Explorer address bar) 109 | - Mac: `~/Library/Application Support/Claude/claude_desktop_config.json` 110 | (In Finder, press Cmd+Shift+G and paste this path) 111 | 112 | 2. Open the file in a text editor. If it doesn't exist, create it with the following content: 113 | ```json 114 | { 115 | "mcpServers": { 116 | "notion": { 117 | "command": "node", 118 | "args": [ 119 | "C:\\path\\to\\notion-mcp-server\\server.js" 120 | ], 121 | "env": { 122 | "NOTION_API_KEY": "your_notion_api_key_here" 123 | } 124 | } 125 | } 126 | } 127 | ``` 128 | 129 | 3. Replace: 130 | - `C:\\path\\to\\notion-mcp-server\\server.js` with the actual path to the server.js file 131 | - Windows: Use double backslashes (\\\\) in the path 132 | - Mac: Use forward slashes (/) 133 | - `your_notion_api_key_here` with your Notion API key 134 | 135 | 4. Save the file and restart Claude Desktop 136 | 137 | ### 5. Testing the Connection 138 | 139 | 1. Start a new conversation in Claude 140 | 2. Ask Claude to interact with your Notion workspace, for example: 141 | - "Show me a list of my Notion databases" 142 | - "Create a new page in my Tasks database with title 'Test Task'" 143 | 144 | ## Available Tools 145 | 146 | The server provides these tools to AI assistants: 147 | 148 | - **list-databases**: View all accessible databases 149 | - **query-database**: Get entries from a database 150 | - **create-page**: Add a new page to a database 151 | - **update-page**: Modify an existing page 152 | - **create-database**: Create a new database 153 | - **update-database**: Modify a database structure 154 | - **get-page**: View a specific page 155 | - **get-block-children**: View content blocks 156 | - **append-block-children**: Add content to a page 157 | - **update-block**: Edit content blocks 158 | - **get-block**: View a specific block 159 | - **search**: Find content across your workspace 160 | 161 | ## Troubleshooting 162 | 163 | ### Common Issues: 164 | 165 | 1. **"Connection failed" in Claude** 166 | - Make sure the server path in claude_desktop_config.json is correct 167 | - Check that your Notion API key is valid 168 | - Ensure Node.js is installed 169 | 170 | 2. **"Access denied" when accessing Notion content** 171 | - Make sure you've shared the page/database with your integration 172 | - Check that your API key has the necessary permissions 173 | 174 | 3. **Server won't start** 175 | - Ensure all dependencies are installed (`npm install`) 176 | - Check that the .env file exists with your API key 177 | 178 | ### Getting Help 179 | 180 | If you encounter issues not covered here, please: 181 | - Check the console output for error messages 182 | - Ensure your Notion API key is valid 183 | - Verify that your integration has access to the pages/databases 184 | 185 | ## License 186 | 187 | MIT 188 | ``` -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "name": "@sjotie/notion-mcp-server", 3 | "version": "1.0.3", 4 | "description": "MCP server for Notion integration", 5 | "type": "module", 6 | "main": "server.js", 7 | "scripts": { 8 | "start": "node server.js", 9 | "dev": "nodemon server.js" 10 | }, 11 | "keywords": [ 12 | "notion", 13 | "mcp", 14 | "model-context-protocol" 15 | ], 16 | "author": "Sjotie", 17 | "license": "MIT", 18 | "bin": { 19 | "notion-mcp-server": "./server.js" 20 | }, 21 | "dependencies": { 22 | "@modelcontextprotocol/sdk": "^0.7.0", 23 | "@notionhq/client": "^2.2.16", 24 | "dotenv": "^16.4.7", 25 | "zod": "^3.24.2" 26 | }, 27 | "devDependencies": { 28 | "nodemon": "^3.0.2" 29 | }, 30 | "repository": { 31 | "type": "git", 32 | "url": "git+https://github.com/Sjotie/notionMCP.git" 33 | }, 34 | "bugs": { 35 | "url": "https://github.com/Sjotie/notionMCP/issues" 36 | }, 37 | "homepage": "https://github.com/Sjotie/notionMCP#readme" 38 | } 39 | ``` -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- ```javascript 1 | #!/usr/bin/env node 2 | 3 | import { Server } from "@modelcontextprotocol/sdk/server/index.js"; 4 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; 5 | import { z } from "zod"; 6 | import { Client } from "@notionhq/client"; 7 | import dotenv from "dotenv"; 8 | 9 | // Load environment variables 10 | dotenv.config(); 11 | 12 | // Initialize Notion client 13 | const notion = new Client({ 14 | auth: process.env.NOTION_API_KEY, 15 | }); 16 | 17 | // Create MCP server 18 | const server = new Server({ 19 | name: "notion-mcp", 20 | version: "1.0.0", 21 | }, { 22 | capabilities: { 23 | tools: {} 24 | } 25 | }); 26 | 27 | // Add a request interceptor for debugging 28 | server.setRequestHandler(z.object({ 29 | method: z.string(), 30 | params: z.any().optional() 31 | }), async (request) => { 32 | console.error("Received request:", JSON.stringify(request, null, 2)); 33 | 34 | // Let the request continue to be handled by other handlers 35 | return undefined; 36 | }, { priority: -1 }); 37 | 38 | // List databases tool 39 | server.setRequestHandler(z.object({ 40 | method: z.literal("tools/list") 41 | }), async () => { 42 | return { 43 | tools: [ 44 | { 45 | name: "list-databases", 46 | description: "List all databases the integration has access to", 47 | inputSchema: { 48 | type: "object", 49 | properties: {} 50 | } 51 | }, 52 | { 53 | name: "query-database", 54 | description: "Query a database", 55 | inputSchema: { 56 | type: "object", 57 | properties: { 58 | database_id: { 59 | type: "string", 60 | description: "ID of the database to query" 61 | }, 62 | filter: { 63 | type: "object", 64 | description: "Optional filter criteria" 65 | }, 66 | sorts: { 67 | type: "array", 68 | description: "Optional sort criteria" 69 | }, 70 | start_cursor: { 71 | type: "string", 72 | description: "Optional cursor for pagination" 73 | }, 74 | page_size: { 75 | type: "number", 76 | description: "Number of results per page", 77 | default: 100 78 | } 79 | }, 80 | required: ["database_id"] 81 | } 82 | }, 83 | { 84 | name: "create-page", 85 | description: "Create a new page in a database", 86 | inputSchema: { 87 | type: "object", 88 | properties: { 89 | parent_id: { 90 | type: "string", 91 | description: "ID of the parent database" 92 | }, 93 | properties: { 94 | type: "object", 95 | description: "Page properties" 96 | }, 97 | children: { 98 | type: "array", 99 | description: "Optional content blocks" 100 | } 101 | }, 102 | required: ["parent_id", "properties"] 103 | } 104 | }, 105 | { 106 | name: "update-page", 107 | description: "Update an existing page", 108 | inputSchema: { 109 | type: "object", 110 | properties: { 111 | page_id: { 112 | type: "string", 113 | description: "ID of the page to update" 114 | }, 115 | properties: { 116 | type: "object", 117 | description: "Updated page properties" 118 | }, 119 | archived: { 120 | type: "boolean", 121 | description: "Whether to archive the page" 122 | } 123 | }, 124 | required: ["page_id", "properties"] 125 | } 126 | }, 127 | { 128 | name: "create-database", 129 | description: "Create a new database", 130 | inputSchema: { 131 | type: "object", 132 | properties: { 133 | parent_id: { 134 | type: "string", 135 | description: "ID of the parent page" 136 | }, 137 | title: { 138 | type: "array", 139 | description: "Database title as rich text array" 140 | }, 141 | properties: { 142 | type: "object", 143 | description: "Database properties schema" 144 | }, 145 | icon: { 146 | type: "object", 147 | description: "Optional icon for the database" 148 | }, 149 | cover: { 150 | type: "object", 151 | description: "Optional cover for the database" 152 | } 153 | }, 154 | required: ["parent_id", "title", "properties"] 155 | } 156 | }, 157 | { 158 | name: "update-database", 159 | description: "Update an existing database", 160 | inputSchema: { 161 | type: "object", 162 | properties: { 163 | database_id: { 164 | type: "string", 165 | description: "ID of the database to update" 166 | }, 167 | title: { 168 | type: "array", 169 | description: "Optional new title as rich text array" 170 | }, 171 | description: { 172 | type: "array", 173 | description: "Optional new description as rich text array" 174 | }, 175 | properties: { 176 | type: "object", 177 | description: "Optional updated properties schema" 178 | } 179 | }, 180 | required: ["database_id"] 181 | } 182 | }, 183 | { 184 | name: "get-page", 185 | description: "Retrieve a page by its ID", 186 | inputSchema: { 187 | type: "object", 188 | properties: { 189 | page_id: { 190 | type: "string", 191 | description: "ID of the page to retrieve" 192 | } 193 | }, 194 | required: ["page_id"] 195 | } 196 | }, 197 | { 198 | name: "get-block-children", 199 | description: "Retrieve the children blocks of a block", 200 | inputSchema: { 201 | type: "object", 202 | properties: { 203 | block_id: { 204 | type: "string", 205 | description: "ID of the block (page or block)" 206 | }, 207 | start_cursor: { 208 | type: "string", 209 | description: "Cursor for pagination" 210 | }, 211 | page_size: { 212 | type: "number", 213 | description: "Number of results per page", 214 | default: 100 215 | } 216 | }, 217 | required: ["block_id"] 218 | } 219 | }, 220 | { 221 | name: "append-block-children", 222 | description: "Append blocks to a parent block", 223 | inputSchema: { 224 | type: "object", 225 | properties: { 226 | block_id: { 227 | type: "string", 228 | description: "ID of the parent block (page or block)" 229 | }, 230 | children: { 231 | type: "array", 232 | description: "List of block objects to append" 233 | }, 234 | after: { 235 | type: "string", 236 | description: "Optional ID of an existing block to append after" 237 | } 238 | }, 239 | required: ["block_id", "children"] 240 | } 241 | }, 242 | { 243 | name: "update-block", 244 | description: "Update a block's content or archive status", 245 | inputSchema: { 246 | type: "object", 247 | properties: { 248 | block_id: { 249 | type: "string", 250 | description: "ID of the block to update" 251 | }, 252 | block_type: { 253 | type: "string", 254 | description: "The type of block (paragraph, heading_1, to_do, etc.)" 255 | }, 256 | content: { 257 | type: "object", 258 | description: "The content for the block based on its type" 259 | }, 260 | archived: { 261 | type: "boolean", 262 | description: "Whether to archive (true) or restore (false) the block" 263 | } 264 | }, 265 | required: ["block_id", "block_type", "content"] 266 | } 267 | }, 268 | { 269 | name: "get-block", 270 | description: "Retrieve a block by its ID", 271 | inputSchema: { 272 | type: "object", 273 | properties: { 274 | block_id: { 275 | type: "string", 276 | description: "ID of the block to retrieve" 277 | } 278 | }, 279 | required: ["block_id"] 280 | } 281 | }, 282 | { 283 | name: "search", 284 | description: "Search Notion for pages or databases", 285 | inputSchema: { 286 | type: "object", 287 | properties: { 288 | query: { 289 | type: "string", 290 | description: "Search query string", 291 | default: "" 292 | }, 293 | filter: { 294 | type: "object", 295 | description: "Optional filter criteria" 296 | }, 297 | sort: { 298 | type: "object", 299 | description: "Optional sort criteria" 300 | }, 301 | start_cursor: { 302 | type: "string", 303 | description: "Cursor for pagination" 304 | }, 305 | page_size: { 306 | type: "number", 307 | description: "Number of results per page", 308 | default: 100 309 | } 310 | } 311 | } 312 | } 313 | ] 314 | }; 315 | }); 316 | 317 | // Define a single CallToolRequestSchema handler for all tools 318 | server.setRequestHandler(z.object({ 319 | method: z.literal("tools/call"), 320 | params: z.object({ 321 | name: z.string(), 322 | arguments: z.any() 323 | }) 324 | }), async (request) => { 325 | const { name, arguments: args } = request.params; 326 | 327 | try { 328 | // Handle each tool based on name 329 | if (name === "list-databases") { 330 | const response = await notion.search({ 331 | filter: { 332 | property: "object", 333 | value: "database", 334 | }, 335 | page_size: 100, 336 | sort: { 337 | direction: "descending", 338 | timestamp: "last_edited_time", 339 | }, 340 | }); 341 | 342 | return { 343 | content: [ 344 | { 345 | type: "text", 346 | text: JSON.stringify(response.results, null, 2), 347 | }, 348 | ], 349 | }; 350 | } 351 | 352 | else if (name === "query-database") { 353 | console.error("Query database handler called with:", JSON.stringify(args, null, 2)); 354 | const { database_id, filter, sorts, start_cursor, page_size } = args; 355 | 356 | const queryParams = { 357 | database_id, 358 | page_size: page_size || 100, 359 | }; 360 | 361 | if (filter) queryParams.filter = filter; 362 | if (sorts) queryParams.sorts = sorts; 363 | if (start_cursor) queryParams.start_cursor = start_cursor; 364 | 365 | const response = await notion.databases.query(queryParams); 366 | 367 | return { 368 | content: [ 369 | { 370 | type: "text", 371 | text: JSON.stringify(response, null, 2), 372 | }, 373 | ], 374 | }; 375 | } 376 | 377 | else if (name === "create-page") { 378 | const { parent_id, properties, children } = args; 379 | 380 | const pageParams = { 381 | parent: { database_id: parent_id }, 382 | properties, 383 | }; 384 | 385 | if (children) { 386 | pageParams.children = children; 387 | } 388 | 389 | const response = await notion.pages.create(pageParams); 390 | 391 | return { 392 | content: [ 393 | { 394 | type: "text", 395 | text: JSON.stringify(response, null, 2), 396 | }, 397 | ], 398 | }; 399 | } 400 | 401 | else if (name === "update-page") { 402 | const { page_id, properties, archived } = args; 403 | 404 | const updateParams = { 405 | page_id, 406 | properties, 407 | }; 408 | 409 | if (archived !== undefined) { 410 | updateParams.archived = archived; 411 | } 412 | 413 | const response = await notion.pages.update(updateParams); 414 | 415 | return { 416 | content: [ 417 | { 418 | type: "text", 419 | text: JSON.stringify(response, null, 2), 420 | }, 421 | ], 422 | }; 423 | } 424 | 425 | else if (name === "create-database") { 426 | let { parent_id, title, properties, icon, cover } = args; 427 | 428 | // Remove dashes if present in parent_id 429 | parent_id = parent_id.replace(/-/g, ""); 430 | 431 | const databaseParams = { 432 | parent: { 433 | type: "page_id", 434 | page_id: parent_id, 435 | }, 436 | title, 437 | properties, 438 | }; 439 | 440 | // Set default emoji if icon is specified but emoji is empty 441 | if (icon && icon.type === "emoji" && !icon.emoji) { 442 | icon.emoji = "📄"; // Default document emoji 443 | databaseParams.icon = icon; 444 | } else if (icon) { 445 | databaseParams.icon = icon; 446 | } 447 | 448 | if (cover) { 449 | databaseParams.cover = cover; 450 | } 451 | 452 | const response = await notion.databases.create(databaseParams); 453 | 454 | return { 455 | content: [ 456 | { 457 | type: "text", 458 | text: JSON.stringify(response, null, 2), 459 | }, 460 | ], 461 | }; 462 | } 463 | 464 | else if (name === "update-database") { 465 | const { database_id, title, description, properties } = args; 466 | 467 | const updateParams = { 468 | database_id, 469 | }; 470 | 471 | if (title !== undefined) { 472 | updateParams.title = title; 473 | } 474 | 475 | if (description !== undefined) { 476 | updateParams.description = description; 477 | } 478 | 479 | if (properties !== undefined) { 480 | updateParams.properties = properties; 481 | } 482 | 483 | const response = await notion.databases.update(updateParams); 484 | 485 | return { 486 | content: [ 487 | { 488 | type: "text", 489 | text: JSON.stringify(response, null, 2), 490 | }, 491 | ], 492 | }; 493 | } 494 | 495 | else if (name === "get-page") { 496 | let { page_id } = args; 497 | 498 | // Remove dashes if present in page_id 499 | page_id = page_id.replace(/-/g, ""); 500 | 501 | const response = await notion.pages.retrieve({ page_id }); 502 | 503 | return { 504 | content: [ 505 | { 506 | type: "text", 507 | text: JSON.stringify(response, null, 2), 508 | }, 509 | ], 510 | }; 511 | } 512 | 513 | else if (name === "get-block-children") { 514 | let { block_id, start_cursor, page_size } = args; 515 | 516 | // Remove dashes if present in block_id 517 | block_id = block_id.replace(/-/g, ""); 518 | 519 | const params = { 520 | block_id, 521 | page_size: page_size || 100, 522 | }; 523 | 524 | if (start_cursor) { 525 | params.start_cursor = start_cursor; 526 | } 527 | 528 | const response = await notion.blocks.children.list(params); 529 | 530 | return { 531 | content: [ 532 | { 533 | type: "text", 534 | text: JSON.stringify(response, null, 2), 535 | }, 536 | ], 537 | }; 538 | } 539 | 540 | else if (name === "append-block-children") { 541 | let { block_id, children, after } = args; 542 | 543 | // Remove dashes if present in block_id 544 | block_id = block_id.replace(/-/g, ""); 545 | 546 | const params = { 547 | block_id, 548 | children, 549 | }; 550 | 551 | if (after) { 552 | params.after = after.replace(/-/g, ""); // Ensure after ID is properly formatted 553 | } 554 | 555 | const response = await notion.blocks.children.append(params); 556 | 557 | return { 558 | content: [ 559 | { 560 | type: "text", 561 | text: JSON.stringify(response, null, 2), 562 | }, 563 | ], 564 | }; 565 | } 566 | 567 | else if (name === "update-block") { 568 | let { block_id, block_type, content, archived } = args; 569 | 570 | // Remove dashes if present in block_id 571 | block_id = block_id.replace(/-/g, ""); 572 | 573 | const updateParams = { 574 | block_id, 575 | [block_type]: content, 576 | }; 577 | 578 | if (archived !== undefined) { 579 | updateParams.archived = archived; 580 | } 581 | 582 | const response = await notion.blocks.update(updateParams); 583 | 584 | return { 585 | content: [ 586 | { 587 | type: "text", 588 | text: JSON.stringify(response, null, 2), 589 | }, 590 | ], 591 | }; 592 | } 593 | 594 | else if (name === "get-block") { 595 | let { block_id } = args; 596 | 597 | // Remove dashes if present in block_id 598 | block_id = block_id.replace(/-/g, ""); 599 | 600 | const response = await notion.blocks.retrieve({ block_id }); 601 | 602 | return { 603 | content: [ 604 | { 605 | type: "text", 606 | text: JSON.stringify(response, null, 2), 607 | }, 608 | ], 609 | }; 610 | } 611 | 612 | else if (name === "search") { 613 | const { query, filter, sort, start_cursor, page_size } = args; 614 | 615 | const searchParams = { 616 | query: query || "", 617 | page_size: page_size || 100, 618 | }; 619 | 620 | if (filter) { 621 | searchParams.filter = filter; 622 | } 623 | 624 | if (sort) { 625 | searchParams.sort = sort; 626 | } 627 | 628 | if (start_cursor) { 629 | searchParams.start_cursor = start_cursor; 630 | } 631 | 632 | const response = await notion.search(searchParams); 633 | 634 | return { 635 | content: [ 636 | { 637 | type: "text", 638 | text: JSON.stringify(response, null, 2), 639 | }, 640 | ], 641 | }; 642 | } 643 | 644 | // If we get here, the tool name wasn't recognized 645 | return { 646 | isError: true, 647 | content: [ 648 | { 649 | type: "text", 650 | text: `Unknown tool: ${name}`, 651 | }, 652 | ], 653 | }; 654 | } catch (error) { 655 | return { 656 | isError: true, 657 | content: [ 658 | { 659 | type: "text", 660 | text: `Error executing ${name}: ${error.message}`, 661 | }, 662 | ], 663 | }; 664 | } 665 | }); 666 | 667 | // Start the server 668 | async function main() { 669 | const transport = new StdioServerTransport(); 670 | await server.connect(transport); 671 | console.error("Notion MCP Server running on stdio"); 672 | } 673 | 674 | // Add error handling for unhandled rejections 675 | process.on('unhandledRejection', (reason, promise) => { 676 | console.error('Unhandled Rejection at:', promise, 'reason:', reason); 677 | }); 678 | 679 | main().catch((error) => { 680 | console.error("Fatal error in main():", error); 681 | process.exit(1); 682 | }); 683 | ```