# Directory Structure ``` ├── .gitignore ├── GUIDE.md ├── LICENSE ├── package-lock.json ├── package.json ├── README.md ├── src │ ├── client.ts │ ├── config.ts │ ├── index.ts │ ├── models │ │ └── Todo.ts │ ├── services │ │ ├── DatabaseService.ts │ │ └── TodoService.ts │ └── utils │ └── formatters.ts └── tsconfig.json ``` # Files -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- ``` 1 | node_modules/ 2 | dist/ 3 | contexts/ 4 | .specstory/ ``` -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- ```markdown 1 | # Todo List MCP Server 2 | 3 | A Model Context Protocol (MCP) server that provides a comprehensive API for managing todo items. 4 | 5 | <a href="https://glama.ai/mcp/servers/kh39rjpplx"> 6 | <img width="380" height="200" src="https://glama.ai/mcp/servers/kh39rjpplx/badge" alt="Todo List Server MCP server" /> 7 | </a> 8 | 9 | > **📚 Learning Resource**: This project is designed as an educational example of MCP implementation. See [GUIDE.md](GUIDE.md) for a comprehensive explanation of how the project works and why things are implemented the way they are. 10 | 11 | ## Features 12 | 13 | - **Create todos**: Add new tasks with title and markdown description 14 | - **Update todos**: Modify existing tasks 15 | - **Complete todos**: Mark tasks as done 16 | - **Delete todos**: Remove tasks from the list 17 | - **Search todos**: Find tasks by title or creation date 18 | - **Summarize todos**: Get a quick overview of active tasks 19 | 20 | ## Tools 21 | 22 | This MCP server exposes the following tools: 23 | 24 | 1. `create-todo`: Create a new todo item 25 | 2. `list-todos`: List all todos 26 | 3. `get-todo`: Get a specific todo by ID 27 | 4. `update-todo`: Update a todo's title or description 28 | 5. `complete-todo`: Mark a todo as completed 29 | 6. `delete-todo`: Delete a todo 30 | 7. `search-todos-by-title`: Search todos by title (case-insensitive partial match) 31 | 8. `search-todos-by-date`: Search todos by creation date (format: YYYY-MM-DD) 32 | 9. `list-active-todos`: List all non-completed todos 33 | 10. `summarize-active-todos`: Generate a summary of all active (non-completed) todos 34 | 35 | ## Installation 36 | 37 | ```bash 38 | # Clone the repository 39 | git clone https://github.com/RegiByte/todo-list-mcp.git 40 | cd todo-list-mcp 41 | 42 | # Install dependencies 43 | npm install 44 | 45 | # Build the project 46 | npm run build 47 | ``` 48 | 49 | ## Usage 50 | 51 | ### Starting the Server 52 | 53 | ```bash 54 | npm start 55 | ``` 56 | 57 | ### Configuring with Claude for Desktop 58 | 59 | #### Claude Desktop 60 | 61 | Add this to your `claude_desktop_config.json`: 62 | 63 | ```json 64 | { 65 | "mcpServers": { 66 | "todo": { 67 | "command": "node", 68 | "args": ["/absolute/path/to/todo-list-mcp/dist/index.js"] 69 | } 70 | } 71 | } 72 | ``` 73 | 74 | #### Cursor 75 | 76 | - Go to "Cursor Settings" -> MCP 77 | - Add a new MCP server with a "command" type 78 | - Add the absolute path of the server and run it with node 79 | - Example: node /absolute/path/to/todo-list-mcp/dist/index.js 80 | 81 | ### Example Commands 82 | 83 | When using with Claude for Desktop or Cursor, you can try: 84 | 85 | - "Create a todo to learn MCP with a description explaining why MCP is useful" 86 | - "List all my active todos" 87 | - "Create a todo for tomorrow's meeting with details about the agenda in markdown" 88 | - "Mark my learning MCP todo as completed" 89 | - "Summarize all my active todos" 90 | 91 | ## Project Structure 92 | 93 | This project follows a clear separation of concerns to make the code easy to understand: 94 | 95 | ``` 96 | src/ 97 | ├── models/ # Data structures and validation schemas 98 | ├── services/ # Business logic and database operations 99 | ├── utils/ # Helper functions and formatters 100 | ├── config.ts # Configuration settings 101 | ├── client.ts # Test client for local testing 102 | └── index.ts # Main entry point with MCP tool definitions 103 | ``` 104 | 105 | ## Learning from This Project 106 | 107 | This project is designed as an educational resource. To get the most out of it: 108 | 109 | 1. Read the [GUIDE.md](GUIDE.md) for a comprehensive explanation of the design 110 | 2. Study the heavily commented source code to understand implementation details 111 | 3. Use the test client to see how the server works in practice 112 | 4. Experiment with adding your own tools or extending the existing ones 113 | 114 | ## Development 115 | 116 | ### Building 117 | 118 | ```bash 119 | npm run build 120 | ``` 121 | 122 | ### Running in Development Mode 123 | 124 | ```bash 125 | npm run dev 126 | ``` 127 | 128 | ## License 129 | 130 | MIT ``` -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "module": "Node16", 5 | "moduleResolution": "Node16", 6 | "outDir": "./dist", 7 | "rootDir": "./src", 8 | "strict": true, 9 | "esModuleInterop": true, 10 | "skipLibCheck": true, 11 | "forceConsistentCasingInFileNames": true 12 | }, 13 | "include": ["src/**/*"], 14 | "exclude": ["node_modules"] 15 | } ``` -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "name": "todo-list-mcp", 3 | "version": "1.0.0", 4 | "main": "dist/index.js", 5 | "type": "module", 6 | "scripts": { 7 | "build": "tsc", 8 | "start": "node dist/index.js", 9 | "dev": "npm run build && npm start", 10 | "test": "npm run build && node dist/client.js", 11 | "inspector": "npx @modelcontextprotocol/inspector node dist/index.js" 12 | }, 13 | "keywords": [ 14 | "mcp", 15 | "todo", 16 | "api" 17 | ], 18 | "author": "", 19 | "license": "ISC", 20 | "description": "Todo list MCP server", 21 | "dependencies": { 22 | "@modelcontextprotocol/sdk": "^1.6.1", 23 | "better-sqlite3": "^9.4.1", 24 | "typescript": "^5.3.3", 25 | "uuid": "^9.0.1", 26 | "zod": "^3.22.4" 27 | }, 28 | "devDependencies": { 29 | "@modelcontextprotocol/inspector": "^0.5.1", 30 | "@types/better-sqlite3": "^7.6.9", 31 | "@types/node": "^20.11.28", 32 | "@types/uuid": "^9.0.8" 33 | } 34 | } 35 | ``` -------------------------------------------------------------------------------- /src/config.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * config.ts 3 | * 4 | * This file manages the application configuration settings. 5 | * It provides a centralized place for all configuration values, 6 | * making them easier to change and maintain. 7 | * 8 | * WHY A SEPARATE CONFIG FILE? 9 | * - Single source of truth for configuration values 10 | * - Easy to update settings without searching through the codebase 11 | * - Allows for environment-specific overrides 12 | * - Makes configuration values available throughout the application 13 | */ 14 | import path from 'path'; 15 | import os from 'os'; 16 | import fs from 'fs'; 17 | 18 | /** 19 | * Database configuration defaults 20 | * 21 | * We use the user's home directory for database storage by default, 22 | * which provides several advantages: 23 | * - Works across different operating systems 24 | * - Available without special permissions 25 | * - Persists across application restarts 26 | * - Doesn't get deleted when updating the application 27 | */ 28 | const DEFAULT_DB_FOLDER = path.join(os.homedir(), '.todo-list-mcp'); 29 | const DEFAULT_DB_FILE = 'todos.sqlite'; 30 | 31 | /** 32 | * Application configuration object 33 | * 34 | * This object provides access to all configuration settings. 35 | * It uses environment variables when available, falling back to defaults. 36 | * 37 | * WHY USE ENVIRONMENT VARIABLES? 38 | * - Allows configuration without changing code 39 | * - Follows the 12-factor app methodology for configuration 40 | * - Enables different settings per environment (dev, test, prod) 41 | * - Keeps sensitive information out of the code 42 | */ 43 | export const config = { 44 | db: { 45 | // Allow overriding through environment variables 46 | folder: process.env.TODO_DB_FOLDER || DEFAULT_DB_FOLDER, 47 | filename: process.env.TODO_DB_FILE || DEFAULT_DB_FILE, 48 | 49 | /** 50 | * Full path to the database file 51 | * 52 | * This getter computes the complete path dynamically, 53 | * ensuring consistency even if the folder or filename change. 54 | */ 55 | get path() { 56 | return path.join(this.folder, this.filename); 57 | } 58 | } 59 | }; 60 | 61 | /** 62 | * Ensure the database folder exists 63 | * 64 | * This utility function makes sure the folder for the database file exists, 65 | * creating it if necessary. This prevents errors when trying to open the 66 | * database file in a non-existent directory. 67 | */ 68 | export function ensureDbFolder() { 69 | if (!fs.existsSync(config.db.folder)) { 70 | fs.mkdirSync(config.db.folder, { recursive: true }); 71 | } 72 | } ``` -------------------------------------------------------------------------------- /src/services/DatabaseService.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * DatabaseService.ts 3 | * 4 | * This file implements a lightweight SQLite database service for the Todo application. 5 | * 6 | * WHY SQLITE? 7 | * - SQLite is perfect for small to medium applications like this one 8 | * - Requires no separate database server (file-based) 9 | * - ACID compliant and reliable 10 | * - Minimal configuration required 11 | * - Easy to install with minimal dependencies 12 | */ 13 | import Database from 'better-sqlite3'; 14 | import { config, ensureDbFolder } from '../config.js'; 15 | 16 | /** 17 | * DatabaseService Class 18 | * 19 | * This service manages the SQLite database connection and schema. 20 | * It follows the singleton pattern to ensure only one database connection exists. 21 | * 22 | * WHY SINGLETON PATTERN? 23 | * - Prevents multiple database connections which could lead to conflicts 24 | * - Provides a central access point to the database throughout the application 25 | * - Makes it easier to manage connection lifecycle (open/close) 26 | */ 27 | class DatabaseService { 28 | private db: Database.Database; 29 | 30 | constructor() { 31 | // Ensure the database folder exists before trying to create the database 32 | ensureDbFolder(); 33 | 34 | // Initialize the database with the configured path 35 | this.db = new Database(config.db.path); 36 | 37 | /** 38 | * Set pragmas for performance and safety: 39 | * - WAL (Write-Ahead Logging): Improves concurrent access performance 40 | * - foreign_keys: Ensures referential integrity (useful for future expansion) 41 | */ 42 | this.db.pragma('journal_mode = WAL'); 43 | this.db.pragma('foreign_keys = ON'); 44 | 45 | // Initialize the database schema when service is created 46 | this.initSchema(); 47 | } 48 | 49 | /** 50 | * Initialize the database schema 51 | * 52 | * This creates the todos table if it doesn't already exist. 53 | * The schema design incorporates: 54 | * - TEXT primary key for UUID compatibility 55 | * - NULL completedAt to represent incomplete todos 56 | * - Timestamp fields for tracking creation and updates 57 | */ 58 | private initSchema(): void { 59 | // Create todos table if it doesn't exist 60 | this.db.exec(` 61 | CREATE TABLE IF NOT EXISTS todos ( 62 | id TEXT PRIMARY KEY, 63 | title TEXT NOT NULL, 64 | description TEXT NOT NULL, 65 | completedAt TEXT NULL, -- ISO timestamp, NULL if not completed 66 | createdAt TEXT NOT NULL, 67 | updatedAt TEXT NOT NULL 68 | ) 69 | `); 70 | } 71 | 72 | /** 73 | * Get the database instance 74 | * 75 | * This allows other services to access the database for operations. 76 | * 77 | * @returns The SQLite database instance 78 | */ 79 | getDb(): Database.Database { 80 | return this.db; 81 | } 82 | 83 | /** 84 | * Close the database connection 85 | * 86 | * This should be called when shutting down the application to ensure 87 | * all data is properly saved and resources are released. 88 | */ 89 | close(): void { 90 | this.db.close(); 91 | } 92 | } 93 | 94 | // Create a singleton instance that will be used throughout the application 95 | export const databaseService = new DatabaseService(); ``` -------------------------------------------------------------------------------- /src/utils/formatters.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * formatters.ts 3 | * 4 | * This file contains utility functions for formatting data in the application. 5 | * These utilities handle the transformation of internal data structures into 6 | * human-readable formats appropriate for display to LLMs and users. 7 | * 8 | * WHY SEPARATE FORMATTERS? 9 | * - Keeps formatting logic separate from business logic 10 | * - Allows consistent formatting across the application 11 | * - Makes it easier to change display formats without affecting core functionality 12 | * - Centralizes presentation concerns in one place 13 | */ 14 | import { Todo } from "../models/Todo.js"; 15 | 16 | /** 17 | * Format a todo item to a readable string representation 18 | * 19 | * This formatter converts a Todo object into a markdown-formatted string 20 | * with clear visual indicators for completion status (emojis). 21 | * 22 | * WHY USE MARKDOWN? 23 | * - Provides structured, readable output 24 | * - Works well with LLMs which understand markdown syntax 25 | * - Allows rich formatting like headers, lists, and emphasis 26 | * - Can be displayed directly in many UI contexts 27 | * 28 | * @param todo The Todo object to format 29 | * @returns A markdown-formatted string representation 30 | */ 31 | export function formatTodo(todo: Todo): string { 32 | return ` 33 | ## ${todo.title} ${todo.completed ? '✅' : '⏳'} 34 | 35 | ID: ${todo.id} 36 | Created: ${new Date(todo.createdAt).toLocaleString()} 37 | Updated: ${new Date(todo.updatedAt).toLocaleString()} 38 | 39 | ${todo.description} 40 | `.trim(); 41 | } 42 | 43 | /** 44 | * Format a list of todos to a readable string representation 45 | * 46 | * This formatter takes an array of Todo objects and creates a complete 47 | * markdown document with a title and formatted entries. 48 | * 49 | * @param todos Array of Todo objects to format 50 | * @returns A markdown-formatted string with the complete list 51 | */ 52 | export function formatTodoList(todos: Todo[]): string { 53 | if (todos.length === 0) { 54 | return "No todos found."; 55 | } 56 | 57 | const todoItems = todos.map(formatTodo).join('\n\n---\n\n'); 58 | return `# Todo List (${todos.length} items)\n\n${todoItems}`; 59 | } 60 | 61 | /** 62 | * Create success response for MCP tool calls 63 | * 64 | * This utility formats successful responses according to the MCP protocol. 65 | * It wraps the message in the expected content structure. 66 | * 67 | * WHY THIS FORMAT? 68 | * - Follows the MCP protocol's expected response structure 69 | * - Allows the message to be properly displayed by MCP clients 70 | * - Clearly indicates success status 71 | * 72 | * @param message The success message to include 73 | * @returns A properly formatted MCP response object 74 | */ 75 | export function createSuccessResponse(message: string) { 76 | return { 77 | content: [ 78 | { 79 | type: "text" as const, 80 | text: message, 81 | }, 82 | ], 83 | }; 84 | } 85 | 86 | /** 87 | * Create error response for MCP tool calls 88 | * 89 | * This utility formats error responses according to the MCP protocol. 90 | * It includes the isError flag to indicate failure. 91 | * 92 | * @param message The error message to include 93 | * @returns A properly formatted MCP error response object 94 | */ 95 | export function createErrorResponse(message: string) { 96 | return { 97 | content: [ 98 | { 99 | type: "text" as const, 100 | text: message, 101 | }, 102 | ], 103 | isError: true, 104 | }; 105 | } ``` -------------------------------------------------------------------------------- /src/models/Todo.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * Todo.ts 3 | * 4 | * This file defines the core data model for our Todo application, along with validation 5 | * schemas and a factory function for creating new Todo instances. 6 | * 7 | * WHY USE ZOD? 8 | * - Zod provides runtime type validation, ensuring our data meets specific requirements 9 | * - Using schemas creates a clear contract for each operation's input requirements 10 | * - Error messages are automatically generated with clear validation feedback 11 | * - TypeScript integration gives us both compile-time and runtime type safety 12 | * - Schemas can be converted to JSON Schema, which is useful for MCP clients 13 | */ 14 | import { z } from 'zod'; 15 | import { v4 as uuidv4 } from 'uuid'; 16 | 17 | /** 18 | * Todo Interface 19 | * 20 | * This defines the structure of a Todo item in our application. 21 | * We've designed it with several important considerations: 22 | * - IDs use UUID for uniqueness across systems 23 | * - Timestamps track creation and updates for data lifecycle management 24 | * - Description supports markdown for rich text formatting 25 | * - Completion status is tracked both as a boolean flag and with a timestamp 26 | */ 27 | export interface Todo { 28 | id: string; 29 | title: string; 30 | description: string; // Markdown format 31 | completed: boolean; // Computed from completedAt for backward compatibility 32 | completedAt: string | null; // ISO timestamp when completed, null if not completed 33 | createdAt: string; 34 | updatedAt: string; 35 | } 36 | 37 | /** 38 | * Input Validation Schemas 39 | * 40 | * These schemas define the requirements for different operations. 41 | * Each schema serves as both documentation and runtime validation. 42 | * 43 | * WHY SEPARATE SCHEMAS? 44 | * - Different operations have different validation requirements 45 | * - Keeps validation focused on only what's needed for each operation 46 | * - Makes the API more intuitive by clearly defining what each operation expects 47 | */ 48 | 49 | // Schema for creating a new todo - requires title and description 50 | export const CreateTodoSchema = z.object({ 51 | title: z.string().min(1, "Title is required"), 52 | description: z.string().min(1, "Description is required"), 53 | }); 54 | 55 | // Schema for updating a todo - requires ID, title and description are optional 56 | export const UpdateTodoSchema = z.object({ 57 | id: z.string().uuid("Invalid Todo ID"), 58 | title: z.string().min(1, "Title is required").optional(), 59 | description: z.string().min(1, "Description is required").optional(), 60 | }); 61 | 62 | // Schema for completing a todo - requires only ID 63 | export const CompleteTodoSchema = z.object({ 64 | id: z.string().uuid("Invalid Todo ID"), 65 | }); 66 | 67 | // Schema for deleting a todo - requires only ID 68 | export const DeleteTodoSchema = z.object({ 69 | id: z.string().uuid("Invalid Todo ID"), 70 | }); 71 | 72 | // Schema for searching todos by title - requires search term 73 | export const SearchTodosByTitleSchema = z.object({ 74 | title: z.string().min(1, "Search term is required"), 75 | }); 76 | 77 | // Schema for searching todos by date - requires date in YYYY-MM-DD format 78 | export const SearchTodosByDateSchema = z.object({ 79 | date: z.string().regex(/^\d{4}-\d{2}-\d{2}$/, "Date must be in YYYY-MM-DD format"), 80 | }); 81 | 82 | /** 83 | * Factory Function: createTodo 84 | * 85 | * WHY USE A FACTORY FUNCTION? 86 | * - Centralizes the creation logic in one place 87 | * - Ensures all required fields are set with proper default values 88 | * - Guarantees all todos have the same structure 89 | * - Makes it easy to change the implementation without affecting code that creates todos 90 | * 91 | * @param data The validated input data 92 | * @returns A fully formed Todo object with generated ID and timestamps 93 | */ 94 | export function createTodo(data: z.infer<typeof CreateTodoSchema>): Todo { 95 | const now = new Date().toISOString(); 96 | return { 97 | id: uuidv4(), 98 | title: data.title, 99 | description: data.description, 100 | completed: false, 101 | completedAt: null, 102 | createdAt: now, 103 | updatedAt: now, 104 | }; 105 | } ``` -------------------------------------------------------------------------------- /src/client.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * client.ts 3 | * 4 | * This file implements a test client for the Todo MCP server. 5 | * It demonstrates how to connect to the server, call various tools, 6 | * and handle the responses. 7 | * 8 | * WHY HAVE A TEST CLIENT? 9 | * - Validates that the server works correctly 10 | * - Provides a working example of how to use the MCP client SDK 11 | * - Makes it easy to test changes without needing an LLM 12 | * - Serves as documentation for how to interact with the server 13 | */ 14 | import { Client } from "@modelcontextprotocol/sdk/client/index.js"; 15 | import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"; 16 | 17 | /** 18 | * Response content type definition 19 | * 20 | * The MCP protocol returns content as an array of typed objects. 21 | * This interface defines the structure of text content items. 22 | */ 23 | interface ContentText { 24 | type: "text"; 25 | text: string; 26 | } 27 | 28 | /** 29 | * Main function that runs the test client 30 | * 31 | * This function: 32 | * 1. Connects to the Todo MCP server 33 | * 2. Demonstrates all the available tools 34 | * 3. Creates, updates, completes, and deletes a test todo 35 | */ 36 | async function main() { 37 | console.log("Starting Todo MCP Test Client..."); 38 | 39 | try { 40 | /** 41 | * Create a client transport to the server 42 | * 43 | * The StdioClientTransport launches the server as a child process 44 | * and communicates with it via standard input/output. 45 | * 46 | * WHY STDIO TRANSPORT? 47 | * - Simple to set up and use 48 | * - Works well for local testing 49 | * - Doesn't require network configuration 50 | * - Similar to how Claude Desktop launches MCP servers 51 | */ 52 | const transport = new StdioClientTransport({ 53 | command: "node", 54 | args: ["dist/index.js"], 55 | }); 56 | 57 | /** 58 | * Create and connect the client 59 | * 60 | * We configure the client with basic identity information 61 | * and the capabilities it needs (tools in this case). 62 | */ 63 | const client = new Client( 64 | { 65 | name: "todo-test-client", 66 | version: "1.0.0", 67 | }, 68 | { 69 | capabilities: { 70 | tools: {} 71 | } 72 | } 73 | ); 74 | 75 | // Connect to the server through the transport 76 | await client.connect(transport); 77 | console.log("Connected to Todo MCP Server"); 78 | 79 | /** 80 | * List available tools 81 | * 82 | * This demonstrates how to query what tools the server provides, 83 | * which is useful for discovery and documentation. 84 | */ 85 | const toolsResult = await client.listTools(); 86 | console.log("\nAvailable tools:", toolsResult.tools.map(tool => tool.name)); 87 | 88 | /** 89 | * Create a test todo 90 | * 91 | * This demonstrates the create-todo tool, which takes a title 92 | * and markdown description as arguments. 93 | */ 94 | console.log("\nCreating a test todo..."); 95 | const createTodoResult = await client.callTool({ 96 | name: "create-todo", 97 | arguments: { 98 | title: "Learn about MCP", 99 | description: "# Model Context Protocol\n\n- Understand core concepts\n- Build a simple server\n- Test with Claude" 100 | } 101 | }); 102 | 103 | // Type assertion to access the content 104 | const createContent = createTodoResult.content as ContentText[]; 105 | console.log(createContent[0].text); 106 | 107 | /** 108 | * Extract the todo ID from the response 109 | * 110 | * We use a simple regex to parse the ID from the formatted response. 111 | * In a real application, you might want a more structured response format. 112 | */ 113 | const idMatch = createContent[0].text.match(/ID: ([0-9a-f-]+)/); 114 | const todoId = idMatch ? idMatch[1] : null; 115 | 116 | // Only proceed if we successfully created a todo and extracted its ID 117 | if (todoId) { 118 | /** 119 | * List all todos 120 | * 121 | * This demonstrates the list-todos tool, which takes no arguments 122 | * and returns a formatted list of all todos. 123 | */ 124 | console.log("\nListing all todos..."); 125 | const listTodosResult = await client.callTool({ 126 | name: "list-todos", 127 | arguments: {} 128 | }); 129 | const listContent = listTodosResult.content as ContentText[]; 130 | console.log(listContent[0].text); 131 | 132 | /** 133 | * Update the todo 134 | * 135 | * This demonstrates the update-todo tool, which takes an ID 136 | * and optional title/description fields to update. 137 | */ 138 | console.log("\nUpdating the test todo..."); 139 | const updateTodoResult = await client.callTool({ 140 | name: "update-todo", 141 | arguments: { 142 | id: todoId, 143 | description: "# Updated MCP Learning Plan\n\n- Learn MCP core concepts\n- Build a server with tools\n- Connect to Claude\n- Create amazing AI experiences" 144 | } 145 | }); 146 | const updateContent = updateTodoResult.content as ContentText[]; 147 | console.log(updateContent[0].text); 148 | 149 | /** 150 | * Mark todo as completed 151 | * 152 | * This demonstrates the complete-todo tool, which takes an ID 153 | * and marks the corresponding todo as completed. 154 | */ 155 | console.log("\nCompleting the test todo..."); 156 | const completeTodoResult = await client.callTool({ 157 | name: "complete-todo", 158 | arguments: { 159 | id: todoId 160 | } 161 | }); 162 | const completeContent = completeTodoResult.content as ContentText[]; 163 | console.log(completeContent[0].text); 164 | 165 | /** 166 | * Summarize active todos 167 | * 168 | * This demonstrates the summarize-active-todos tool, which 169 | * generates a summary of all non-completed todos. 170 | */ 171 | console.log("\nSummarizing active todos..."); 172 | const summaryResult = await client.callTool({ 173 | name: "summarize-active-todos", 174 | arguments: {} 175 | }); 176 | const summaryContent = summaryResult.content as ContentText[]; 177 | console.log(summaryContent[0].text); 178 | 179 | /** 180 | * Delete the todo 181 | * 182 | * This demonstrates the delete-todo tool, which permanently 183 | * removes a todo from the database. 184 | */ 185 | console.log("\nDeleting the test todo..."); 186 | const deleteTodoResult = await client.callTool({ 187 | name: "delete-todo", 188 | arguments: { 189 | id: todoId 190 | } 191 | }); 192 | const deleteContent = deleteTodoResult.content as ContentText[]; 193 | console.log(deleteContent[0].text); 194 | } 195 | 196 | // Close the client connection 197 | await client.close(); 198 | console.log("\nTest completed successfully!"); 199 | } catch (error) { 200 | console.error("Error in test client:", error); 201 | process.exit(1); 202 | } 203 | } 204 | 205 | // Start the test client 206 | main(); ``` -------------------------------------------------------------------------------- /GUIDE.md: -------------------------------------------------------------------------------- ```markdown 1 | # Todo List MCP Server: A Learning Guide 2 | 3 | ## Introduction to Model Context Protocol (MCP) 4 | 5 | The Model Context Protocol (MCP) is a specification that enables AI models like Claude to interact with external tools and services. It creates a standardized way for LLMs to discover, understand, and use tools provided by separate processes. 6 | 7 | ### Why MCP Matters 8 | 9 | 1. **Extended Capabilities**: MCP allows AI models to perform actions beyond just generating text (database operations, file management, API calls, etc.) 10 | 2. **Standardization**: Creates a consistent interface for tools regardless of implementation 11 | 3. **Controlled Access**: Provides a secure way to expose specific functionality to AI models 12 | 4. **Real-time Integration**: Enables AI to access up-to-date information and perform real-world actions 13 | 14 | ## About This Project 15 | 16 | This Todo List MCP Server is designed to be a clear, educational example of how to build an MCP server. It implements a complete todo list management system that can be used by Claude or other MCP-compatible systems. 17 | 18 | ### Learning Objectives 19 | 20 | By studying this codebase, you can learn: 21 | 22 | 1. How to structure an MCP server project 23 | 2. How to implement CRUD operations via MCP tools 24 | 3. Best practices for error handling and validation 25 | 4. How to format responses for AI consumption 26 | 5. How the MCP protocol works in practice 27 | 28 | ## Codebase Structure and Design Philosophy 29 | 30 | The project follows several key design principles: 31 | 32 | ### 1. Clear Separation of Concerns 33 | 34 | The codebase is organized into distinct layers: 35 | 36 | - **Models** (`src/models/`): Data structures and validation schemas 37 | - **Services** (`src/services/`): Business logic and data access 38 | - **Utils** (`src/utils/`): Helper functions and formatters 39 | - **Entry Point** (`src/index.ts`): MCP server definition and tool implementations 40 | 41 | This separation makes the code easier to understand, maintain, and extend. 42 | 43 | ### 2. Type Safety and Validation 44 | 45 | The project uses TypeScript and Zod for comprehensive type safety: 46 | 47 | - **TypeScript Interfaces**: Define data structures with static typing 48 | - **Zod Schemas**: Provide runtime validation with descriptive error messages 49 | - **Consistent Validation**: Each operation validates its inputs before processing 50 | 51 | ### 3. Error Handling 52 | 53 | A consistent error handling approach is used throughout: 54 | 55 | - **Central Error Processing**: The `safeExecute` function standardizes error handling 56 | - **Descriptive Error Messages**: All errors provide clear context about what went wrong 57 | - **Proper Error Responses**: Errors are formatted according to MCP requirements 58 | 59 | ### 4. Data Persistence 60 | 61 | The project uses SQLite for simple but effective data storage: 62 | 63 | - **File-based Database**: Easy to set up with no external dependencies 64 | - **SQL Operations**: Demonstrates parameterized queries and basic CRUD operations 65 | - **Singleton Pattern**: Ensures a single database connection throughout the application 66 | 67 | ## Key Implementation Patterns 68 | 69 | ### The Tool Definition Pattern 70 | 71 | Every MCP tool follows the same pattern: 72 | 73 | ```typescript 74 | server.tool( 75 | "tool-name", // Name: How the tool is identified 76 | "Tool description", // Description: What the tool does 77 | { /* parameter schema */ }, // Schema: Expected inputs with validation 78 | async (params) => { // Handler: The implementation function 79 | // 1. Validate inputs 80 | // 2. Execute business logic 81 | // 3. Format and return response 82 | } 83 | ); 84 | ``` 85 | 86 | ### Error Handling Pattern 87 | 88 | The error handling pattern ensures consistent behavior: 89 | 90 | ```typescript 91 | const result = await safeExecute(() => { 92 | // Operation that might fail 93 | }, "Descriptive error message"); 94 | 95 | if (result instanceof Error) { 96 | return createErrorResponse(result.message); 97 | } 98 | 99 | return createSuccessResponse(formattedResult); 100 | ``` 101 | 102 | ### Response Formatting Pattern 103 | 104 | Responses are consistently formatted for easy consumption by LLMs: 105 | 106 | ```typescript 107 | // Success responses 108 | return createSuccessResponse(`✅ Success message with ${formattedData}`); 109 | 110 | // Error responses 111 | return createErrorResponse(`Error: ${errorMessage}`); 112 | ``` 113 | 114 | ## How to Learn from This Project 115 | 116 | ### For Beginners 117 | 118 | 1. Start by understanding the `Todo` model in `src/models/Todo.ts` 119 | 2. Look at how tools are defined in `src/index.ts` 120 | 3. Explore the basic CRUD operations in `src/services/TodoService.ts` 121 | 4. See how responses are formatted in `src/utils/formatters.ts` 122 | 123 | ### For Intermediate Developers 124 | 125 | 1. Study the error handling patterns throughout the codebase 126 | 2. Look at how validation is implemented with Zod schemas 127 | 3. Examine the database operations and SQL queries 128 | 4. Understand how the MCP tools are organized and structured 129 | 130 | ### For Advanced Developers 131 | 132 | 1. Consider how this approach could be extended for more complex applications 133 | 2. Think about how to add authentication, caching, or more advanced features 134 | 3. Look at the client implementation to understand the full MCP communication cycle 135 | 4. Consider how to implement testing for an MCP server 136 | 137 | ## Running and Testing 138 | 139 | ### Local Testing 140 | 141 | Use the provided test client to see the server in action: 142 | 143 | ```bash 144 | npm run build 145 | node dist/client.js 146 | ``` 147 | 148 | This will run through a complete lifecycle of creating, updating, completing, and deleting a todo. 149 | 150 | ### Integration with Claude for Desktop 151 | 152 | To use this server with Claude for Desktop, add it to your `claude_desktop_config.json`: 153 | 154 | ```json 155 | { 156 | "mcpServers": { 157 | "todo": { 158 | "command": "node", 159 | "args": ["/absolute/path/to/todo-list-mcp/dist/index.js"] 160 | } 161 | } 162 | } 163 | ``` 164 | 165 | ## Common Patterns and Best Practices Demonstrated 166 | 167 | 1. **Singleton Pattern**: Used for database and service access 168 | 2. **Repository Pattern**: Abstracts data access operations 169 | 3. **Factory Pattern**: Creates new Todo objects with consistent structure 170 | 4. **Validation Pattern**: Validates inputs before processing 171 | 5. **Error Handling Pattern**: Centralizes and standardizes error handling 172 | 6. **Formatting Pattern**: Consistently formats outputs for consumption 173 | 7. **Configuration Pattern**: Centralizes application settings 174 | 175 | ## Conclusion 176 | 177 | This Todo List MCP Server demonstrates a clean, well-structured approach to building an MCP server. By studying the code and comments, you can gain a deep understanding of how MCP works and how to implement your own MCP servers for various use cases. 178 | 179 | The project emphasizes not just what code to write, but why specific approaches are taken, making it an excellent learning resource for understanding both MCP and general best practices in TypeScript application development. ``` -------------------------------------------------------------------------------- /src/services/TodoService.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * TodoService.ts 3 | * 4 | * This service implements the core business logic for managing todos. 5 | * It acts as an intermediary between the data model and the database, 6 | * handling all CRUD operations and search functionality. 7 | * 8 | * WHY A SERVICE LAYER? 9 | * - Separates business logic from database operations 10 | * - Provides a clean API for the application to work with 11 | * - Makes it easier to change the database implementation later 12 | * - Encapsulates complex operations into simple method calls 13 | */ 14 | import { Todo, createTodo, CreateTodoSchema, UpdateTodoSchema } from '../models/Todo.js'; 15 | import { z } from 'zod'; 16 | import { databaseService } from './DatabaseService.js'; 17 | 18 | /** 19 | * TodoService Class 20 | * 21 | * This service follows the repository pattern to provide a clean 22 | * interface for working with todos. It encapsulates all database 23 | * operations and business logic in one place. 24 | */ 25 | class TodoService { 26 | /** 27 | * Create a new todo 28 | * 29 | * This method: 30 | * 1. Uses the factory function to create a new Todo object 31 | * 2. Persists it to the database 32 | * 3. Returns the created Todo 33 | * 34 | * @param data Validated input data (title and description) 35 | * @returns The newly created Todo 36 | */ 37 | createTodo(data: z.infer<typeof CreateTodoSchema>): Todo { 38 | // Use the factory function to create a Todo with proper defaults 39 | const todo = createTodo(data); 40 | 41 | // Get the database instance 42 | const db = databaseService.getDb(); 43 | 44 | // Prepare the SQL statement for inserting a new todo 45 | const stmt = db.prepare(` 46 | INSERT INTO todos (id, title, description, completedAt, createdAt, updatedAt) 47 | VALUES (?, ?, ?, ?, ?, ?) 48 | `); 49 | 50 | // Execute the statement with the todo's data 51 | stmt.run( 52 | todo.id, 53 | todo.title, 54 | todo.description, 55 | todo.completedAt, 56 | todo.createdAt, 57 | todo.updatedAt 58 | ); 59 | 60 | // Return the created todo 61 | return todo; 62 | } 63 | 64 | /** 65 | * Get a todo by ID 66 | * 67 | * This method: 68 | * 1. Queries the database for a todo with the given ID 69 | * 2. Converts the database row to a Todo object if found 70 | * 71 | * @param id The UUID of the todo to retrieve 72 | * @returns The Todo if found, undefined otherwise 73 | */ 74 | getTodo(id: string): Todo | undefined { 75 | const db = databaseService.getDb(); 76 | 77 | // Use parameterized query to prevent SQL injection 78 | const stmt = db.prepare('SELECT * FROM todos WHERE id = ?'); 79 | const row = stmt.get(id) as any; 80 | 81 | // Return undefined if no todo was found 82 | if (!row) return undefined; 83 | 84 | // Convert the database row to a Todo object 85 | return this.rowToTodo(row); 86 | } 87 | 88 | /** 89 | * Get all todos 90 | * 91 | * This method returns all todos in the database without filtering. 92 | * 93 | * @returns Array of all Todos 94 | */ 95 | getAllTodos(): Todo[] { 96 | const db = databaseService.getDb(); 97 | const stmt = db.prepare('SELECT * FROM todos'); 98 | const rows = stmt.all() as any[]; 99 | 100 | // Convert each database row to a Todo object 101 | return rows.map(row => this.rowToTodo(row)); 102 | } 103 | 104 | /** 105 | * Get all active (non-completed) todos 106 | * 107 | * This method returns only todos that haven't been marked as completed. 108 | * A todo is considered active when its completedAt field is NULL. 109 | * 110 | * @returns Array of active Todos 111 | */ 112 | getActiveTodos(): Todo[] { 113 | const db = databaseService.getDb(); 114 | const stmt = db.prepare('SELECT * FROM todos WHERE completedAt IS NULL'); 115 | const rows = stmt.all() as any[]; 116 | 117 | // Convert each database row to a Todo object 118 | return rows.map(row => this.rowToTodo(row)); 119 | } 120 | 121 | /** 122 | * Update a todo 123 | * 124 | * This method: 125 | * 1. Checks if the todo exists 126 | * 2. Updates the specified fields 127 | * 3. Returns the updated todo 128 | * 129 | * @param data The update data (id required, title/description optional) 130 | * @returns The updated Todo if found, undefined otherwise 131 | */ 132 | updateTodo(data: z.infer<typeof UpdateTodoSchema>): Todo | undefined { 133 | // First check if the todo exists 134 | const todo = this.getTodo(data.id); 135 | if (!todo) return undefined; 136 | 137 | // Create a timestamp for the update 138 | const updatedAt = new Date().toISOString(); 139 | 140 | const db = databaseService.getDb(); 141 | const stmt = db.prepare(` 142 | UPDATE todos 143 | SET title = ?, description = ?, updatedAt = ? 144 | WHERE id = ? 145 | `); 146 | 147 | // Update with new values or keep existing ones if not provided 148 | stmt.run( 149 | data.title || todo.title, 150 | data.description || todo.description, 151 | updatedAt, 152 | todo.id 153 | ); 154 | 155 | // Return the updated todo 156 | return this.getTodo(todo.id); 157 | } 158 | 159 | /** 160 | * Mark a todo as completed 161 | * 162 | * This method: 163 | * 1. Checks if the todo exists 164 | * 2. Sets the completedAt timestamp to the current time 165 | * 3. Returns the updated todo 166 | * 167 | * @param id The UUID of the todo to complete 168 | * @returns The updated Todo if found, undefined otherwise 169 | */ 170 | completeTodo(id: string): Todo | undefined { 171 | // First check if the todo exists 172 | const todo = this.getTodo(id); 173 | if (!todo) return undefined; 174 | 175 | // Create a timestamp for the completion and update 176 | const now = new Date().toISOString(); 177 | 178 | const db = databaseService.getDb(); 179 | const stmt = db.prepare(` 180 | UPDATE todos 181 | SET completedAt = ?, updatedAt = ? 182 | WHERE id = ? 183 | `); 184 | 185 | // Set the completedAt timestamp 186 | stmt.run(now, now, id); 187 | 188 | // Return the updated todo 189 | return this.getTodo(id); 190 | } 191 | 192 | /** 193 | * Delete a todo 194 | * 195 | * This method removes a todo from the database permanently. 196 | * 197 | * @param id The UUID of the todo to delete 198 | * @returns true if deleted, false if not found or not deleted 199 | */ 200 | deleteTodo(id: string): boolean { 201 | const db = databaseService.getDb(); 202 | const stmt = db.prepare('DELETE FROM todos WHERE id = ?'); 203 | const result = stmt.run(id); 204 | 205 | // Check if any rows were affected 206 | return result.changes > 0; 207 | } 208 | 209 | /** 210 | * Search todos by title 211 | * 212 | * This method performs a case-insensitive partial match search 213 | * on todo titles. 214 | * 215 | * @param title The search term to look for in titles 216 | * @returns Array of matching Todos 217 | */ 218 | searchByTitle(title: string): Todo[] { 219 | // Add wildcards to the search term for partial matching 220 | const searchTerm = `%${title}%`; 221 | 222 | const db = databaseService.getDb(); 223 | 224 | // COLLATE NOCASE makes the search case-insensitive 225 | const stmt = db.prepare('SELECT * FROM todos WHERE title LIKE ? COLLATE NOCASE'); 226 | const rows = stmt.all(searchTerm) as any[]; 227 | 228 | return rows.map(row => this.rowToTodo(row)); 229 | } 230 | 231 | /** 232 | * Search todos by date 233 | * 234 | * This method finds todos created on a specific date. 235 | * It matches the start of the ISO string with the given date. 236 | * 237 | * @param dateStr The date to search for in YYYY-MM-DD format 238 | * @returns Array of matching Todos 239 | */ 240 | searchByDate(dateStr: string): Todo[] { 241 | // Add wildcard to match the time portion of ISO string 242 | const datePattern = `${dateStr}%`; 243 | 244 | const db = databaseService.getDb(); 245 | const stmt = db.prepare('SELECT * FROM todos WHERE createdAt LIKE ?'); 246 | const rows = stmt.all(datePattern) as any[]; 247 | 248 | return rows.map(row => this.rowToTodo(row)); 249 | } 250 | 251 | /** 252 | * Generate a summary of active todos 253 | * 254 | * This method creates a markdown-formatted summary of all active todos. 255 | * 256 | * WHY RETURN FORMATTED STRING? 257 | * - Provides ready-to-display content for the MCP client 258 | * - Encapsulates formatting logic in the service 259 | * - Makes it easy for LLMs to present a readable summary 260 | * 261 | * @returns Markdown-formatted summary string 262 | */ 263 | summarizeActiveTodos(): string { 264 | const activeTodos = this.getActiveTodos(); 265 | 266 | // Handle the case when there are no active todos 267 | if (activeTodos.length === 0) { 268 | return "No active todos found."; 269 | } 270 | 271 | // Create a bulleted list of todo titles 272 | const summary = activeTodos.map(todo => `- ${todo.title}`).join('\n'); 273 | return `# Active Todos Summary\n\nThere are ${activeTodos.length} active todos:\n\n${summary}`; 274 | } 275 | 276 | /** 277 | * Helper to convert a database row to a Todo object 278 | * 279 | * This private method handles the conversion between the database 280 | * representation and the application model. 281 | * 282 | * WHY SEPARATE THIS LOGIC? 283 | * - Avoids repeating the conversion code in multiple methods 284 | * - Creates a single place to update if the model changes 285 | * - Isolates database-specific knowledge from the rest of the code 286 | * 287 | * @param row The database row data 288 | * @returns A properly formatted Todo object 289 | */ 290 | private rowToTodo(row: any): Todo { 291 | return { 292 | id: row.id, 293 | title: row.title, 294 | description: row.description, 295 | completedAt: row.completedAt, 296 | completed: row.completedAt !== null, // Computed from completedAt 297 | createdAt: row.createdAt, 298 | updatedAt: row.updatedAt 299 | }; 300 | } 301 | } 302 | 303 | // Create a singleton instance for use throughout the application 304 | export const todoService = new TodoService(); ``` -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * index.ts 3 | * 4 | * This is the main entry point for the Todo MCP server. 5 | * It defines all the tools provided by the server and handles 6 | * connecting to clients. 7 | * 8 | * WHAT IS MCP? 9 | * The Model Context Protocol (MCP) allows AI models like Claude 10 | * to interact with external tools and services. This server implements 11 | * the MCP specification to provide a Todo list functionality that 12 | * Claude can use. 13 | * 14 | * HOW THE SERVER WORKS: 15 | * 1. It creates an MCP server instance with identity information 16 | * 2. It defines a set of tools for managing todos 17 | * 3. It connects to a transport (stdio in this case) 18 | * 4. It handles incoming tool calls from clients (like Claude) 19 | */ 20 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; 21 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; 22 | import { z } from "zod"; 23 | 24 | // Import models and schemas 25 | import { 26 | CreateTodoSchema, 27 | UpdateTodoSchema, 28 | CompleteTodoSchema, 29 | DeleteTodoSchema, 30 | SearchTodosByTitleSchema, 31 | SearchTodosByDateSchema 32 | } from "./models/Todo.js"; 33 | 34 | // Import services 35 | import { todoService } from "./services/TodoService.js"; 36 | import { databaseService } from "./services/DatabaseService.js"; 37 | 38 | // Import utilities 39 | import { createSuccessResponse, createErrorResponse, formatTodo, formatTodoList } from "./utils/formatters.js"; 40 | import { config } from "./config.js"; 41 | 42 | /** 43 | * Create the MCP server 44 | * 45 | * We initialize with identity information that helps clients 46 | * understand what they're connecting to. 47 | */ 48 | const server = new McpServer({ 49 | name: "Todo-MCP-Server", 50 | version: "1.0.0", 51 | }); 52 | 53 | /** 54 | * Helper function to safely execute operations 55 | * 56 | * This function: 57 | * 1. Attempts to execute an operation 58 | * 2. Catches any errors 59 | * 3. Returns either the result or an Error object 60 | * 61 | * WHY USE THIS PATTERN? 62 | * - Centralizes error handling 63 | * - Prevents crashes from uncaught exceptions 64 | * - Makes error reporting consistent across all tools 65 | * - Simplifies the tool implementations 66 | * 67 | * @param operation The function to execute 68 | * @param errorMessage The message to include if an error occurs 69 | * @returns Either the operation result or an Error 70 | */ 71 | async function safeExecute<T>(operation: () => T, errorMessage: string) { 72 | try { 73 | const result = operation(); 74 | return result; 75 | } catch (error) { 76 | console.error(errorMessage, error); 77 | if (error instanceof Error) { 78 | return new Error(`${errorMessage}: ${error.message}`); 79 | } 80 | return new Error(errorMessage); 81 | } 82 | } 83 | 84 | /** 85 | * Tool 1: Create a new todo 86 | * 87 | * This tool: 88 | * 1. Validates the input (title and description) 89 | * 2. Creates a new todo using the service 90 | * 3. Returns the formatted todo 91 | * 92 | * PATTERN FOR ALL TOOLS: 93 | * - Register with server.tool() 94 | * - Define name, description, and parameter schema 95 | * - Implement the async handler function 96 | * - Use safeExecute for error handling 97 | * - Return properly formatted response 98 | */ 99 | server.tool( 100 | "create-todo", 101 | "Create a new todo item", 102 | { 103 | title: z.string().min(1, "Title is required"), 104 | description: z.string().min(1, "Description is required"), 105 | }, 106 | async ({ title, description }) => { 107 | const result = await safeExecute(() => { 108 | const validatedData = CreateTodoSchema.parse({ title, description }); 109 | const newTodo = todoService.createTodo(validatedData); 110 | return formatTodo(newTodo); 111 | }, "Failed to create todo"); 112 | 113 | if (result instanceof Error) { 114 | return createErrorResponse(result.message); 115 | } 116 | 117 | return createSuccessResponse(`✅ Todo Created:\n\n${result}`); 118 | } 119 | ); 120 | 121 | /** 122 | * Tool 2: List all todos 123 | * 124 | * This tool: 125 | * 1. Retrieves all todos from the service 126 | * 2. Formats them as a list 127 | * 3. Returns the formatted list 128 | */ 129 | server.tool( 130 | "list-todos", 131 | "List all todos", 132 | {}, 133 | async () => { 134 | const result = await safeExecute(() => { 135 | const todos = todoService.getAllTodos(); 136 | return formatTodoList(todos); 137 | }, "Failed to list todos"); 138 | 139 | if (result instanceof Error) { 140 | return createErrorResponse(result.message); 141 | } 142 | 143 | return createSuccessResponse(result); 144 | } 145 | ); 146 | 147 | /** 148 | * Tool 3: Get a specific todo by ID 149 | * 150 | * This tool: 151 | * 1. Validates the input ID 152 | * 2. Retrieves the specific todo 153 | * 3. Returns the formatted todo 154 | */ 155 | server.tool( 156 | "get-todo", 157 | "Get a specific todo by ID", 158 | { 159 | id: z.string().uuid("Invalid Todo ID"), 160 | }, 161 | async ({ id }) => { 162 | const result = await safeExecute(() => { 163 | const todo = todoService.getTodo(id); 164 | if (!todo) { 165 | throw new Error(`Todo with ID ${id} not found`); 166 | } 167 | return formatTodo(todo); 168 | }, "Failed to get todo"); 169 | 170 | if (result instanceof Error) { 171 | return createErrorResponse(result.message); 172 | } 173 | 174 | return createSuccessResponse(result); 175 | } 176 | ); 177 | 178 | /** 179 | * Tool 4: Update a todo 180 | * 181 | * This tool: 182 | * 1. Validates the input (id required, title/description optional) 183 | * 2. Ensures at least one field is being updated 184 | * 3. Updates the todo using the service 185 | * 4. Returns the formatted updated todo 186 | */ 187 | server.tool( 188 | "update-todo", 189 | "Update a todo title or description", 190 | { 191 | id: z.string().uuid("Invalid Todo ID"), 192 | title: z.string().min(1, "Title is required").optional(), 193 | description: z.string().min(1, "Description is required").optional(), 194 | }, 195 | async ({ id, title, description }) => { 196 | const result = await safeExecute(() => { 197 | const validatedData = UpdateTodoSchema.parse({ id, title, description }); 198 | 199 | // Ensure at least one field is being updated 200 | if (!title && !description) { 201 | throw new Error("At least one field (title or description) must be provided"); 202 | } 203 | 204 | const updatedTodo = todoService.updateTodo(validatedData); 205 | if (!updatedTodo) { 206 | throw new Error(`Todo with ID ${id} not found`); 207 | } 208 | 209 | return formatTodo(updatedTodo); 210 | }, "Failed to update todo"); 211 | 212 | if (result instanceof Error) { 213 | return createErrorResponse(result.message); 214 | } 215 | 216 | return createSuccessResponse(`✅ Todo Updated:\n\n${result}`); 217 | } 218 | ); 219 | 220 | /** 221 | * Tool 5: Complete a todo 222 | * 223 | * This tool: 224 | * 1. Validates the todo ID 225 | * 2. Marks the todo as completed using the service 226 | * 3. Returns the formatted completed todo 227 | * 228 | * WHY SEPARATE FROM UPDATE? 229 | * - Provides a dedicated semantic action for completion 230 | * - Simplifies the client interaction model 231 | * - It's easier for the LLM to match the user intent with the completion action 232 | * - Makes it clear in the UI that the todo is done 233 | */ 234 | server.tool( 235 | "complete-todo", 236 | "Mark a todo as completed", 237 | { 238 | id: z.string().uuid("Invalid Todo ID"), 239 | }, 240 | async ({ id }) => { 241 | const result = await safeExecute(() => { 242 | const validatedData = CompleteTodoSchema.parse({ id }); 243 | const completedTodo = todoService.completeTodo(validatedData.id); 244 | 245 | if (!completedTodo) { 246 | throw new Error(`Todo with ID ${id} not found`); 247 | } 248 | 249 | return formatTodo(completedTodo); 250 | }, "Failed to complete todo"); 251 | 252 | if (result instanceof Error) { 253 | return createErrorResponse(result.message); 254 | } 255 | 256 | return createSuccessResponse(`✅ Todo Completed:\n\n${result}`); 257 | } 258 | ); 259 | 260 | /** 261 | * Tool 6: Delete a todo 262 | * 263 | * This tool: 264 | * 1. Validates the todo ID 265 | * 2. Retrieves the todo to be deleted (for the response) 266 | * 3. Deletes the todo using the service 267 | * 4. Returns a success message with the deleted todo's title 268 | */ 269 | server.tool( 270 | "delete-todo", 271 | "Delete a todo", 272 | { 273 | id: z.string().uuid("Invalid Todo ID"), 274 | }, 275 | async ({ id }) => { 276 | const result = await safeExecute(() => { 277 | const validatedData = DeleteTodoSchema.parse({ id }); 278 | const todo = todoService.getTodo(validatedData.id); 279 | 280 | if (!todo) { 281 | throw new Error(`Todo with ID ${id} not found`); 282 | } 283 | 284 | const success = todoService.deleteTodo(validatedData.id); 285 | 286 | if (!success) { 287 | throw new Error(`Failed to delete todo with ID ${id}`); 288 | } 289 | 290 | return todo.title; 291 | }, "Failed to delete todo"); 292 | 293 | if (result instanceof Error) { 294 | return createErrorResponse(result.message); 295 | } 296 | 297 | return createSuccessResponse(`✅ Todo Deleted: "${result}"`); 298 | } 299 | ); 300 | 301 | /** 302 | * Tool 7: Search todos by title 303 | * 304 | * This tool: 305 | * 1. Validates the search term 306 | * 2. Searches todos by title using the service 307 | * 3. Returns a formatted list of matching todos 308 | * 309 | * WHY HAVE SEARCH? 310 | * - Makes it easy to find specific todos when the list grows large 311 | * - Allows partial matching without requiring exact title 312 | * - Case-insensitive for better user experience 313 | */ 314 | server.tool( 315 | "search-todos-by-title", 316 | "Search todos by title (case insensitive partial match)", 317 | { 318 | title: z.string().min(1, "Search term is required"), 319 | }, 320 | async ({ title }) => { 321 | const result = await safeExecute(() => { 322 | const validatedData = SearchTodosByTitleSchema.parse({ title }); 323 | const todos = todoService.searchByTitle(validatedData.title); 324 | return formatTodoList(todos); 325 | }, "Failed to search todos"); 326 | 327 | if (result instanceof Error) { 328 | return createErrorResponse(result.message); 329 | } 330 | 331 | return createSuccessResponse(result); 332 | } 333 | ); 334 | 335 | /** 336 | * Tool 8: Search todos by date 337 | * 338 | * This tool: 339 | * 1. Validates the date format (YYYY-MM-DD) 340 | * 2. Searches todos created on that date 341 | * 3. Returns a formatted list of matching todos 342 | * 343 | * WHY DATE SEARCH? 344 | * - Allows finding todos created on a specific day 345 | * - Useful for reviewing what was added on a particular date 346 | * - Complements title search for different search needs 347 | */ 348 | server.tool( 349 | "search-todos-by-date", 350 | "Search todos by creation date (format: YYYY-MM-DD)", 351 | { 352 | date: z.string().regex(/^\d{4}-\d{2}-\d{2}$/, "Date must be in YYYY-MM-DD format"), 353 | }, 354 | async ({ date }) => { 355 | const result = await safeExecute(() => { 356 | const validatedData = SearchTodosByDateSchema.parse({ date }); 357 | const todos = todoService.searchByDate(validatedData.date); 358 | return formatTodoList(todos); 359 | }, "Failed to search todos by date"); 360 | 361 | if (result instanceof Error) { 362 | return createErrorResponse(result.message); 363 | } 364 | 365 | return createSuccessResponse(result); 366 | } 367 | ); 368 | 369 | /** 370 | * Tool 9: List active todos 371 | * 372 | * This tool: 373 | * 1. Retrieves all non-completed todos 374 | * 2. Returns a formatted list of active todos 375 | * 376 | * WHY SEPARATE FROM LIST ALL? 377 | * - Active todos are typically what users most often want to see 378 | * - Reduces noise by filtering out completed items 379 | * - Provides a clearer view of outstanding work 380 | */ 381 | server.tool( 382 | "list-active-todos", 383 | "List all non-completed todos", 384 | {}, 385 | async () => { 386 | const result = await safeExecute(() => { 387 | const todos = todoService.getActiveTodos(); 388 | return formatTodoList(todos); 389 | }, "Failed to list active todos"); 390 | 391 | if (result instanceof Error) { 392 | return createErrorResponse(result.message); 393 | } 394 | 395 | return createSuccessResponse(result); 396 | } 397 | ); 398 | 399 | /** 400 | * Tool 10: Summarize active todos 401 | * 402 | * This tool: 403 | * 1. Generates a summary of all active todos 404 | * 2. Returns a formatted markdown summary 405 | * 406 | * WHY HAVE A SUMMARY? 407 | * - Provides a quick overview without details 408 | * - Perfect for a quick status check 409 | * - Easier to read than a full list when there are many todos 410 | * - Particularly useful for LLM interfaces where conciseness matters 411 | */ 412 | server.tool( 413 | "summarize-active-todos", 414 | "Generate a summary of all active (non-completed) todos", 415 | {}, 416 | async () => { 417 | const result = await safeExecute(() => { 418 | return todoService.summarizeActiveTodos(); 419 | }, "Failed to summarize active todos"); 420 | 421 | if (result instanceof Error) { 422 | return createErrorResponse(result.message); 423 | } 424 | 425 | return createSuccessResponse(result); 426 | } 427 | ); 428 | 429 | /** 430 | * Main function to start the server 431 | * 432 | * This function: 433 | * 1. Initializes the server 434 | * 2. Sets up graceful shutdown handlers 435 | * 3. Connects to the transport 436 | * 437 | * WHY USE STDIO TRANSPORT? 438 | * - Works well with the MCP protocol 439 | * - Simple to integrate with LLM platforms like Claude Desktop 440 | * - No network configuration required 441 | * - Easy to debug and test 442 | */ 443 | async function main() { 444 | console.error("Starting Todo MCP Server..."); 445 | console.error(`SQLite database path: ${config.db.path}`); 446 | 447 | try { 448 | // Database is automatically initialized when the service is imported 449 | 450 | /** 451 | * Set up graceful shutdown to close the database 452 | * 453 | * This ensures data is properly saved when the server is stopped. 454 | * Both SIGINT (Ctrl+C) and SIGTERM (kill command) are handled. 455 | */ 456 | process.on('SIGINT', () => { 457 | console.error('Shutting down...'); 458 | databaseService.close(); 459 | process.exit(0); 460 | }); 461 | 462 | process.on('SIGTERM', () => { 463 | console.error('Shutting down...'); 464 | databaseService.close(); 465 | process.exit(0); 466 | }); 467 | 468 | /** 469 | * Connect to stdio transport 470 | * 471 | * The StdioServerTransport uses standard input/output for communication, 472 | * which is how Claude Desktop and other MCP clients connect to the server. 473 | */ 474 | const transport = new StdioServerTransport(); 475 | await server.connect(transport); 476 | 477 | console.error("Todo MCP Server running on stdio transport"); 478 | } catch (error) { 479 | console.error("Failed to start Todo MCP Server:", error); 480 | databaseService.close(); 481 | process.exit(1); 482 | } 483 | } 484 | 485 | // Start the server 486 | main(); ```