UNPKG

mini-todo-list-mcp

Version:

A streamlined Model Context Protocol (MCP) server for todo management with essential CRUD operations, bulk functionality, and workflow support

491 lines (420 loc) 13.3 kB
#!/usr/bin/env node /** * index.ts - Mini Todo MCP Server * * A streamlined version of the Todo MCP server with only essential tools: * - CRUD operations (create, get, update, complete, delete) * - Workflow (get-next-todo) * - Bulk operations (bulk-add-todos, clear-all-todos) * * This mini version reduces token usage while maintaining core functionality. */ 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, BulkAddTodosSchema } from "./models/Todo.js"; import { AddRulesSchema, GetRulesSchema } from "./models/Rule.js"; // Import services import { todoService } from "./services/TodoService.js"; import { ruleService } from "./services/RuleService.js"; import { databaseService } from "./services/DatabaseService.js"; // Import utilities import { createSuccessResponse, createErrorResponse, createErrorResponseWithUsage, formatTodo } from "./utils/formatters.js"; import { config } from "./config.js"; /** * Create the mini MCP server */ const server = new McpServer({ name: config.server.name, version: config.server.version, }); /** * Helper function to safely execute operations */ async function safeExecute<T>(operation: () => T | Promise<T>, errorMessage: string) { try { const result = await 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 */ server.tool( "create-todo", "Create a new todo item with auto-assigned task number", { title: z.string().min(1, "Title is required"), description: z.string().min(1, "Description is required"), filePath: z.string().min(1, "File path is required").optional(), }, async ({ title, description, filePath }) => { const result = await safeExecute(() => { const validatedData = CreateTodoSchema.parse({ title, description, filePath }); const newTodo = todoService.createTodo(validatedData); return `Todo ${newTodo.taskNumber}: ${newTodo.title}`; }, "Failed to create todo"); if (result instanceof Error) { return createErrorResponseWithUsage( "create-todo", result.message, `Parameters required: - title (string): Brief task name - description (string): Detailed task description - filePath (string, optional): Path to file to include content from Example: create-todo with title: "Implement auth", description: "Add JWT authentication to login endpoint"` ); } return createSuccessResponse(`✅ Created ${result}`); } ); /** * Tool 2: Get a specific todo by ID */ server.tool( "get-todo", "Get a specific todo by ID", { id: z.number().int().positive("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 createErrorResponseWithUsage( "get-todo", result.message, `Parameter required: - id (number): The unique todo ID to retrieve Example: get-todo with id: 5` ); } return createSuccessResponse(result); } ); /** * Tool 3: Update a todo */ server.tool( "update-todo", "Update a todo title or description", { id: z.number().int().positive("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 `Todo ${updatedTodo.taskNumber}: ${updatedTodo.title}`; }, "Failed to update todo"); if (result instanceof Error) { return createErrorResponseWithUsage( "update-todo", result.message, `Parameters required: - id (number): The unique todo ID to update - title (string, optional): New task title - description (string, optional): New task description Note: At least one of title or description must be provided Example: update-todo with id: 3, title: "Updated task name", description: "New description"` ); } return createSuccessResponse(`✅ Updated ${result}`); } ); /** * Tool 4: Complete a todo */ server.tool( "complete-todo", "Mark a todo as completed", { id: z.number().int().positive("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 `Todo ${completedTodo.taskNumber}: ${completedTodo.title}`; }, "Failed to complete todo"); if (result instanceof Error) { return createErrorResponseWithUsage( "complete-todo", result.message, `Parameter required: - id (number): The unique todo ID to mark as completed Example: complete-todo with id: 5` ); } return createSuccessResponse(`✅ ${result} completed`); } ); /** * Tool 5: Delete a todo */ server.tool( "delete-todo", "Delete a todo", { id: z.number().int().positive("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 6: Get next todo */ server.tool( "get-next-todo", "Get the next todo that needs to be completed (status != 'Done')", {}, async () => { const result = await safeExecute(() => { const nextTodo = todoService.getNextTodo(); if (!nextTodo) { return "No todos found that need to be completed."; } return formatTodo(nextTodo); }, "Failed to get next todo"); if (result instanceof Error) { return createErrorResponse(result.message); } return createSuccessResponse(result); } ); /** * Tool 7: Get next todo ID and task number */ server.tool( "get-next-todo-id", "Get the ID and task number of the next incomplete todo", {}, async () => { const result = await safeExecute(() => { const nextTodo = todoService.getNextTodoId(); if (nextTodo === null) { return "All todos have been completed"; } return `ID: ${nextTodo.id}, Task Number: ${nextTodo.taskNumber}`; }, "Failed to get next todo ID"); if (result instanceof Error) { return createErrorResponse(result.message); } return createSuccessResponse(result); } ); /** * Tool 8: Bulk add todos */ server.tool( "bulk-add-todos", "Create todos by reading file contents from a folder (recursively scans all files)", { folderPath: z.string().min(1, "Folder path is required"), clearAll: z.boolean().optional().default(false), }, async ({ folderPath, clearAll }) => { const result = await safeExecute(async () => { const validatedData = BulkAddTodosSchema.parse({ folderPath, clearAll }); const createdTodos = await todoService.bulkAddTodos(validatedData); const clearMessage = clearAll ? " (after clearing all existing todos)" : ""; return { todoCount: createdTodos.length, summary: `Created ${createdTodos.length} todos from files in ${folderPath}${clearMessage}` }; }, "Failed to bulk add todos"); if (result instanceof Error) { return createErrorResponseWithUsage( "bulk-add-todos", result.message, `Parameters required: - folderPath (string): Absolute path to folder containing files to process - clearAll (boolean, optional): Whether to clear existing todos first (default: false) Example: bulk-add-todos with folderPath: "/home/user/my-project", clearAll: false` ); } const { todoCount, summary } = result; return createSuccessResponse(`✅ ${summary}`); } ); /** * Tool 9: Clear all todos */ server.tool( "clear-all-todos", "Delete all todos from the database", {}, async () => { const result = await safeExecute(() => { const deletedCount = todoService.clearAllTodos(); return deletedCount; }, "Failed to clear all todos"); if (result instanceof Error) { return createErrorResponse(result.message); } return createSuccessResponse(`✅ Cleared ${result} todos from the database.`); } ); /** * Tool 10: Add rules from file */ server.tool( "add-rules", "Add rules by reading content from a file", { filePath: z.string().min(1, "File path is required"), clearAll: z.boolean().optional().default(false), }, async ({ filePath, clearAll }) => { const result = await safeExecute(async () => { const validatedData = AddRulesSchema.parse({ filePath, clearAll }); const createdRules = await ruleService.addRules(validatedData); const clearMessage = clearAll ? " (after clearing all existing rules)" : ""; return { ruleCount: createdRules.length, summary: `Created ${createdRules.length} rule from ${filePath}${clearMessage}` }; }, "Failed to add rules"); if (result instanceof Error) { return createErrorResponseWithUsage( "add-rules", result.message, `Parameters required: - filePath (string): Absolute path to file containing rule content - clearAll (boolean, optional): Whether to clear existing rules first (default: false) Example: add-rules with filePath: "/home/user/rules.txt", clearAll: false` ); } const { ruleCount, summary } = result; return createSuccessResponse(`✅ ${summary}`); } ); /** * Tool 11: Get rules */ server.tool( "get-rules", "Get all rules or a specific rule by ID", { id: z.number().int().positive("Invalid Rule ID").optional(), }, async ({ id }) => { const result = await safeExecute(() => { const validatedData = GetRulesSchema.parse({ id }); const rules = ruleService.getRules(validatedData); if (rules.length === 0) { if (id) { return `No rule found with ID ${id}`; } else { return "No rules found"; } } if (id) { const rule = rules[0]; return `**Rule ${rule.id}** Created: ${rule.createdAt} ${rule.filePath ? `Source: ${rule.filePath}` : ''} ${rule.description}`; } else { return rules.map(rule => `**Rule ${rule.id}** Created: ${rule.createdAt} ${rule.filePath ? `Source: ${rule.filePath}` : ''} ${rule.description} ---` ).join('\n'); } }, "Failed to get rules"); if (result instanceof Error) { return createErrorResponseWithUsage( "get-rules", result.message, `Parameter optional: - id (number, optional): The unique rule ID to retrieve. If not provided, returns all rules. Examples: - get-rules (returns all rules) - get-rules with id: 5 (returns rule with ID 5)` ); } return createSuccessResponse(result); } ); /** * Main function to start the server */ async function main() { console.error("Starting Mini Todo MCP Server..."); console.error(`SQLite database path: ${config.db.path}`); try { // Set up graceful shutdown 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 const transport = new StdioServerTransport(); await server.connect(transport); console.error("Mini Todo MCP Server running on stdio transport"); } catch (error) { console.error("Failed to start Mini Todo MCP Server:", error); databaseService.close(); process.exit(1); } } // Start the server main();