├── .DS_Store ├── .gitignore ├── tsconfig.json ├── package.json ├── LICENSE ├── CONTRIBUTING.md ├── README.md └── src └── index.ts /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/v-3/notion-server/HEAD/.DS_Store -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | node_modules/ 3 | 4 | # Environment variables 5 | .env 6 | 7 | # Build outputs 8 | dist/ 9 | build/ 10 | 11 | # Logs 12 | *.log 13 | 14 | # IDE specific files 15 | .vscode/ 16 | .idea/ 17 | 18 | # OS specific files 19 | .DS_Store 20 | Thumbs.db -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "module": "Node16", 5 | "moduleResolution": "Node16", 6 | "outDir": "./build", 7 | "rootDir": "./src", 8 | "strict": true, 9 | "esModuleInterop": true, 10 | "skipLibCheck": true, 11 | "forceConsistentCasingInFileNames": true 12 | }, 13 | "include": ["src/**/*"], 14 | "exclude": ["node_modules"] 15 | } 16 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "@modelcontextprotocol/sdk": "^1.0.3", 4 | "@notionhq/client": "^2.2.15", 5 | "body-parser": "^1.20.3", 6 | "dotenv": "^16.4.7", 7 | "express": "^4.21.2" 8 | }, 9 | "name": "notionmcp", 10 | "version": "1.0.0", 11 | "main": "index.js", 12 | "type":"module", 13 | "bin":{ 14 | "notion":"./build/index.js" 15 | }, 16 | "scripts": { 17 | "build": "tsc && node -e \"require('fs').chmodSync('build/index.js', '755')\"", 18 | "test": "echo \"Error: no test specified\" && exit 1" 19 | }, 20 | "keywords": [], 21 | "author": "", 22 | "license": "ISC", 23 | "devDependencies": { 24 | "@types/node": "^22.10.2", 25 | "typescript": "^5.7.2" 26 | }, 27 | "description": "", 28 | "files": [ 29 | "build" 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 v-3 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Notion MCP Server 2 | 3 | First off, thank you for considering contributing to the Notion MCP Server! This is an open-source project that helps integrate Notion with Large Language Models through the Model Context Protocol. 4 | 5 | ## Code of Conduct 6 | 7 | By participating in this project, you are expected to uphold our Code of Conduct of treating all contributors with respect and maintaining a harassment-free experience for everyone. 8 | 9 | ## How Can I Contribute? 10 | 11 | ### Reporting Bugs 12 | 13 | Before submitting a bug report: 14 | - Check the existing issues to avoid duplicates 15 | - Collect relevant information about the bug 16 | - Use the bug report template below 17 | 18 | **Bug Report Template:** 19 | ```markdown 20 | **Description** 21 | A clear description of the bug. 22 | 23 | **To Reproduce** 24 | Steps to reproduce the behavior: 25 | 1. Configure server with '...' 26 | 2. Call tool '...' 27 | 3. See error 28 | 29 | **Expected behavior** 30 | What you expected to happen. 31 | 32 | **Actual behavior** 33 | What actually happened. 34 | 35 | **Environment** 36 | - Node.js version: 37 | - Operating System: 38 | - Notion MCP Server version: 39 | ``` 40 | 41 | ### Suggesting Enhancements 42 | 43 | When suggesting enhancements: 44 | - Use a clear and descriptive title 45 | - Provide a detailed description of the proposed functionality 46 | - Include examples of how the enhancement would be used 47 | 48 | ### Pull Requests 49 | 50 | 1. Fork the repository 51 | 2. Create a new branch (`git checkout -b feature/amazing-feature`) 52 | 3. Make your changes 53 | 4. Run tests if available 54 | 5. Commit your changes (`git commit -m 'Add amazing feature'`) 55 | 6. Push to your branch (`git push origin feature/amazing-feature`) 56 | 7. Open a Pull Request 57 | 58 | #### Pull Request Guidelines 59 | 60 | - Follow the existing code style 61 | - Update documentation as needed 62 | - Add tests if applicable 63 | - Keep pull requests focused on a single feature or fix 64 | 65 | ## Development Setup 66 | 67 | 1. Clone your fork: 68 | ```bash 69 | git clone https://github.com/your-username/notion-server.git 70 | cd notion-server 71 | ``` 72 | 73 | 2. Install dependencies: 74 | ```bash 75 | npm install 76 | ``` 77 | 78 | 3. Create a `.env` file: 79 | ```env 80 | NOTION_API_KEY=your_notion_api_key_here 81 | ``` 82 | 83 | 4. Build the project: 84 | ```bash 85 | npm run build 86 | ``` 87 | 88 | ## Style Guidelines 89 | 90 | ### TypeScript Style Guide 91 | 92 | - Use TypeScript for all new code 93 | - Follow existing code formatting 94 | - Use meaningful variable and function names 95 | - Add appropriate type annotations 96 | - Document complex logic with comments 97 | 98 | ### Commit Messages 99 | 100 | - Use clear and meaningful commit messages 101 | - Start with a verb in the present tense 102 | - Keep the first line under 50 characters 103 | - Add detailed description if needed 104 | 105 | Examples: 106 | ``` 107 | Add page deletion tool 108 | Fix error handling in search_pages 109 | Update documentation for create_page 110 | ``` 111 | 112 | ## Adding New Tools 113 | 114 | When adding new tools: 115 | 116 | 1. Define the tool in `TOOL_DEFINITIONS`: 117 | ```typescript 118 | { 119 | name: "tool_name", 120 | description: "Clear description of what the tool does", 121 | inputSchema: { 122 | type: "object", 123 | properties: { 124 | // Define input parameters 125 | }, 126 | required: [] 127 | } 128 | } 129 | ``` 130 | 131 | 2. Implement the handler in `toolHandlers` 132 | 3. Add appropriate error handling 133 | 4. Update documentation 134 | 5. Add examples in README.md 135 | 136 | ## Testing 137 | 138 | - Test your changes with different inputs 139 | - Verify error handling 140 | - Test integration with Claude Desktop or other MCP clients 141 | - Document any new test cases added 142 | 143 | ## Documentation 144 | 145 | - Update README.md for new features 146 | - Add JSDoc comments for new functions 147 | - Update type definitions as needed 148 | - Include examples for new functionality 149 | 150 | ## Questions? 151 | 152 | Feel free to open an issue for: 153 | - Help with development 154 | - Questions about contributing 155 | - Clarification on project direction 156 | 157 | ## License 158 | 159 | By contributing, you agree that your contributions will be licensed under the MIT License. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Notion MCP Server 2 | 3 | A Model Context Protocol (MCP) server that provides seamless integration with Notion. This server enables Language Models to interact with your Notion workspace through standardized tools for searching, reading, creating, and updating pages and databases. 4 | 5 | ## 🌟 Key Features 6 | 7 | ### Page Operations 8 | - 🔍 Search through your Notion workspace 9 | - 📝 Create new pages with rich markdown content 10 | - 📖 Read page content with clean formatting 11 | - 🔄 Update existing pages 12 | - 💬 Add and retrieve comments 13 | - 🧱 Block-level operations (update, delete) 14 | 15 | ### Enhanced Markdown Support 16 | - Multiple heading levels (H1-H3) 17 | - Code blocks with language support 18 | - Interactive todo items with checkbox states 19 | - Blockquotes with multi-line support 20 | - Horizontal dividers 21 | - Images with captions 22 | - Nested bullet points 23 | 24 | ### Database Operations 25 | - Create and manage databases 26 | - Add and update database items 27 | - Query with filters and sorting 28 | - Support for various property types: 29 | - Title, Rich text, Number 30 | - Select, Multi-select 31 | - Date, Checkbox 32 | - And more! 33 | 34 | ## 🚀 Getting Started 35 | 36 | ### Prerequisites 37 | - Node.js (v16 or higher) 38 | - Notion API key 39 | - MCP-compatible client (e.g., Claude Desktop) 40 | 41 | ### Installation 42 | 43 | 1. Clone the repository: 44 | ```bash 45 | git clone https://github.com/v-3/notion-server.git 46 | cd notion-server 47 | ``` 48 | 49 | 2. Install dependencies: 50 | ```bash 51 | npm install 52 | ``` 53 | 54 | 3. Set up your environment: 55 | ```bash 56 | # Create .env file 57 | echo "NOTION_API_KEY=your_notion_api_key_here" > .env 58 | 59 | # Or export directly 60 | export NOTION_API_KEY=your_notion_api_key_here 61 | ``` 62 | 63 | 4. Build the server: 64 | ```bash 65 | npm run build 66 | ``` 67 | 68 | ## 🔧 Configuration 69 | 70 | ### Claude Desktop Setup 71 | 72 | 1. Update your Claude Desktop configuration (`claude_desktop_config.json`): 73 | ```json 74 | { 75 | "mcpServers": { 76 | "notion": { 77 | "command": "node", 78 | "args": ["/absolute/path/to/notion-server/build/index.js"], 79 | "env": { 80 | "NOTION_API_KEY": "your_notion_api_key_here" 81 | } 82 | } 83 | } 84 | } 85 | ``` 86 | 87 | 2. Restart Claude Desktop to apply changes 88 | 89 | ## 🛠️ Available Tools 90 | 91 | ### Page Operations 92 | ```typescript 93 | // Search pages 94 | { 95 | query: string // Search query 96 | } 97 | 98 | // Read page 99 | { 100 | pageId: string // ID of the page to read 101 | } 102 | 103 | // Create page 104 | { 105 | title?: string, // Page title 106 | content?: string, // Page content in markdown 107 | parentPageId: string // Parent page ID 108 | properties?: object // For database items 109 | } 110 | 111 | // Update page 112 | { 113 | pageId: string, // Page ID to update 114 | content: string, // New content 115 | type?: string // Content type 116 | } 117 | ``` 118 | 119 | ### Database Operations 120 | ```typescript 121 | // Create database 122 | { 123 | parentPageId: string, 124 | title: string, 125 | properties: object 126 | } 127 | 128 | // Query database 129 | { 130 | databaseId: string, 131 | filter?: object, 132 | sort?: object 133 | } 134 | ``` 135 | 136 | ## 🔐 Setting Up Notion Access 137 | 138 | ### Creating an Integration 139 | 1. Visit [Notion Integrations](https://www.notion.so/my-integrations) 140 | 2. Click "New integration" 141 | 3. Configure permissions: 142 | - Content: Read, Update, Insert 143 | - Comments: Read, Create 144 | - User Information: Read 145 | 146 | ### Connecting Pages 147 | 1. Open your Notion page 148 | 2. Click "..." menu → "Connections" 149 | 3. Add your integration 150 | 4. Repeat for other pages as needed 151 | 152 | ## 📝 Usage Examples 153 | 154 | ### Creating a Page 155 | ```typescript 156 | const result = await notion.create_page({ 157 | parentPageId: "page_id", 158 | title: "My Page", 159 | content: "# Welcome\nThis is a test page." 160 | }); 161 | ``` 162 | 163 | ### Querying a Database 164 | ```typescript 165 | const result = await notion.query_database({ 166 | databaseId: "db_id", 167 | filter: { 168 | property: "Status", 169 | select: { 170 | equals: "In Progress" 171 | } 172 | } 173 | }); 174 | ``` 175 | 176 | ## 🤝 Contributing 177 | 178 | Contributions are welcome! Please: 179 | 1. Fork the repository 180 | 2. Create a feature branch 181 | 3. Submit a Pull Request 182 | 183 | ## 📜 License 184 | 185 | This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. 186 | 187 | ## 🙏 Acknowledgments 188 | 189 | This project has been significantly improved by [sweir1/notion-server](https://github.com/sweir1/notion-server), who has made following updates: 190 | - Enhanced markdown support with more block types 191 | - Comprehensive database operations 192 | - Improved error handling and debugging 193 | - Better property handling for database items 194 | - Cleaner page output formatting 195 | 196 | To use sweir1's version, you can clone their repository: 197 | ```bash 198 | git clone https://github.com/sweir1/notion-server.git 199 | ``` -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { Server } from "@modelcontextprotocol/sdk/server/index.js"; 2 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; 3 | import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js"; 4 | import { Client } from "@notionhq/client"; 5 | import { z } from "zod"; 6 | 7 | // Initialize Notion client 8 | const notion = new Client({ 9 | auth: process.env.NOTION_API_KEY, 10 | }); 11 | 12 | // Validation schemas 13 | const schemas = { 14 | notionTitle: z.object({ 15 | type: z.literal("title"), 16 | title: z.array( 17 | z.object({ 18 | plain_text: z.string(), 19 | }), 20 | ), 21 | }), 22 | 23 | notionPage: z.object({ 24 | id: z.string(), 25 | url: z.string(), 26 | properties: z.record( 27 | z.union([ 28 | z.object({ 29 | type: z.literal("title"), 30 | title: z.array( 31 | z.object({ 32 | plain_text: z.string(), 33 | }), 34 | ), 35 | }), 36 | z.any(), 37 | ]), 38 | ), 39 | }), 40 | 41 | toolInputs: { 42 | searchPages: z.object({ 43 | query: z.string(), 44 | }), 45 | readPage: z.object({ 46 | pageId: z.string(), 47 | }), 48 | createPage: z.object({ 49 | title: z.string().optional(), 50 | content: z.string().optional(), 51 | parentPageId: z.string(), 52 | properties: z.record(z.any()).optional() 53 | }), 54 | updatePage: z.object({ 55 | pageId: z.string(), 56 | content: z.string(), 57 | type: z.enum([ 58 | "paragraph", 59 | "heading_1", 60 | "heading_2", 61 | "heading_3", 62 | "bulleted_list_item", 63 | "numbered_list_item", 64 | "to_do", 65 | "image" 66 | ]).optional(), 67 | mode: z.enum(["replace", "append", "merge"]).default("replace"), // Add this 68 | position: z.enum(["start", "end"]).default("end") // Add this 69 | }), 70 | retrieveDatabase: z.object({ 71 | databaseId: z.string(), 72 | }), 73 | updateDatabase: z.object({ 74 | databaseId: z.string(), 75 | title: z.string().optional(), 76 | description: z.string().optional(), 77 | properties: z.record(z.any()).optional(), 78 | }), 79 | }, 80 | 81 | databaseProperties: z.record(z.union([ 82 | z.object({ title: z.object({}) }), 83 | z.object({ rich_text: z.object({}) }), 84 | z.object({ number: z.object({ format: z.string().optional() }) }), 85 | z.object({ 86 | select: z.object({ 87 | options: z.array( 88 | z.object({ 89 | name: z.string(), 90 | color: z.string().optional() 91 | }) 92 | ).optional() 93 | }) 94 | }), 95 | z.object({ 96 | multi_select: z.object({ 97 | options: z.array( 98 | z.object({ 99 | name: z.string(), 100 | color: z.string().optional() 101 | }) 102 | ).optional() 103 | }) 104 | }), 105 | z.object({ date: z.object({}) }), 106 | z.object({ checkbox: z.object({}) }) 107 | ])), 108 | }; 109 | 110 | // Add this after your schemas 111 | function formatError(error: any): string { 112 | console.error('Full error:', JSON.stringify(error, null, 2)); 113 | 114 | if (error.status === 404) { 115 | return `Resource not found. Please check the provided ID. Details: ${error.body?.message || error.message}`; 116 | } 117 | if (error.status === 401) { 118 | return `Authentication error. Please check your API token. Details: ${error.body?.message || error.message}`; 119 | } 120 | if (error.status === 400) { 121 | return `Bad request. Details: ${error.body?.message || error.message}`; 122 | } 123 | if (error.code) { 124 | return `API Error (${error.code}): ${error.body?.message || error.message}`; 125 | } 126 | return error.body?.message || error.message || "An unknown error occurred"; 127 | } 128 | 129 | // Tool definitions 130 | const TOOL_DEFINITIONS = [ 131 | { 132 | name: "search_pages", 133 | description: "Search through Notion pages", 134 | inputSchema: { 135 | type: "object", 136 | properties: { 137 | query: { 138 | type: "string", 139 | description: "Search query", 140 | }, 141 | }, 142 | required: ["query"], 143 | }, 144 | }, 145 | { 146 | name: "read_page", 147 | description: "Read a regular page's content (not for databases - use retrieve_database for databases). Shows block IDs with their types (needed for block operations)", 148 | inputSchema: { 149 | type: "object", 150 | properties: { 151 | pageId: { 152 | type: "string", 153 | description: "ID of the page to read", 154 | }, 155 | }, 156 | required: ["pageId"], 157 | }, 158 | }, 159 | { 160 | name: "create_page", 161 | description: "Create a new page or database item. For database items, include 'properties' matching database schema. For pages, use 'title' and 'content'", 162 | inputSchema: { 163 | type: "object", 164 | properties: { 165 | title: { 166 | type: "string", 167 | description: "Page title (optional)", 168 | }, 169 | content: { 170 | type: "string", 171 | description: "Page content in markdown format (optional)", 172 | }, 173 | parentPageId: { 174 | type: "string", 175 | description: "ID of the parent page where this page will be created", 176 | }, 177 | properties: { 178 | type: "object", 179 | description: "Additional properties for database items (optional)", 180 | } 181 | }, 182 | required: ["parentPageId"], 183 | }, 184 | }, 185 | { 186 | name: "update_page", 187 | description: "Update an existing Notion page", 188 | inputSchema: { 189 | type: "object", 190 | properties: { 191 | pageId: { 192 | type: "string", 193 | description: "ID of the page to update", 194 | }, 195 | content: { 196 | type: "string", 197 | description: "New content to append", 198 | }, 199 | type: { 200 | type: "string", 201 | enum: ["paragraph", "task", "todo", "heading", "image"], 202 | description: "Type of content to append", 203 | optional: true, 204 | }, 205 | mode: { // Add this 206 | type: "string", 207 | enum: ["replace", "append", "merge"], 208 | description: "Update mode: replace all content, append to existing, or merge", 209 | optional: true, 210 | }, 211 | position: { // Add this 212 | type: "string", 213 | enum: ["start", "end"], 214 | description: "Position for merge mode: start or end", 215 | optional: true, 216 | } 217 | }, 218 | required: ["pageId", "content"], 219 | }, 220 | }, 221 | { 222 | name: "retrieve_comments", 223 | description: "Get all comments from a page", 224 | inputSchema: { 225 | type: "object", 226 | properties: { 227 | pageId: { 228 | type: "string", 229 | description: "ID of the page", 230 | }, 231 | startCursor: { 232 | type: "string", 233 | description: "Pagination cursor", 234 | }, 235 | pageSize: { 236 | type: "number", 237 | description: "Number of comments to retrieve (max 100)", 238 | }, 239 | }, 240 | required: ["pageId"], 241 | }, 242 | }, 243 | { 244 | name: "add_comment", 245 | description: "Add a comment to a page", 246 | inputSchema: { 247 | type: "object", 248 | properties: { 249 | pageId: { 250 | type: "string", 251 | description: "ID of the page to comment on", 252 | }, 253 | content: { 254 | type: "string", 255 | description: "Comment text", 256 | }, 257 | }, 258 | required: ["pageId", "content"], 259 | }, 260 | }, 261 | { 262 | name: "create_database", 263 | description: "Create a new database in a page", 264 | inputSchema: { 265 | type: "object", 266 | properties: { 267 | parentPageId: { 268 | type: "string", 269 | description: "ID of the parent page", 270 | }, 271 | title: { 272 | type: "string", 273 | description: "Database title", 274 | }, 275 | properties: { 276 | type: "object", 277 | description: "Database schema properties", 278 | }, 279 | }, 280 | required: ["parentPageId", "title", "properties"], 281 | }, 282 | }, 283 | { 284 | name: "query_database", 285 | description: "Query a database", 286 | inputSchema: { 287 | type: "object", 288 | properties: { 289 | databaseId: { 290 | type: "string", 291 | description: "ID of the database", 292 | }, 293 | filter: { 294 | type: "object", 295 | description: "Filter conditions", 296 | }, 297 | sort: { 298 | type: "object", 299 | description: "Sort conditions", 300 | }, 301 | }, 302 | required: ["databaseId"], 303 | }, 304 | }, 305 | { 306 | name: "update_block", 307 | description: "Update a block's content (must use same type as original block, use read_page first to get block IDs and types)", 308 | inputSchema: { 309 | type: "object", 310 | properties: { 311 | blockId: { 312 | type: "string", 313 | description: "ID of the block to update", 314 | }, 315 | content: { 316 | type: "string", 317 | description: "New content for the block", 318 | }, 319 | type: { 320 | type: "string", 321 | enum: [ 322 | "paragraph", 323 | "heading_1", 324 | "heading_2", 325 | "heading_3", 326 | "bulleted_list_item", 327 | "numbered_list_item", 328 | ], 329 | description: "Type of block", 330 | }, 331 | }, 332 | required: ["blockId", "content"], 333 | }, 334 | }, 335 | { 336 | name: "delete_block", 337 | description: "Delete a specific block from a page", 338 | inputSchema: { 339 | type: "object", 340 | properties: { 341 | blockId: { 342 | type: "string", 343 | description: "ID of the block to delete", 344 | }, 345 | }, 346 | required: ["blockId"], 347 | }, 348 | }, 349 | { 350 | name: "update_database_item", 351 | description: "Update a database item's properties (use query_database first to see required property structure)", 352 | inputSchema: { 353 | type: "object", 354 | properties: { 355 | pageId: { 356 | type: "string", 357 | description: "ID of the database item (page) to update", 358 | }, 359 | properties: { 360 | type: "object", 361 | description: "Properties to update", 362 | }, 363 | }, 364 | required: ["pageId", "properties"], 365 | }, 366 | }, 367 | // Add these to TOOL_DEFINITIONS 368 | { 369 | name: "retrieve_database", 370 | description: "Retrieve a database's metadata", 371 | inputSchema: { 372 | type: "object", 373 | properties: { 374 | databaseId: { 375 | type: "string", 376 | description: "ID of the database to retrieve", 377 | }, 378 | }, 379 | required: ["databaseId"], 380 | }, 381 | }, 382 | { 383 | name: "update_database", 384 | description: "Update a database's properties", 385 | inputSchema: { 386 | type: "object", 387 | properties: { 388 | databaseId: { 389 | type: "string", 390 | description: "ID of the database to update", 391 | }, 392 | title: { 393 | type: "string", 394 | description: "New title for the database", 395 | }, 396 | description: { 397 | type: "string", 398 | description: "New description for the database", 399 | }, 400 | properties: { 401 | type: "object", 402 | description: "Properties schema to update", 403 | }, 404 | }, 405 | required: ["databaseId"], 406 | }, 407 | }, 408 | ]; 409 | 410 | // Tool implementation handlers 411 | const toolHandlers = { 412 | async search_pages(args: unknown) { 413 | const { query } = schemas.toolInputs.searchPages.parse(args); 414 | console.error(`Searching for: ${query}`); 415 | 416 | const response = await notion.search({ 417 | query, 418 | filter: { property: "object", value: "page" }, 419 | page_size: 10, 420 | }); 421 | 422 | if (!response.results || response.results.length === 0) { 423 | return { 424 | content: [ 425 | { 426 | type: "text" as const, 427 | text: `No pages found matching "${query}"`, 428 | }, 429 | ], 430 | }; 431 | } 432 | 433 | const formattedResults = response.results 434 | .map((page: any) => { 435 | let title = "Untitled"; 436 | try { 437 | // Extract title from URL 438 | const urlMatch = page.url.match(/\/([^/]+)-[^/]+$/); 439 | if (urlMatch) { 440 | title = decodeURIComponent(urlMatch[1].replace(/-/g, ' ')); 441 | } 442 | // If no title from URL or it's still "Untitled", try properties 443 | if (title === "Untitled" && page.properties) { 444 | const titleProperty = page.properties.title || page.properties.Name; 445 | if (titleProperty?.title?.[0]?.plain_text) { 446 | title = titleProperty.title[0].plain_text; 447 | } 448 | } 449 | } catch (e) { 450 | console.error("Error extracting title:", e); 451 | } 452 | 453 | return `• ${title}\n Link: ${page.url}`; 454 | }) 455 | .join("\n\n"); 456 | 457 | return { 458 | content: [ 459 | { 460 | type: "text" as const, 461 | text: `Found ${response.results.length} pages matching "${query}":\n\n${formattedResults}`, 462 | }, 463 | ], 464 | }; 465 | }, 466 | 467 | async read_page(args: unknown) { 468 | const { pageId } = schemas.toolInputs.readPage.parse(args); 469 | 470 | try { 471 | const [blocksResponse, pageResponse] = await Promise.all([ 472 | notion.blocks.children.list({ block_id: pageId }), 473 | notion.pages.retrieve({ page_id: pageId }), 474 | ]); 475 | 476 | const page = schemas.notionPage.parse(pageResponse); 477 | 478 | // Get title 479 | const titleProp = Object.values(page.properties).find((prop) => prop.type === "title"); 480 | const title = titleProp?.type === "title" ? titleProp.title[0]?.plain_text || "Untitled" : "Untitled"; 481 | 482 | // Process blocks and collect child pages/databases 483 | const childPages: string[] = []; 484 | const childDatabases: string[] = []; 485 | const contentBlocks: string[] = []; 486 | 487 | for (const block of blocksResponse.results as Array<{ type: string; id: string;[key: string]: any }>) { 488 | const type = block.type; 489 | 490 | if (type === "child_page") { 491 | childPages.push(`📄 ${block.child_page.title || "Untitled Page"} (ID: ${block.id.replace(/-/g, "")})`); 492 | continue; 493 | } 494 | 495 | if (type === "child_database") { 496 | childDatabases.push(`📊 ${block.child_database.title || "Untitled Database"} (ID: ${block.id.replace(/-/g, "")})`); 497 | continue; 498 | } 499 | 500 | const textContent = block[type]?.rich_text?.map((text: any) => text.plain_text).join("") || ""; 501 | let formattedContent = ""; 502 | 503 | switch (type) { 504 | case "paragraph": 505 | case "heading_1": 506 | case "heading_2": 507 | case "heading_3": 508 | formattedContent = textContent; 509 | break; 510 | case "bulleted_list_item": 511 | case "numbered_list_item": 512 | formattedContent = "• " + textContent; 513 | break; 514 | case "to_do": 515 | const checked = block.to_do?.checked ? "[x]" : "[ ]"; 516 | formattedContent = checked + " " + textContent; 517 | break; 518 | case "code": 519 | formattedContent = "```\n" + textContent + "\n```"; 520 | break; 521 | default: 522 | formattedContent = textContent; 523 | } 524 | 525 | if (formattedContent) { 526 | contentBlocks.push(formattedContent); 527 | } 528 | } 529 | 530 | // Combine all content 531 | let output = `# ${title}\n\n`; 532 | 533 | if (contentBlocks.length > 0) { 534 | output += contentBlocks.join("\n") + "\n\n"; 535 | } 536 | 537 | if (childPages.length > 0) { 538 | output += "## Child Pages\n" + childPages.join("\n") + "\n\n"; 539 | } 540 | 541 | if (childDatabases.length > 0) { 542 | output += "## Child Databases\n" + childDatabases.join("\n") + "\n"; 543 | } 544 | 545 | return { 546 | content: [ 547 | { 548 | type: "text" as const, 549 | text: output.trim(), 550 | }, 551 | ], 552 | }; 553 | } catch (error) { 554 | console.error("Error reading page:", error); 555 | return { 556 | content: [ 557 | { 558 | type: "text" as const, 559 | text: formatError(error), 560 | }, 561 | ], 562 | }; 563 | } 564 | }, 565 | 566 | async create_page(args: unknown) { 567 | const { title, content, parentPageId, properties } = args as any; 568 | 569 | try { 570 | // First try to retrieve as database to check if it's a database parent 571 | let isDatabase = false; 572 | try { 573 | await notion.databases.retrieve({ database_id: parentPageId }); 574 | isDatabase = true; 575 | } catch { 576 | // If not a database, verify it's a valid page 577 | await notion.pages.retrieve({ page_id: parentPageId }); 578 | } 579 | 580 | // Set up properties based on whether it's a database or page 581 | const pageProperties = isDatabase ? properties : { 582 | title: { 583 | type: "title", 584 | title: [ 585 | { 586 | type: "text", 587 | text: { 588 | content: title || "", 589 | }, 590 | }, 591 | ], 592 | } 593 | }; 594 | 595 | // Parse content into blocks 596 | const parseBlocks = (content: string) => { 597 | const lines = content.split('\n'); 598 | const blocks: any[] = []; 599 | let currentCodeBlock: any = null; 600 | 601 | for (let i = 0; i < lines.length; i++) { 602 | const line = lines[i]; 603 | const trimmedLine = line.trim(); 604 | 605 | // Handle code blocks 606 | if (trimmedLine.startsWith('```')) { 607 | if (currentCodeBlock) { 608 | // End code block 609 | blocks.push(currentCodeBlock); 610 | currentCodeBlock = null; 611 | } else { 612 | // Start code block 613 | const language = trimmedLine.slice(3).trim(); 614 | currentCodeBlock = { 615 | object: "block", 616 | type: "code", 617 | code: { 618 | rich_text: [], 619 | language: language || "plain text" 620 | } 621 | }; 622 | } 623 | continue; 624 | } 625 | 626 | if (currentCodeBlock) { 627 | // Add line to current code block 628 | currentCodeBlock.code.rich_text.push({ 629 | type: "text", 630 | text: { content: line } 631 | }); 632 | continue; 633 | } 634 | 635 | // Handle other block types 636 | let block: any = null; 637 | 638 | if (trimmedLine === '') { 639 | block = { 640 | object: "block", 641 | type: "paragraph", 642 | paragraph: { rich_text: [] } 643 | }; 644 | } else if (trimmedLine.startsWith('# ')) { 645 | block = { 646 | object: "block", 647 | type: "heading_1", 648 | heading_1: { 649 | rich_text: [{ 650 | type: "text", 651 | text: { content: trimmedLine.substring(2) } 652 | }] 653 | } 654 | }; 655 | } else if (trimmedLine.startsWith('## ')) { 656 | block = { 657 | object: "block", 658 | type: "heading_2", 659 | heading_2: { 660 | rich_text: [{ 661 | type: "text", 662 | text: { content: trimmedLine.substring(3) } 663 | }] 664 | } 665 | }; 666 | } else if (trimmedLine.startsWith('### ')) { 667 | block = { 668 | object: "block", 669 | type: "heading_3", 670 | heading_3: { 671 | rich_text: [{ 672 | type: "text", 673 | text: { content: trimmedLine.substring(4) } 674 | }] 675 | } 676 | }; 677 | } else if (trimmedLine.startsWith('- [ ] ')) { 678 | block = { 679 | object: "block", 680 | type: "to_do", 681 | to_do: { 682 | rich_text: [{ 683 | type: "text", 684 | text: { content: trimmedLine.substring(6) } 685 | }], 686 | checked: false 687 | } 688 | }; 689 | } else if (trimmedLine.startsWith('- [x] ')) { 690 | block = { 691 | object: "block", 692 | type: "to_do", 693 | to_do: { 694 | rich_text: [{ 695 | type: "text", 696 | text: { content: trimmedLine.substring(6) } 697 | }], 698 | checked: true 699 | } 700 | }; 701 | } else if (trimmedLine.startsWith('- ')) { 702 | block = { 703 | object: "block", 704 | type: "bulleted_list_item", 705 | bulleted_list_item: { 706 | rich_text: [{ 707 | type: "text", 708 | text: { content: trimmedLine.substring(2) } 709 | }] 710 | } 711 | }; 712 | } else if (trimmedLine.startsWith('> ')) { 713 | block = { 714 | object: "block", 715 | type: "quote", 716 | quote: { 717 | rich_text: [{ 718 | type: "text", 719 | text: { content: trimmedLine.substring(2) } 720 | }] 721 | } 722 | }; 723 | } else if (trimmedLine.startsWith('---')) { 724 | block = { 725 | object: "block", 726 | type: "divider", 727 | divider: {} 728 | }; 729 | } else if (trimmedLine.match(/^!\[.*\]\(.*\)$/)) { 730 | // Image in markdown format: ![alt](url) 731 | const match = trimmedLine.match(/^!\[(.*)\]\((.*)\)$/); 732 | if (match) { 733 | block = { 734 | object: "block", 735 | type: "image", 736 | image: { 737 | type: "external", 738 | external: { url: match[2] }, 739 | caption: match[1] ? [{ 740 | type: "text", 741 | text: { content: match[1] } 742 | }] : [] 743 | } 744 | }; 745 | } 746 | } else { 747 | block = { 748 | object: "block", 749 | type: "paragraph", 750 | paragraph: { 751 | rich_text: [{ 752 | type: "text", 753 | text: { content: line } 754 | }] 755 | } 756 | }; 757 | } 758 | 759 | if (block) { 760 | blocks.push(block); 761 | } 762 | } 763 | 764 | // Add any remaining code block 765 | if (currentCodeBlock) { 766 | blocks.push(currentCodeBlock); 767 | } 768 | 769 | return blocks; 770 | }; 771 | 772 | const newPage = await notion.pages.create({ 773 | parent: isDatabase ? { 774 | type: "database_id", 775 | database_id: parentPageId 776 | } : { 777 | type: "page_id", 778 | page_id: parentPageId 779 | }, 780 | properties: pageProperties, 781 | children: content ? parseBlocks(content) : [] 782 | }); 783 | 784 | return { 785 | content: [ 786 | { 787 | type: "text" as const, 788 | text: `Successfully created page with ID: ${newPage.id}`, 789 | }, 790 | ], 791 | }; 792 | } catch (error) { 793 | console.error("Error creating page:", error); 794 | return { 795 | content: [ 796 | { 797 | type: "text" as const, 798 | text: formatError(error), 799 | }, 800 | ], 801 | }; 802 | } 803 | }, 804 | 805 | async update_page(args: unknown) { 806 | const { pageId, content: newContent, type = "paragraph", mode = "replace", position = "end" } = schemas.toolInputs.updatePage.parse(args); 807 | try { 808 | // Get existing blocks and delete them 809 | const blocks = await notion.blocks.children.list({ block_id: pageId }); 810 | const backup = blocks.results; 811 | 812 | // Helper to create blocks array based on content type and handle multiple lines 813 | const createBlocks = (content: string, _type: string): any[] => { 814 | const lines = content.split('\n'); 815 | const blocks: any[] = []; 816 | let currentCodeBlock: any = null; 817 | 818 | for (let i = 0; i < lines.length; i++) { 819 | const line = lines[i]; 820 | const trimmedLine = line.trim(); 821 | 822 | // Handle code blocks 823 | if (trimmedLine.startsWith('```')) { 824 | if (currentCodeBlock) { 825 | // End code block 826 | blocks.push(currentCodeBlock); 827 | currentCodeBlock = null; 828 | } else { 829 | // Start code block 830 | const language = trimmedLine.slice(3).trim(); 831 | currentCodeBlock = { 832 | object: "block", 833 | type: "code", 834 | code: { 835 | rich_text: [], 836 | language: language || "plain text" 837 | } 838 | }; 839 | } 840 | continue; 841 | } 842 | 843 | if (currentCodeBlock) { 844 | // Add line to current code block 845 | currentCodeBlock.code.rich_text.push({ 846 | type: "text", 847 | text: { content: line } 848 | }); 849 | continue; 850 | } 851 | 852 | // Handle other block types 853 | let block: any = null; 854 | 855 | if (trimmedLine === '') { 856 | block = { 857 | object: "block", 858 | type: "paragraph", 859 | paragraph: { rich_text: [] } 860 | }; 861 | } else if (trimmedLine.startsWith('# ')) { 862 | block = { 863 | object: "block", 864 | type: "heading_1", 865 | heading_1: { 866 | rich_text: [{ 867 | type: "text", 868 | text: { content: trimmedLine.substring(2) } 869 | }] 870 | } 871 | }; 872 | } else if (trimmedLine.startsWith('## ')) { 873 | block = { 874 | object: "block", 875 | type: "heading_2", 876 | heading_2: { 877 | rich_text: [{ 878 | type: "text", 879 | text: { content: trimmedLine.substring(3) } 880 | }] 881 | } 882 | }; 883 | } else if (trimmedLine.startsWith('### ')) { 884 | block = { 885 | object: "block", 886 | type: "heading_3", 887 | heading_3: { 888 | rich_text: [{ 889 | type: "text", 890 | text: { content: trimmedLine.substring(4) } 891 | }] 892 | } 893 | }; 894 | } else if (trimmedLine.startsWith('- [ ] ')) { 895 | block = { 896 | object: "block", 897 | type: "to_do", 898 | to_do: { 899 | rich_text: [{ 900 | type: "text", 901 | text: { content: trimmedLine.substring(6) } 902 | }], 903 | checked: false 904 | } 905 | }; 906 | } else if (trimmedLine.startsWith('- [x] ')) { 907 | block = { 908 | object: "block", 909 | type: "to_do", 910 | to_do: { 911 | rich_text: [{ 912 | type: "text", 913 | text: { content: trimmedLine.substring(6) } 914 | }], 915 | checked: true 916 | } 917 | }; 918 | } else if (trimmedLine.startsWith('- ')) { 919 | block = { 920 | object: "block", 921 | type: "bulleted_list_item", 922 | bulleted_list_item: { 923 | rich_text: [{ 924 | type: "text", 925 | text: { content: trimmedLine.substring(2) } 926 | }] 927 | } 928 | }; 929 | } else if (trimmedLine.startsWith('> ')) { 930 | block = { 931 | object: "block", 932 | type: "quote", 933 | quote: { 934 | rich_text: [{ 935 | type: "text", 936 | text: { content: trimmedLine.substring(2) } 937 | }] 938 | } 939 | }; 940 | } else if (trimmedLine.startsWith('---')) { 941 | block = { 942 | object: "block", 943 | type: "divider", 944 | divider: {} 945 | }; 946 | } else if (trimmedLine.match(/^!\[.*\]\(.*\)$/)) { 947 | // Image in markdown format: ![alt](url) 948 | const match = trimmedLine.match(/^!\[(.*)\]\((.*)\)$/); 949 | if (match) { 950 | block = { 951 | object: "block", 952 | type: "image", 953 | image: { 954 | type: "external", 955 | external: { url: match[2] }, 956 | caption: match[1] ? [{ 957 | type: "text", 958 | text: { content: match[1] } 959 | }] : [] 960 | } 961 | }; 962 | } 963 | } else { 964 | block = { 965 | object: "block", 966 | type: "paragraph", 967 | paragraph: { 968 | rich_text: [{ 969 | type: "text", 970 | text: { content: line } 971 | }] 972 | } 973 | }; 974 | } 975 | 976 | if (block) { 977 | blocks.push(block); 978 | } 979 | } 980 | 981 | // Add any remaining code block 982 | if (currentCodeBlock) { 983 | blocks.push(currentCodeBlock); 984 | } 985 | 986 | return blocks; 987 | }; 988 | if (mode === "replace" || mode === "merge") { 989 | if (backup.length > 0) { 990 | console.warn(`Deleting ${backup.length} existing blocks`); 991 | } 992 | for (const block of backup) { 993 | await notion.blocks.delete({ block_id: block.id }); 994 | } 995 | } try { 996 | const newBlocks = createBlocks(newContent, type); 997 | 998 | if (mode === "merge") { 999 | const mergedBlocks = position === "start" 1000 | ? [...newBlocks, ...backup] 1001 | : [...backup, ...newBlocks]; 1002 | 1003 | await notion.blocks.children.append({ 1004 | block_id: pageId, 1005 | children: mergedBlocks, 1006 | }); 1007 | } else { 1008 | await notion.blocks.children.append({ 1009 | block_id: pageId, 1010 | children: newBlocks, 1011 | }); 1012 | } 1013 | 1014 | return { 1015 | content: [ 1016 | { 1017 | type: "text" as const, 1018 | text: `Successfully updated page: ${pageId}`, 1019 | }, 1020 | ], 1021 | }; 1022 | }catch(error){ 1023 | throw error; 1024 | }}catch (error) { 1025 | console.error("Error updating page:", error); 1026 | return { 1027 | content: [ 1028 | { 1029 | type: "text" as const, 1030 | text: formatError(error), 1031 | }, 1032 | ], 1033 | }; 1034 | } 1035 | }, 1036 | async add_comment(args: unknown) { 1037 | const { pageId, content } = args as any; 1038 | 1039 | try { 1040 | const response = await notion.comments.create({ 1041 | parent: { page_id: pageId }, 1042 | rich_text: [ 1043 | { 1044 | type: "text", 1045 | text: { content }, 1046 | }, 1047 | ], 1048 | }); 1049 | 1050 | return { 1051 | content: [ 1052 | { 1053 | type: "text" as const, 1054 | text: `Successfully added comment`, 1055 | }, 1056 | ], 1057 | }; 1058 | } catch (error) { 1059 | console.error("Error adding comment:", error); 1060 | return { 1061 | content: [ 1062 | { 1063 | type: "text" as const, 1064 | text: `Error adding comment: ${formatError(error)}`, 1065 | }, 1066 | ], 1067 | }; 1068 | } 1069 | }, 1070 | 1071 | async retrieve_comments(args: unknown) { 1072 | const { pageId, startCursor, pageSize } = args as any; 1073 | 1074 | try { 1075 | const response = await notion.comments.list({ 1076 | block_id: pageId, 1077 | start_cursor: startCursor, 1078 | page_size: pageSize, 1079 | }); 1080 | 1081 | return { 1082 | content: [ 1083 | { 1084 | type: "text" as const, 1085 | text: JSON.stringify(response.results, null, 2), 1086 | }, 1087 | ], 1088 | }; 1089 | } catch (error) { 1090 | console.error("Error retrieving comments:", error); 1091 | return { 1092 | content: [ 1093 | { 1094 | type: "text" as const, 1095 | text: `Error retrieving comments: ${formatError(error)}`, 1096 | }, 1097 | ], 1098 | }; 1099 | } 1100 | }, 1101 | 1102 | async create_database(args: unknown) { 1103 | const { parentPageId, title, properties } = args as any; 1104 | 1105 | try { 1106 | const response = await notion.databases.create({ 1107 | parent: { 1108 | type: "page_id", 1109 | page_id: parentPageId, 1110 | }, 1111 | title: [ 1112 | { 1113 | type: "text", 1114 | text: { 1115 | content: title, 1116 | }, 1117 | }, 1118 | ], 1119 | properties, 1120 | }); 1121 | 1122 | return { 1123 | content: [ 1124 | { 1125 | type: "text" as const, 1126 | text: `Successfully created database with ID: ${response.id}`, 1127 | }, 1128 | ], 1129 | }; 1130 | } catch (error) { 1131 | console.error("Error creating database:", error); 1132 | return { 1133 | content: [ 1134 | { 1135 | type: "text" as const, 1136 | text: `Error creating database: ${formatError(error)}`, 1137 | }, 1138 | ], 1139 | }; 1140 | } 1141 | }, 1142 | 1143 | async query_database(args: unknown) { 1144 | const { databaseId, filter, sort } = args as any; 1145 | 1146 | try { 1147 | const response = await notion.databases.query({ 1148 | database_id: databaseId, 1149 | filter, 1150 | sorts: sort ? [sort] : undefined, 1151 | }); 1152 | 1153 | return { 1154 | content: [ 1155 | { 1156 | type: "text" as const, 1157 | text: JSON.stringify(response.results, null, 2), 1158 | }, 1159 | ], 1160 | }; 1161 | } catch (error) { 1162 | console.error("Error querying database:", error); 1163 | return { 1164 | content: [ 1165 | { 1166 | type: "text" as const, 1167 | text: `Error querying database: ${formatError(error)}`, 1168 | }, 1169 | ], 1170 | }; 1171 | } 1172 | }, 1173 | async update_block(args: unknown) { 1174 | const { blockId, content, type = "paragraph" } = args as any; 1175 | 1176 | try { 1177 | const response = await notion.blocks.update({ 1178 | block_id: blockId, 1179 | [type]: { 1180 | rich_text: [ 1181 | { 1182 | type: "text", 1183 | text: { 1184 | content: content, 1185 | }, 1186 | }, 1187 | ], 1188 | }, 1189 | }); 1190 | 1191 | return { 1192 | content: [ 1193 | { 1194 | type: "text" as const, 1195 | text: `Successfully updated block`, 1196 | }, 1197 | ], 1198 | }; 1199 | } catch (error) { 1200 | console.error("Error updating block:", error); 1201 | return { 1202 | content: [ 1203 | { 1204 | type: "text" as const, 1205 | text: formatError(error), 1206 | }, 1207 | ], 1208 | }; 1209 | } 1210 | }, 1211 | 1212 | async delete_block(args: unknown) { 1213 | const { blockId } = args as any; 1214 | 1215 | try { 1216 | await notion.blocks.delete({ 1217 | block_id: blockId, 1218 | }); 1219 | 1220 | return { 1221 | content: [ 1222 | { 1223 | type: "text" as const, 1224 | text: "Successfully deleted block", 1225 | }, 1226 | ], 1227 | }; 1228 | } catch (error) { 1229 | console.error("Error deleting block:", error); 1230 | return { 1231 | content: [ 1232 | { 1233 | type: "text" as const, 1234 | text: formatError(error), 1235 | }, 1236 | ], 1237 | }; 1238 | } 1239 | }, 1240 | async update_database_item(args: unknown) { 1241 | const { pageId, properties } = args as any; 1242 | 1243 | try { 1244 | const response = await notion.pages.update({ 1245 | page_id: pageId, 1246 | properties, 1247 | }); 1248 | 1249 | return { 1250 | content: [ 1251 | { 1252 | type: "text" as const, 1253 | text: `Successfully updated database item`, 1254 | }, 1255 | ], 1256 | }; 1257 | } catch (error) { 1258 | console.error("Error updating database item:", error); 1259 | return { 1260 | content: [ 1261 | { 1262 | type: "text" as const, 1263 | text: formatError(error), 1264 | }, 1265 | ], 1266 | }; 1267 | } 1268 | }, 1269 | async retrieve_database(args: unknown) { 1270 | const { databaseId } = args as any; 1271 | 1272 | try { 1273 | const response = await notion.databases.retrieve({ 1274 | database_id: databaseId, 1275 | }); 1276 | 1277 | return { 1278 | content: [ 1279 | { 1280 | type: "text" as const, 1281 | text: JSON.stringify(response, null, 2), 1282 | }, 1283 | ], 1284 | }; 1285 | } catch (error) { 1286 | console.error("Error retrieving database:", error); 1287 | return { 1288 | content: [ 1289 | { 1290 | type: "text" as const, 1291 | text: formatError(error), 1292 | }, 1293 | ], 1294 | }; 1295 | } 1296 | }, 1297 | 1298 | async update_database(args: unknown) { 1299 | const { databaseId, title, description, properties } = args as any; 1300 | 1301 | try { 1302 | const response = await notion.databases.update({ 1303 | database_id: databaseId, 1304 | title: title 1305 | ? [ 1306 | { 1307 | type: "text", 1308 | text: { content: title }, 1309 | }, 1310 | ] 1311 | : undefined, 1312 | description: description 1313 | ? [ 1314 | { 1315 | type: "text", 1316 | text: { content: description }, 1317 | }, 1318 | ] 1319 | : undefined, 1320 | properties, 1321 | }); 1322 | 1323 | return { 1324 | content: [ 1325 | { 1326 | type: "text" as const, 1327 | text: `Successfully updated database with ID: ${response.id}`, 1328 | }, 1329 | ], 1330 | }; 1331 | } catch (error) { 1332 | console.error("Error updating database:", error); 1333 | return { 1334 | content: [ 1335 | { 1336 | type: "text" as const, 1337 | text: formatError(error), 1338 | }, 1339 | ], 1340 | }; 1341 | } 1342 | }, 1343 | }; 1344 | 1345 | // Initialize MCP server 1346 | const server = new Server( 1347 | { 1348 | name: "notion-server", 1349 | version: "1.0.0", 1350 | }, 1351 | { 1352 | capabilities: { 1353 | tools: {}, 1354 | }, 1355 | }, 1356 | ); 1357 | 1358 | // Register tool handlers 1359 | server.setRequestHandler(ListToolsRequestSchema, async () => { 1360 | console.error("Tools requested by client"); 1361 | return { tools: TOOL_DEFINITIONS }; 1362 | }); 1363 | 1364 | server.setRequestHandler(CallToolRequestSchema, async (request, extra) => { 1365 | const { name, arguments: args } = request.params; 1366 | 1367 | try { 1368 | const handler = toolHandlers[name as keyof typeof toolHandlers]; 1369 | if (!handler) { 1370 | throw new Error(`Unknown tool: ${name}`); 1371 | } 1372 | 1373 | return await handler(args); 1374 | } catch (error) { 1375 | console.error(`Error executing tool ${name}:`, error); 1376 | throw error; 1377 | } 1378 | }); 1379 | 1380 | // Start the server 1381 | async function main() { 1382 | if (!process.env.NOTION_API_KEY) { 1383 | throw new Error("NOTION_API_KEY environment variable is required"); 1384 | } 1385 | 1386 | const transport = new StdioServerTransport(); 1387 | await server.connect(transport); 1388 | console.error("Notion MCP Server running on stdio"); 1389 | } 1390 | 1391 | main().catch((error) => { 1392 | console.error("Fatal error:", error); 1393 | process.exit(1); 1394 | }); 1395 | --------------------------------------------------------------------------------