# 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: -------------------------------------------------------------------------------- ``` node_modules/ dist/ contexts/ .specstory/ ``` -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- ```markdown # Todo List MCP Server A Model Context Protocol (MCP) server that provides a comprehensive API for managing todo items. <a href="https://glama.ai/mcp/servers/kh39rjpplx"> <img width="380" height="200" src="https://glama.ai/mcp/servers/kh39rjpplx/badge" alt="Todo List Server MCP server" /> </a> > **📚 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. ## Features - **Create todos**: Add new tasks with title and markdown description - **Update todos**: Modify existing tasks - **Complete todos**: Mark tasks as done - **Delete todos**: Remove tasks from the list - **Search todos**: Find tasks by title or creation date - **Summarize todos**: Get a quick overview of active tasks ## Tools This MCP server exposes the following tools: 1. `create-todo`: Create a new todo item 2. `list-todos`: List all todos 3. `get-todo`: Get a specific todo by ID 4. `update-todo`: Update a todo's title or description 5. `complete-todo`: Mark a todo as completed 6. `delete-todo`: Delete a todo 7. `search-todos-by-title`: Search todos by title (case-insensitive partial match) 8. `search-todos-by-date`: Search todos by creation date (format: YYYY-MM-DD) 9. `list-active-todos`: List all non-completed todos 10. `summarize-active-todos`: Generate a summary of all active (non-completed) todos ## Installation ```bash # Clone the repository git clone https://github.com/RegiByte/todo-list-mcp.git cd todo-list-mcp # Install dependencies npm install # Build the project npm run build ``` ## Usage ### Starting the Server ```bash npm start ``` ### Configuring with Claude for Desktop #### Claude Desktop Add this to your `claude_desktop_config.json`: ```json { "mcpServers": { "todo": { "command": "node", "args": ["/absolute/path/to/todo-list-mcp/dist/index.js"] } } } ``` #### Cursor - Go to "Cursor Settings" -> MCP - Add a new MCP server with a "command" type - Add the absolute path of the server and run it with node - Example: node /absolute/path/to/todo-list-mcp/dist/index.js ### Example Commands When using with Claude for Desktop or Cursor, you can try: - "Create a todo to learn MCP with a description explaining why MCP is useful" - "List all my active todos" - "Create a todo for tomorrow's meeting with details about the agenda in markdown" - "Mark my learning MCP todo as completed" - "Summarize all my active todos" ## Project Structure This project follows a clear separation of concerns to make the code easy to understand: ``` src/ ├── models/ # Data structures and validation schemas ├── services/ # Business logic and database operations ├── utils/ # Helper functions and formatters ├── config.ts # Configuration settings ├── client.ts # Test client for local testing └── index.ts # Main entry point with MCP tool definitions ``` ## Learning from This Project This project is designed as an educational resource. To get the most out of it: 1. Read the [GUIDE.md](GUIDE.md) for a comprehensive explanation of the design 2. Study the heavily commented source code to understand implementation details 3. Use the test client to see how the server works in practice 4. Experiment with adding your own tools or extending the existing ones ## Development ### Building ```bash npm run build ``` ### Running in Development Mode ```bash npm run dev ``` ## License MIT ``` -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- ```json { "compilerOptions": { "target": "ES2022", "module": "Node16", "moduleResolution": "Node16", "outDir": "./dist", "rootDir": "./src", "strict": true, "esModuleInterop": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true }, "include": ["src/**/*"], "exclude": ["node_modules"] } ``` -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- ```json { "name": "todo-list-mcp", "version": "1.0.0", "main": "dist/index.js", "type": "module", "scripts": { "build": "tsc", "start": "node dist/index.js", "dev": "npm run build && npm start", "test": "npm run build && node dist/client.js", "inspector": "npx @modelcontextprotocol/inspector node dist/index.js" }, "keywords": [ "mcp", "todo", "api" ], "author": "", "license": "ISC", "description": "Todo list MCP server", "dependencies": { "@modelcontextprotocol/sdk": "^1.6.1", "better-sqlite3": "^9.4.1", "typescript": "^5.3.3", "uuid": "^9.0.1", "zod": "^3.22.4" }, "devDependencies": { "@modelcontextprotocol/inspector": "^0.5.1", "@types/better-sqlite3": "^7.6.9", "@types/node": "^20.11.28", "@types/uuid": "^9.0.8" } } ``` -------------------------------------------------------------------------------- /src/config.ts: -------------------------------------------------------------------------------- ```typescript /** * config.ts * * This file manages the application configuration settings. * It provides a centralized place for all configuration values, * making them easier to change and maintain. * * WHY A SEPARATE CONFIG FILE? * - Single source of truth for configuration values * - Easy to update settings without searching through the codebase * - Allows for environment-specific overrides * - Makes configuration values available throughout the application */ import path from 'path'; import os from 'os'; import fs from 'fs'; /** * Database configuration defaults * * We use the user's home directory for database storage by default, * which provides several advantages: * - Works across different operating systems * - Available without special permissions * - Persists across application restarts * - Doesn't get deleted when updating the application */ const DEFAULT_DB_FOLDER = path.join(os.homedir(), '.todo-list-mcp'); const DEFAULT_DB_FILE = 'todos.sqlite'; /** * Application configuration object * * This object provides access to all configuration settings. * It uses environment variables when available, falling back to defaults. * * WHY USE ENVIRONMENT VARIABLES? * - Allows configuration without changing code * - Follows the 12-factor app methodology for configuration * - Enables different settings per environment (dev, test, prod) * - Keeps sensitive information out of the code */ export const config = { db: { // Allow overriding through environment variables folder: process.env.TODO_DB_FOLDER || DEFAULT_DB_FOLDER, filename: process.env.TODO_DB_FILE || DEFAULT_DB_FILE, /** * Full path to the database file * * This getter computes the complete path dynamically, * ensuring consistency even if the folder or filename change. */ get path() { return path.join(this.folder, this.filename); } } }; /** * Ensure the database folder exists * * This utility function makes sure the folder for the database file exists, * creating it if necessary. This prevents errors when trying to open the * database file in a non-existent directory. */ export function ensureDbFolder() { if (!fs.existsSync(config.db.folder)) { fs.mkdirSync(config.db.folder, { recursive: true }); } } ``` -------------------------------------------------------------------------------- /src/services/DatabaseService.ts: -------------------------------------------------------------------------------- ```typescript /** * DatabaseService.ts * * This file implements a lightweight SQLite database service for the Todo application. * * WHY SQLITE? * - SQLite is perfect for small to medium applications like this one * - Requires no separate database server (file-based) * - ACID compliant and reliable * - Minimal configuration required * - Easy to install with minimal dependencies */ import Database from 'better-sqlite3'; import { config, ensureDbFolder } from '../config.js'; /** * DatabaseService Class * * This service manages the SQLite database connection and schema. * It follows the singleton pattern to ensure only one database connection exists. * * WHY SINGLETON PATTERN? * - Prevents multiple database connections which could lead to conflicts * - Provides a central access point to the database throughout the application * - Makes it easier to manage connection lifecycle (open/close) */ class DatabaseService { private db: Database.Database; constructor() { // Ensure the database folder exists before trying to create the database ensureDbFolder(); // Initialize the database with the configured path this.db = new Database(config.db.path); /** * Set pragmas for performance and safety: * - WAL (Write-Ahead Logging): Improves concurrent access performance * - foreign_keys: Ensures referential integrity (useful for future expansion) */ this.db.pragma('journal_mode = WAL'); this.db.pragma('foreign_keys = ON'); // Initialize the database schema when service is created this.initSchema(); } /** * Initialize the database schema * * This creates the todos table if it doesn't already exist. * The schema design incorporates: * - TEXT primary key for UUID compatibility * - NULL completedAt to represent incomplete todos * - Timestamp fields for tracking creation and updates */ private initSchema(): void { // Create todos table if it doesn't exist this.db.exec(` CREATE TABLE IF NOT EXISTS todos ( id TEXT PRIMARY KEY, title TEXT NOT NULL, description TEXT NOT NULL, completedAt TEXT NULL, -- ISO timestamp, NULL if not completed createdAt TEXT NOT NULL, updatedAt TEXT NOT NULL ) `); } /** * Get the database instance * * This allows other services to access the database for operations. * * @returns The SQLite database instance */ getDb(): Database.Database { return this.db; } /** * Close the database connection * * This should be called when shutting down the application to ensure * all data is properly saved and resources are released. */ close(): void { this.db.close(); } } // Create a singleton instance that will be used throughout the application export const databaseService = new DatabaseService(); ``` -------------------------------------------------------------------------------- /src/utils/formatters.ts: -------------------------------------------------------------------------------- ```typescript /** * formatters.ts * * This file contains utility functions for formatting data in the application. * These utilities handle the transformation of internal data structures into * human-readable formats appropriate for display to LLMs and users. * * WHY SEPARATE FORMATTERS? * - Keeps formatting logic separate from business logic * - Allows consistent formatting across the application * - Makes it easier to change display formats without affecting core functionality * - Centralizes presentation concerns in one place */ import { Todo } from "../models/Todo.js"; /** * Format a todo item to a readable string representation * * This formatter converts a Todo object into a markdown-formatted string * with clear visual indicators for completion status (emojis). * * WHY USE MARKDOWN? * - Provides structured, readable output * - Works well with LLMs which understand markdown syntax * - Allows rich formatting like headers, lists, and emphasis * - Can be displayed directly in many UI contexts * * @param todo The Todo object to format * @returns A markdown-formatted string representation */ export function formatTodo(todo: Todo): string { return ` ## ${todo.title} ${todo.completed ? '✅' : '⏳'} ID: ${todo.id} Created: ${new Date(todo.createdAt).toLocaleString()} Updated: ${new Date(todo.updatedAt).toLocaleString()} ${todo.description} `.trim(); } /** * Format a list of todos to a readable string representation * * This formatter takes an array of Todo objects and creates a complete * markdown document with a title and formatted entries. * * @param todos Array of Todo objects to format * @returns A markdown-formatted string with the complete list */ export function formatTodoList(todos: Todo[]): string { if (todos.length === 0) { return "No todos found."; } const todoItems = todos.map(formatTodo).join('\n\n---\n\n'); return `# Todo List (${todos.length} items)\n\n${todoItems}`; } /** * Create success response for MCP tool calls * * This utility formats successful responses according to the MCP protocol. * It wraps the message in the expected content structure. * * WHY THIS FORMAT? * - Follows the MCP protocol's expected response structure * - Allows the message to be properly displayed by MCP clients * - Clearly indicates success status * * @param message The success message to include * @returns A properly formatted MCP response object */ export function createSuccessResponse(message: string) { return { content: [ { type: "text" as const, text: message, }, ], }; } /** * Create error response for MCP tool calls * * This utility formats error responses according to the MCP protocol. * It includes the isError flag to indicate failure. * * @param message The error message to include * @returns A properly formatted MCP error response object */ export function createErrorResponse(message: string) { return { content: [ { type: "text" as const, text: message, }, ], isError: true, }; } ``` -------------------------------------------------------------------------------- /src/models/Todo.ts: -------------------------------------------------------------------------------- ```typescript /** * Todo.ts * * This file defines the core data model for our Todo application, along with validation * schemas and a factory function for creating new Todo instances. * * WHY USE ZOD? * - Zod provides runtime type validation, ensuring our data meets specific requirements * - Using schemas creates a clear contract for each operation's input requirements * - Error messages are automatically generated with clear validation feedback * - TypeScript integration gives us both compile-time and runtime type safety * - Schemas can be converted to JSON Schema, which is useful for MCP clients */ import { z } from 'zod'; import { v4 as uuidv4 } from 'uuid'; /** * Todo Interface * * This defines the structure of a Todo item in our application. * We've designed it with several important considerations: * - IDs use UUID for uniqueness across systems * - Timestamps track creation and updates for data lifecycle management * - Description supports markdown for rich text formatting * - Completion status is tracked both as a boolean flag and with a timestamp */ export interface Todo { id: string; title: string; description: string; // Markdown format completed: boolean; // Computed from completedAt for backward compatibility completedAt: string | null; // ISO timestamp when completed, null if not completed createdAt: string; updatedAt: string; } /** * Input Validation Schemas * * These schemas define the requirements for different operations. * Each schema serves as both documentation and runtime validation. * * WHY SEPARATE SCHEMAS? * - Different operations have different validation requirements * - Keeps validation focused on only what's needed for each operation * - Makes the API more intuitive by clearly defining what each operation expects */ // Schema for creating a new todo - requires title and description export const CreateTodoSchema = z.object({ title: z.string().min(1, "Title is required"), description: z.string().min(1, "Description is required"), }); // Schema for updating a todo - requires ID, title and description are optional export const UpdateTodoSchema = z.object({ id: z.string().uuid("Invalid Todo ID"), title: z.string().min(1, "Title is required").optional(), description: z.string().min(1, "Description is required").optional(), }); // Schema for completing a todo - requires only ID export const CompleteTodoSchema = z.object({ id: z.string().uuid("Invalid Todo ID"), }); // Schema for deleting a todo - requires only ID export const DeleteTodoSchema = z.object({ id: z.string().uuid("Invalid Todo ID"), }); // Schema for searching todos by title - requires search term export const SearchTodosByTitleSchema = z.object({ title: z.string().min(1, "Search term is required"), }); // Schema for searching todos by date - requires date in YYYY-MM-DD format export const SearchTodosByDateSchema = z.object({ date: z.string().regex(/^\d{4}-\d{2}-\d{2}$/, "Date must be in YYYY-MM-DD format"), }); /** * Factory Function: createTodo * * WHY USE A FACTORY FUNCTION? * - Centralizes the creation logic in one place * - Ensures all required fields are set with proper default values * - Guarantees all todos have the same structure * - Makes it easy to change the implementation without affecting code that creates todos * * @param data The validated input data * @returns A fully formed Todo object with generated ID and timestamps */ export function createTodo(data: z.infer<typeof CreateTodoSchema>): Todo { const now = new Date().toISOString(); return { id: uuidv4(), title: data.title, description: data.description, completed: false, completedAt: null, createdAt: now, updatedAt: now, }; } ``` -------------------------------------------------------------------------------- /src/client.ts: -------------------------------------------------------------------------------- ```typescript /** * client.ts * * This file implements a test client for the Todo MCP server. * It demonstrates how to connect to the server, call various tools, * and handle the responses. * * WHY HAVE A TEST CLIENT? * - Validates that the server works correctly * - Provides a working example of how to use the MCP client SDK * - Makes it easy to test changes without needing an LLM * - Serves as documentation for how to interact with the server */ import { Client } from "@modelcontextprotocol/sdk/client/index.js"; import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"; /** * Response content type definition * * The MCP protocol returns content as an array of typed objects. * This interface defines the structure of text content items. */ interface ContentText { type: "text"; text: string; } /** * Main function that runs the test client * * This function: * 1. Connects to the Todo MCP server * 2. Demonstrates all the available tools * 3. Creates, updates, completes, and deletes a test todo */ async function main() { console.log("Starting Todo MCP Test Client..."); try { /** * Create a client transport to the server * * The StdioClientTransport launches the server as a child process * and communicates with it via standard input/output. * * WHY STDIO TRANSPORT? * - Simple to set up and use * - Works well for local testing * - Doesn't require network configuration * - Similar to how Claude Desktop launches MCP servers */ const transport = new StdioClientTransport({ command: "node", args: ["dist/index.js"], }); /** * Create and connect the client * * We configure the client with basic identity information * and the capabilities it needs (tools in this case). */ const client = new Client( { name: "todo-test-client", version: "1.0.0", }, { capabilities: { tools: {} } } ); // Connect to the server through the transport await client.connect(transport); console.log("Connected to Todo MCP Server"); /** * List available tools * * This demonstrates how to query what tools the server provides, * which is useful for discovery and documentation. */ const toolsResult = await client.listTools(); console.log("\nAvailable tools:", toolsResult.tools.map(tool => tool.name)); /** * Create a test todo * * This demonstrates the create-todo tool, which takes a title * and markdown description as arguments. */ console.log("\nCreating a test todo..."); const createTodoResult = await client.callTool({ name: "create-todo", arguments: { title: "Learn about MCP", description: "# Model Context Protocol\n\n- Understand core concepts\n- Build a simple server\n- Test with Claude" } }); // Type assertion to access the content const createContent = createTodoResult.content as ContentText[]; console.log(createContent[0].text); /** * Extract the todo ID from the response * * We use a simple regex to parse the ID from the formatted response. * In a real application, you might want a more structured response format. */ const idMatch = createContent[0].text.match(/ID: ([0-9a-f-]+)/); const todoId = idMatch ? idMatch[1] : null; // Only proceed if we successfully created a todo and extracted its ID if (todoId) { /** * List all todos * * This demonstrates the list-todos tool, which takes no arguments * and returns a formatted list of all todos. */ console.log("\nListing all todos..."); const listTodosResult = await client.callTool({ name: "list-todos", arguments: {} }); const listContent = listTodosResult.content as ContentText[]; console.log(listContent[0].text); /** * Update the todo * * This demonstrates the update-todo tool, which takes an ID * and optional title/description fields to update. */ console.log("\nUpdating the test todo..."); const updateTodoResult = await client.callTool({ name: "update-todo", arguments: { id: todoId, 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" } }); const updateContent = updateTodoResult.content as ContentText[]; console.log(updateContent[0].text); /** * Mark todo as completed * * This demonstrates the complete-todo tool, which takes an ID * and marks the corresponding todo as completed. */ console.log("\nCompleting the test todo..."); const completeTodoResult = await client.callTool({ name: "complete-todo", arguments: { id: todoId } }); const completeContent = completeTodoResult.content as ContentText[]; console.log(completeContent[0].text); /** * Summarize active todos * * This demonstrates the summarize-active-todos tool, which * generates a summary of all non-completed todos. */ console.log("\nSummarizing active todos..."); const summaryResult = await client.callTool({ name: "summarize-active-todos", arguments: {} }); const summaryContent = summaryResult.content as ContentText[]; console.log(summaryContent[0].text); /** * Delete the todo * * This demonstrates the delete-todo tool, which permanently * removes a todo from the database. */ console.log("\nDeleting the test todo..."); const deleteTodoResult = await client.callTool({ name: "delete-todo", arguments: { id: todoId } }); const deleteContent = deleteTodoResult.content as ContentText[]; console.log(deleteContent[0].text); } // Close the client connection await client.close(); console.log("\nTest completed successfully!"); } catch (error) { console.error("Error in test client:", error); process.exit(1); } } // Start the test client main(); ``` -------------------------------------------------------------------------------- /GUIDE.md: -------------------------------------------------------------------------------- ```markdown # Todo List MCP Server: A Learning Guide ## Introduction to Model Context Protocol (MCP) 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. ### Why MCP Matters 1. **Extended Capabilities**: MCP allows AI models to perform actions beyond just generating text (database operations, file management, API calls, etc.) 2. **Standardization**: Creates a consistent interface for tools regardless of implementation 3. **Controlled Access**: Provides a secure way to expose specific functionality to AI models 4. **Real-time Integration**: Enables AI to access up-to-date information and perform real-world actions ## About This Project 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. ### Learning Objectives By studying this codebase, you can learn: 1. How to structure an MCP server project 2. How to implement CRUD operations via MCP tools 3. Best practices for error handling and validation 4. How to format responses for AI consumption 5. How the MCP protocol works in practice ## Codebase Structure and Design Philosophy The project follows several key design principles: ### 1. Clear Separation of Concerns The codebase is organized into distinct layers: - **Models** (`src/models/`): Data structures and validation schemas - **Services** (`src/services/`): Business logic and data access - **Utils** (`src/utils/`): Helper functions and formatters - **Entry Point** (`src/index.ts`): MCP server definition and tool implementations This separation makes the code easier to understand, maintain, and extend. ### 2. Type Safety and Validation The project uses TypeScript and Zod for comprehensive type safety: - **TypeScript Interfaces**: Define data structures with static typing - **Zod Schemas**: Provide runtime validation with descriptive error messages - **Consistent Validation**: Each operation validates its inputs before processing ### 3. Error Handling A consistent error handling approach is used throughout: - **Central Error Processing**: The `safeExecute` function standardizes error handling - **Descriptive Error Messages**: All errors provide clear context about what went wrong - **Proper Error Responses**: Errors are formatted according to MCP requirements ### 4. Data Persistence The project uses SQLite for simple but effective data storage: - **File-based Database**: Easy to set up with no external dependencies - **SQL Operations**: Demonstrates parameterized queries and basic CRUD operations - **Singleton Pattern**: Ensures a single database connection throughout the application ## Key Implementation Patterns ### The Tool Definition Pattern Every MCP tool follows the same pattern: ```typescript server.tool( "tool-name", // Name: How the tool is identified "Tool description", // Description: What the tool does { /* parameter schema */ }, // Schema: Expected inputs with validation async (params) => { // Handler: The implementation function // 1. Validate inputs // 2. Execute business logic // 3. Format and return response } ); ``` ### Error Handling Pattern The error handling pattern ensures consistent behavior: ```typescript const result = await safeExecute(() => { // Operation that might fail }, "Descriptive error message"); if (result instanceof Error) { return createErrorResponse(result.message); } return createSuccessResponse(formattedResult); ``` ### Response Formatting Pattern Responses are consistently formatted for easy consumption by LLMs: ```typescript // Success responses return createSuccessResponse(`✅ Success message with ${formattedData}`); // Error responses return createErrorResponse(`Error: ${errorMessage}`); ``` ## How to Learn from This Project ### For Beginners 1. Start by understanding the `Todo` model in `src/models/Todo.ts` 2. Look at how tools are defined in `src/index.ts` 3. Explore the basic CRUD operations in `src/services/TodoService.ts` 4. See how responses are formatted in `src/utils/formatters.ts` ### For Intermediate Developers 1. Study the error handling patterns throughout the codebase 2. Look at how validation is implemented with Zod schemas 3. Examine the database operations and SQL queries 4. Understand how the MCP tools are organized and structured ### For Advanced Developers 1. Consider how this approach could be extended for more complex applications 2. Think about how to add authentication, caching, or more advanced features 3. Look at the client implementation to understand the full MCP communication cycle 4. Consider how to implement testing for an MCP server ## Running and Testing ### Local Testing Use the provided test client to see the server in action: ```bash npm run build node dist/client.js ``` This will run through a complete lifecycle of creating, updating, completing, and deleting a todo. ### Integration with Claude for Desktop To use this server with Claude for Desktop, add it to your `claude_desktop_config.json`: ```json { "mcpServers": { "todo": { "command": "node", "args": ["/absolute/path/to/todo-list-mcp/dist/index.js"] } } } ``` ## Common Patterns and Best Practices Demonstrated 1. **Singleton Pattern**: Used for database and service access 2. **Repository Pattern**: Abstracts data access operations 3. **Factory Pattern**: Creates new Todo objects with consistent structure 4. **Validation Pattern**: Validates inputs before processing 5. **Error Handling Pattern**: Centralizes and standardizes error handling 6. **Formatting Pattern**: Consistently formats outputs for consumption 7. **Configuration Pattern**: Centralizes application settings ## Conclusion 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. 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 /** * TodoService.ts * * This service implements the core business logic for managing todos. * It acts as an intermediary between the data model and the database, * handling all CRUD operations and search functionality. * * WHY A SERVICE LAYER? * - Separates business logic from database operations * - Provides a clean API for the application to work with * - Makes it easier to change the database implementation later * - Encapsulates complex operations into simple method calls */ import { Todo, createTodo, CreateTodoSchema, UpdateTodoSchema } from '../models/Todo.js'; import { z } from 'zod'; import { databaseService } from './DatabaseService.js'; /** * TodoService Class * * This service follows the repository pattern to provide a clean * interface for working with todos. It encapsulates all database * operations and business logic in one place. */ class TodoService { /** * Create a new todo * * This method: * 1. Uses the factory function to create a new Todo object * 2. Persists it to the database * 3. Returns the created Todo * * @param data Validated input data (title and description) * @returns The newly created Todo */ createTodo(data: z.infer<typeof CreateTodoSchema>): Todo { // Use the factory function to create a Todo with proper defaults const todo = createTodo(data); // Get the database instance const db = databaseService.getDb(); // Prepare the SQL statement for inserting a new todo const stmt = db.prepare(` INSERT INTO todos (id, title, description, completedAt, createdAt, updatedAt) VALUES (?, ?, ?, ?, ?, ?) `); // Execute the statement with the todo's data stmt.run( todo.id, todo.title, todo.description, todo.completedAt, todo.createdAt, todo.updatedAt ); // Return the created todo return todo; } /** * Get a todo by ID * * This method: * 1. Queries the database for a todo with the given ID * 2. Converts the database row to a Todo object if found * * @param id The UUID of the todo to retrieve * @returns The Todo if found, undefined otherwise */ getTodo(id: string): Todo | undefined { const db = databaseService.getDb(); // Use parameterized query to prevent SQL injection const stmt = db.prepare('SELECT * FROM todos WHERE id = ?'); const row = stmt.get(id) as any; // Return undefined if no todo was found if (!row) return undefined; // Convert the database row to a Todo object return this.rowToTodo(row); } /** * Get all todos * * This method returns all todos in the database without filtering. * * @returns Array of all Todos */ getAllTodos(): Todo[] { const db = databaseService.getDb(); const stmt = db.prepare('SELECT * FROM todos'); const rows = stmt.all() as any[]; // Convert each database row to a Todo object return rows.map(row => this.rowToTodo(row)); } /** * Get all active (non-completed) todos * * This method returns only todos that haven't been marked as completed. * A todo is considered active when its completedAt field is NULL. * * @returns Array of active Todos */ getActiveTodos(): Todo[] { const db = databaseService.getDb(); const stmt = db.prepare('SELECT * FROM todos WHERE completedAt IS NULL'); const rows = stmt.all() as any[]; // Convert each database row to a Todo object return rows.map(row => this.rowToTodo(row)); } /** * Update a todo * * This method: * 1. Checks if the todo exists * 2. Updates the specified fields * 3. Returns the updated todo * * @param data The update data (id required, title/description optional) * @returns The updated Todo if found, undefined otherwise */ updateTodo(data: z.infer<typeof UpdateTodoSchema>): Todo | undefined { // First check if the todo exists const todo = this.getTodo(data.id); if (!todo) return undefined; // Create a timestamp for the update const updatedAt = new Date().toISOString(); const db = databaseService.getDb(); const stmt = db.prepare(` UPDATE todos SET title = ?, description = ?, updatedAt = ? WHERE id = ? `); // Update with new values or keep existing ones if not provided stmt.run( data.title || todo.title, data.description || todo.description, updatedAt, todo.id ); // Return the updated todo return this.getTodo(todo.id); } /** * Mark a todo as completed * * This method: * 1. Checks if the todo exists * 2. Sets the completedAt timestamp to the current time * 3. Returns the updated todo * * @param id The UUID of the todo to complete * @returns The updated Todo if found, undefined otherwise */ completeTodo(id: string): Todo | undefined { // First check if the todo exists const todo = this.getTodo(id); if (!todo) return undefined; // Create a timestamp for the completion and update const now = new Date().toISOString(); const db = databaseService.getDb(); const stmt = db.prepare(` UPDATE todos SET completedAt = ?, updatedAt = ? WHERE id = ? `); // Set the completedAt timestamp stmt.run(now, now, id); // Return the updated todo return this.getTodo(id); } /** * Delete a todo * * This method removes a todo from the database permanently. * * @param id The UUID of the todo to delete * @returns true if deleted, false if not found or not deleted */ deleteTodo(id: string): boolean { const db = databaseService.getDb(); const stmt = db.prepare('DELETE FROM todos WHERE id = ?'); const result = stmt.run(id); // Check if any rows were affected return result.changes > 0; } /** * Search todos by title * * This method performs a case-insensitive partial match search * on todo titles. * * @param title The search term to look for in titles * @returns Array of matching Todos */ searchByTitle(title: string): Todo[] { // Add wildcards to the search term for partial matching const searchTerm = `%${title}%`; const db = databaseService.getDb(); // COLLATE NOCASE makes the search case-insensitive const stmt = db.prepare('SELECT * FROM todos WHERE title LIKE ? COLLATE NOCASE'); const rows = stmt.all(searchTerm) as any[]; return rows.map(row => this.rowToTodo(row)); } /** * Search todos by date * * This method finds todos created on a specific date. * It matches the start of the ISO string with the given date. * * @param dateStr The date to search for in YYYY-MM-DD format * @returns Array of matching Todos */ searchByDate(dateStr: string): Todo[] { // Add wildcard to match the time portion of ISO string const datePattern = `${dateStr}%`; const db = databaseService.getDb(); const stmt = db.prepare('SELECT * FROM todos WHERE createdAt LIKE ?'); const rows = stmt.all(datePattern) as any[]; return rows.map(row => this.rowToTodo(row)); } /** * Generate a summary of active todos * * This method creates a markdown-formatted summary of all active todos. * * WHY RETURN FORMATTED STRING? * - Provides ready-to-display content for the MCP client * - Encapsulates formatting logic in the service * - Makes it easy for LLMs to present a readable summary * * @returns Markdown-formatted summary string */ summarizeActiveTodos(): string { const activeTodos = this.getActiveTodos(); // Handle the case when there are no active todos if (activeTodos.length === 0) { return "No active todos found."; } // Create a bulleted list of todo titles const summary = activeTodos.map(todo => `- ${todo.title}`).join('\n'); return `# Active Todos Summary\n\nThere are ${activeTodos.length} active todos:\n\n${summary}`; } /** * Helper to convert a database row to a Todo object * * This private method handles the conversion between the database * representation and the application model. * * WHY SEPARATE THIS LOGIC? * - Avoids repeating the conversion code in multiple methods * - Creates a single place to update if the model changes * - Isolates database-specific knowledge from the rest of the code * * @param row The database row data * @returns A properly formatted Todo object */ private rowToTodo(row: any): Todo { return { id: row.id, title: row.title, description: row.description, completedAt: row.completedAt, completed: row.completedAt !== null, // Computed from completedAt createdAt: row.createdAt, updatedAt: row.updatedAt }; } } // Create a singleton instance for use throughout the application export const todoService = new TodoService(); ``` -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- ```typescript /** * index.ts * * This is the main entry point for the Todo MCP server. * It defines all the tools provided by the server and handles * connecting to clients. * * WHAT IS MCP? * The Model Context Protocol (MCP) allows AI models like Claude * to interact with external tools and services. This server implements * the MCP specification to provide a Todo list functionality that * Claude can use. * * HOW THE SERVER WORKS: * 1. It creates an MCP server instance with identity information * 2. It defines a set of tools for managing todos * 3. It connects to a transport (stdio in this case) * 4. It handles incoming tool calls from clients (like Claude) */ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { z } from "zod"; // Import models and schemas import { CreateTodoSchema, UpdateTodoSchema, CompleteTodoSchema, DeleteTodoSchema, SearchTodosByTitleSchema, SearchTodosByDateSchema } from "./models/Todo.js"; // Import services import { todoService } from "./services/TodoService.js"; import { databaseService } from "./services/DatabaseService.js"; // Import utilities import { createSuccessResponse, createErrorResponse, formatTodo, formatTodoList } from "./utils/formatters.js"; import { config } from "./config.js"; /** * Create the MCP server * * We initialize with identity information that helps clients * understand what they're connecting to. */ const server = new McpServer({ name: "Todo-MCP-Server", version: "1.0.0", }); /** * Helper function to safely execute operations * * This function: * 1. Attempts to execute an operation * 2. Catches any errors * 3. Returns either the result or an Error object * * WHY USE THIS PATTERN? * - Centralizes error handling * - Prevents crashes from uncaught exceptions * - Makes error reporting consistent across all tools * - Simplifies the tool implementations * * @param operation The function to execute * @param errorMessage The message to include if an error occurs * @returns Either the operation result or an Error */ async function safeExecute<T>(operation: () => T, errorMessage: string) { try { const result = operation(); return result; } catch (error) { console.error(errorMessage, error); if (error instanceof Error) { return new Error(`${errorMessage}: ${error.message}`); } return new Error(errorMessage); } } /** * Tool 1: Create a new todo * * This tool: * 1. Validates the input (title and description) * 2. Creates a new todo using the service * 3. Returns the formatted todo * * PATTERN FOR ALL TOOLS: * - Register with server.tool() * - Define name, description, and parameter schema * - Implement the async handler function * - Use safeExecute for error handling * - Return properly formatted response */ server.tool( "create-todo", "Create a new todo item", { title: z.string().min(1, "Title is required"), description: z.string().min(1, "Description is required"), }, async ({ title, description }) => { const result = await safeExecute(() => { const validatedData = CreateTodoSchema.parse({ title, description }); const newTodo = todoService.createTodo(validatedData); return formatTodo(newTodo); }, "Failed to create todo"); if (result instanceof Error) { return createErrorResponse(result.message); } return createSuccessResponse(`✅ Todo Created:\n\n${result}`); } ); /** * Tool 2: List all todos * * This tool: * 1. Retrieves all todos from the service * 2. Formats them as a list * 3. Returns the formatted list */ server.tool( "list-todos", "List all todos", {}, async () => { const result = await safeExecute(() => { const todos = todoService.getAllTodos(); return formatTodoList(todos); }, "Failed to list todos"); if (result instanceof Error) { return createErrorResponse(result.message); } return createSuccessResponse(result); } ); /** * Tool 3: Get a specific todo by ID * * This tool: * 1. Validates the input ID * 2. Retrieves the specific todo * 3. Returns the formatted todo */ server.tool( "get-todo", "Get a specific todo by ID", { id: z.string().uuid("Invalid Todo ID"), }, async ({ id }) => { const result = await safeExecute(() => { const todo = todoService.getTodo(id); if (!todo) { throw new Error(`Todo with ID ${id} not found`); } return formatTodo(todo); }, "Failed to get todo"); if (result instanceof Error) { return createErrorResponse(result.message); } return createSuccessResponse(result); } ); /** * Tool 4: Update a todo * * This tool: * 1. Validates the input (id required, title/description optional) * 2. Ensures at least one field is being updated * 3. Updates the todo using the service * 4. Returns the formatted updated todo */ server.tool( "update-todo", "Update a todo title or description", { id: z.string().uuid("Invalid Todo ID"), title: z.string().min(1, "Title is required").optional(), description: z.string().min(1, "Description is required").optional(), }, async ({ id, title, description }) => { const result = await safeExecute(() => { const validatedData = UpdateTodoSchema.parse({ id, title, description }); // Ensure at least one field is being updated if (!title && !description) { throw new Error("At least one field (title or description) must be provided"); } const updatedTodo = todoService.updateTodo(validatedData); if (!updatedTodo) { throw new Error(`Todo with ID ${id} not found`); } return formatTodo(updatedTodo); }, "Failed to update todo"); if (result instanceof Error) { return createErrorResponse(result.message); } return createSuccessResponse(`✅ Todo Updated:\n\n${result}`); } ); /** * Tool 5: Complete a todo * * This tool: * 1. Validates the todo ID * 2. Marks the todo as completed using the service * 3. Returns the formatted completed todo * * WHY SEPARATE FROM UPDATE? * - Provides a dedicated semantic action for completion * - Simplifies the client interaction model * - It's easier for the LLM to match the user intent with the completion action * - Makes it clear in the UI that the todo is done */ server.tool( "complete-todo", "Mark a todo as completed", { id: z.string().uuid("Invalid Todo ID"), }, async ({ id }) => { const result = await safeExecute(() => { const validatedData = CompleteTodoSchema.parse({ id }); const completedTodo = todoService.completeTodo(validatedData.id); if (!completedTodo) { throw new Error(`Todo with ID ${id} not found`); } return formatTodo(completedTodo); }, "Failed to complete todo"); if (result instanceof Error) { return createErrorResponse(result.message); } return createSuccessResponse(`✅ Todo Completed:\n\n${result}`); } ); /** * Tool 6: Delete a todo * * This tool: * 1. Validates the todo ID * 2. Retrieves the todo to be deleted (for the response) * 3. Deletes the todo using the service * 4. Returns a success message with the deleted todo's title */ server.tool( "delete-todo", "Delete a todo", { id: z.string().uuid("Invalid Todo ID"), }, async ({ id }) => { const result = await safeExecute(() => { const validatedData = DeleteTodoSchema.parse({ id }); const todo = todoService.getTodo(validatedData.id); if (!todo) { throw new Error(`Todo with ID ${id} not found`); } const success = todoService.deleteTodo(validatedData.id); if (!success) { throw new Error(`Failed to delete todo with ID ${id}`); } return todo.title; }, "Failed to delete todo"); if (result instanceof Error) { return createErrorResponse(result.message); } return createSuccessResponse(`✅ Todo Deleted: "${result}"`); } ); /** * Tool 7: Search todos by title * * This tool: * 1. Validates the search term * 2. Searches todos by title using the service * 3. Returns a formatted list of matching todos * * WHY HAVE SEARCH? * - Makes it easy to find specific todos when the list grows large * - Allows partial matching without requiring exact title * - Case-insensitive for better user experience */ server.tool( "search-todos-by-title", "Search todos by title (case insensitive partial match)", { title: z.string().min(1, "Search term is required"), }, async ({ title }) => { const result = await safeExecute(() => { const validatedData = SearchTodosByTitleSchema.parse({ title }); const todos = todoService.searchByTitle(validatedData.title); return formatTodoList(todos); }, "Failed to search todos"); if (result instanceof Error) { return createErrorResponse(result.message); } return createSuccessResponse(result); } ); /** * Tool 8: Search todos by date * * This tool: * 1. Validates the date format (YYYY-MM-DD) * 2. Searches todos created on that date * 3. Returns a formatted list of matching todos * * WHY DATE SEARCH? * - Allows finding todos created on a specific day * - Useful for reviewing what was added on a particular date * - Complements title search for different search needs */ server.tool( "search-todos-by-date", "Search todos by creation date (format: YYYY-MM-DD)", { date: z.string().regex(/^\d{4}-\d{2}-\d{2}$/, "Date must be in YYYY-MM-DD format"), }, async ({ date }) => { const result = await safeExecute(() => { const validatedData = SearchTodosByDateSchema.parse({ date }); const todos = todoService.searchByDate(validatedData.date); return formatTodoList(todos); }, "Failed to search todos by date"); if (result instanceof Error) { return createErrorResponse(result.message); } return createSuccessResponse(result); } ); /** * Tool 9: List active todos * * This tool: * 1. Retrieves all non-completed todos * 2. Returns a formatted list of active todos * * WHY SEPARATE FROM LIST ALL? * - Active todos are typically what users most often want to see * - Reduces noise by filtering out completed items * - Provides a clearer view of outstanding work */ server.tool( "list-active-todos", "List all non-completed todos", {}, async () => { const result = await safeExecute(() => { const todos = todoService.getActiveTodos(); return formatTodoList(todos); }, "Failed to list active todos"); if (result instanceof Error) { return createErrorResponse(result.message); } return createSuccessResponse(result); } ); /** * Tool 10: Summarize active todos * * This tool: * 1. Generates a summary of all active todos * 2. Returns a formatted markdown summary * * WHY HAVE A SUMMARY? * - Provides a quick overview without details * - Perfect for a quick status check * - Easier to read than a full list when there are many todos * - Particularly useful for LLM interfaces where conciseness matters */ server.tool( "summarize-active-todos", "Generate a summary of all active (non-completed) todos", {}, async () => { const result = await safeExecute(() => { return todoService.summarizeActiveTodos(); }, "Failed to summarize active todos"); if (result instanceof Error) { return createErrorResponse(result.message); } return createSuccessResponse(result); } ); /** * Main function to start the server * * This function: * 1. Initializes the server * 2. Sets up graceful shutdown handlers * 3. Connects to the transport * * WHY USE STDIO TRANSPORT? * - Works well with the MCP protocol * - Simple to integrate with LLM platforms like Claude Desktop * - No network configuration required * - Easy to debug and test */ async function main() { console.error("Starting Todo MCP Server..."); console.error(`SQLite database path: ${config.db.path}`); try { // Database is automatically initialized when the service is imported /** * Set up graceful shutdown to close the database * * This ensures data is properly saved when the server is stopped. * Both SIGINT (Ctrl+C) and SIGTERM (kill command) are handled. */ process.on('SIGINT', () => { console.error('Shutting down...'); databaseService.close(); process.exit(0); }); process.on('SIGTERM', () => { console.error('Shutting down...'); databaseService.close(); process.exit(0); }); /** * Connect to stdio transport * * The StdioServerTransport uses standard input/output for communication, * which is how Claude Desktop and other MCP clients connect to the server. */ const transport = new StdioServerTransport(); await server.connect(transport); console.error("Todo MCP Server running on stdio transport"); } catch (error) { console.error("Failed to start Todo MCP Server:", error); databaseService.close(); process.exit(1); } } // Start the server main(); ```