Notion MCP

from mcp.types import Tool, TextContent, EmbeddedResource from mcp.server import Server from typing import Any, Sequence from dotenv import load_dotenv from pathlib import Path import logging import httpx import os # Find and load .env file from project root project_root = Path(__file__).resolve().parent.parent env_path = project_root / '.env' if not env_path.exists(): raise FileNotFoundError(f"No .env file found at {env_path}") load_dotenv(env_path) # Setup logging configuration logging.basicConfig(level=logging.INFO) NOTION_TOKEN = os.getenv("NOTION_TOKEN") DATABSE_ID = os.getenv("DATABSE_ID") PAGE_ID = os.getenv("PAGE_ID") NOTION_VERSION = os.getenv("NOTION_VERSION") NOTION_BASE_URL = os.getenv("NOTION_BASE_URL") # Notion API headers headers = { "Authorization": f"Bearer {NOTION_TOKEN}", "Content-Type": "application/json", "Notion-Version": NOTION_VERSION } # Create a named server server = Server("notion-mcp") async def fetch_todos_on_page(page_id: str) -> list: """ Fetch all to-do items from a Notion page. :param page_id: The ID of the Notion page (UUID format). :return: A list of to-do items with text and their completion status. """ async with httpx.AsyncClient() as client: todos = [] has_more = True next_cursor = None while has_more: # Fetch child blocks from the page response = await client.get( f"{NOTION_BASE_URL}/blocks/{page_id}/children", headers=headers, params={"start_cursor": next_cursor} if next_cursor else None, ) response.raise_for_status() data = response.json() # Extract to-do items for block in data.get("results", []): if block["type"] == "to_do": todo_text = "".join( [text["plain_text"] for text in block["to_do"]["rich_text"]] ) is_checked = block["to_do"]["checked"] todos.append({"text": todo_text, "checked": is_checked, "task_id": block["id"]}) # Handle pagination has_more = data.get("has_more", False) next_cursor = data.get("next_cursor") return todos async def create_todo_on_page(task: str) -> dict: """ Add a to-do item to an existing Notion page (using the PAGE_ID from .env). Args: task (str): The text of the to-do item. Returns: dict: The response from the Notion API. Raises: ValueError: If PAGE_ID is not set in the .env file. httpx.HTTPStatusError: If the request to the Notion API fails. """ if not PAGE_ID: raise ValueError("PAGE_ID is not set in the .env file.") async with httpx.AsyncClient() as client: response = await client.patch( f"{NOTION_BASE_URL}/blocks/{PAGE_ID}/children", headers=headers, json={ "children": [ { "object": "block", "type": "to_do", "to_do": { "rich_text": [ {"type": "text", "text": {"content": task}} ], "checked": False } } ] } ) response.raise_for_status() return response.json() async def complete_todo_on_page(task_id: str) -> None: """ Mark a to-do item as complete in a Notion Page. Args: task_id (str): The task_id of the to-do item to be marked as complete. Raises: ValueError: If there is an error completing the to-do item. Returns: None """ todos = await fetch_todos_on_page(PAGE_ID) if not any(todo.get("task_id") == task_id for todo in todos): raise ValueError(f"No to-do item found with title: {task_id}") # The payload to update the block (to change the 'checked' status) payload = { "to_do": { "checked": True } } try: async with httpx.AsyncClient() as client: response = await client.patch( f"{NOTION_BASE_URL}/blocks/{task_id}", headers=headers, json=payload ) response.raise_for_status() return response.json() except httpx.HTTPError as e: logging.info("No to-do found in the page.") raise ValueError(f"Error completing todo: {str(e)}") async def handle_add_todo(arguments: dict) -> Sequence[TextContent | EmbeddedResource]: """ Handle adding a new to-do. Args: arguments (dict): A dictionary containing the task details. Returns: Sequence[TextContent | EmbeddedResource]: A sequence containing the result of the operation, either a success message or an error message. Raises: ValueError: If the arguments are not a dictionary or if the task is not provided. """ if not isinstance(arguments, dict): raise ValueError("Invalid arguments") task = arguments.get("task") if not task: raise ValueError("Task is required") try: result = await create_todo_on_page(task) return [ TextContent( type="text", text=f"Added todo: {task} in the Task Integration Page" ) ] except httpx.HTTPError as e: logging.error(f"Notion API error: {str(e)}") return [ TextContent( type="text", text=f"Error adding todo: {str(e)}\nPlease make sure your Notion integration is properly set up and has access to the database." ) ] async def handle_show_all_todos() -> Sequence[TextContent | EmbeddedResource]: """ Handle showing all to-do items. Fetches all to-do items from a specific page and returns them as a list of TextContent objects. If no to-do items are found, returns a message indicating that no items were found. Returns: Sequence[TextContent | EmbeddedResource]: A list containing a TextContent object with the to-do items or a message indicating no items were found. """ todos = await fetch_todos_on_page(PAGE_ID) if todos: todo_list = "\n".join([ f"- {todo['task_id']}: {todo['text']} (Completed: {'Yes' if todo['checked'] else 'No'})" for todo in todos ]) return [ TextContent( type="text", text=f"Here are the todo items in the Task Integration Page:\n{todo_list}" ) ] else: return [ TextContent( type="text", text="No todo items found in the Task Integration Page." ) ] async def handle_complete_todo(arguments: dict) -> Sequence[TextContent | EmbeddedResource]: """ Handle completing a to-do item. Args: arguments (dict): A dictionary containing the task ID. Returns: Sequence[TextContent | EmbeddedResource]: A sequence containing the result of the operation, either a success message or an error message. Raises: ValueError: If the arguments are not a dictionary or if the task ID is not provided. httpx.HTTPStatusError: If the request to the Notion API fails. """ if not isinstance(arguments, dict): raise ValueError("Invalid arguments") task_id = arguments.get("task_id") if not task_id: raise ValueError("Task_ID is required") try: await complete_todo_on_page(task_id) return [ TextContent( type="text", text=f"Marked todo as complete (Task_ID: {task_id})" ) ] except httpx.HTTPError as e: logging.error(f"Notion API error: {str(e)}") return [ TextContent( type="text", text=f"Error completing todo: {str(e)}\nPlease make sure your Notion integration is properly set up and has access to the database." ) ] @server.list_tools() async def list_tools() -> list[Tool]: """ List all available tools. Returns: list[Tool]: A list of available tools. """ return [ Tool( name="add_todo", description="Add a new todo item", inputSchema={ "type": "object", "properties": { "task": { "type": "string", "description": "The todo task description", }, }, "required": ["task"] } ), Tool( name="show_all_todos", description="Show all todo items from Notion.", inputSchema={ "type": "object", "properties": {}, "required": [] } ), Tool( name="complete_todo", description="Mark a todo item as completed.", inputSchema={ "type": "object", "properties": { "task_id": { "type": "string", "description": "The task_id of the todo task to mark as complete." } }, "required": ["task_id"] } ), ] @server.call_tool() async def call_tool(name: str, arguments: Any) -> Sequence[TextContent | EmbeddedResource]: """ Handle tool calls for todo management. Args: name (str): The name of the tool to call. arguments (Any): The arguments to pass to the tool. Returns: Sequence[TextContent | EmbeddedResource]: The result of the tool call, either a success message or an error message. """ if name == "add_todo": return await handle_add_todo(arguments) elif name == "show_all_todos" or name == "show_todos": return await handle_show_all_todos() elif name == "complete_todo": return await handle_complete_todo(arguments) raise ValueError(f"Unknown tool: {name}") async def main(): """Main entry point for the server""" from mcp.server.stdio import stdio_server if not NOTION_TOKEN or not PAGE_ID: raise ValueError("NOTION_TOKEN and PAGE_ID environment variables are required") async with stdio_server() as (read_stream, write_stream): await server.run( read_stream, write_stream, server.create_initialization_options() ) if __name__ == "__main__": import asyncio asyncio.run(main())