├── .gitignore ├── LICENSE ├── README.md ├── package.json ├── src └── index.ts └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | node_modules/ 3 | package-lock.json 4 | 5 | # Build output 6 | dist/ 7 | 8 | # Environment variables 9 | .env 10 | 11 | # IDE and OS files 12 | .DS_Store 13 | .vscode/ 14 | .idea/ 15 | 16 | # Logs 17 | *.log 18 | npm-debug.log* 19 | yarn-debug.log* 20 | yarn-error.log* -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Abhiram Nair 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Todoist MCP Server 2 | [![smithery badge](https://smithery.ai/badge/@abhiz123/todoist-mcp-server)](https://smithery.ai/server/@abhiz123/todoist-mcp-server) 3 | 4 | An MCP (Model Context Protocol) server implementation that integrates Claude with Todoist, enabling natural language task management. This server allows Claude to interact with your Todoist tasks using everyday language. 5 | 6 | 7 | Todoist Server MCP server 8 | 9 | 10 | ## Features 11 | 12 | * **Natural Language Task Management**: Create, update, complete, and delete tasks using everyday language 13 | * **Smart Task Search**: Find tasks using partial name matches 14 | * **Flexible Filtering**: Filter tasks by due date, priority, and other attributes 15 | * **Rich Task Details**: Support for descriptions, due dates, and priority levels 16 | * **Intuitive Error Handling**: Clear feedback for better user experience 17 | 18 | ## Installation 19 | 20 | ### Installing via Smithery 21 | 22 | To install Todoist MCP Server for Claude Desktop automatically via [Smithery](https://smithery.ai/server/@abhiz123/todoist-mcp-server): 23 | 24 | ```bash 25 | npx -y @smithery/cli install @abhiz123/todoist-mcp-server --client claude 26 | ``` 27 | 28 | ### Manual Installation 29 | ```bash 30 | npm install -g @abhiz123/todoist-mcp-server 31 | ``` 32 | 33 | ## Tools 34 | 35 | ### todoist_create_task 36 | Create new tasks with various attributes: 37 | * Required: content (task title) 38 | * Optional: description, due date, priority level (1-4) 39 | * Example: "Create task 'Team Meeting' with description 'Weekly sync' due tomorrow" 40 | 41 | ### todoist_get_tasks 42 | Retrieve and filter tasks: 43 | * Filter by due date, priority, or project 44 | * Natural language date filtering 45 | * Optional result limit 46 | * Example: "Show high priority tasks due this week" 47 | 48 | ### todoist_update_task 49 | Update existing tasks using natural language search: 50 | * Find tasks by partial name match 51 | * Update any task attribute (content, description, due date, priority) 52 | * Example: "Update meeting task to be due next Monday" 53 | 54 | ### todoist_complete_task 55 | Mark tasks as complete using natural language search: 56 | * Find tasks by partial name match 57 | * Confirm completion status 58 | * Example: "Mark the documentation task as complete" 59 | 60 | ### todoist_delete_task 61 | Remove tasks using natural language search: 62 | * Find and delete tasks by name 63 | * Confirmation messages 64 | * Example: "Delete the PR review task" 65 | 66 | ## Setup 67 | 68 | ### Getting a Todoist API Token 69 | 1. Log in to your Todoist account 70 | 2. Navigate to Settings → Integrations 71 | 3. Find your API token under "Developer" 72 | 73 | ### Usage with Claude Desktop 74 | 75 | Add to your `claude_desktop_config.json`: 76 | 77 | ```json 78 | { 79 | "mcpServers": { 80 | "todoist": { 81 | "command": "npx", 82 | "args": ["-y", "@abhiz123/todoist-mcp-server"], 83 | "env": { 84 | "TODOIST_API_TOKEN": "your_api_token_here" 85 | } 86 | } 87 | } 88 | } 89 | ``` 90 | 91 | ## Example Usage 92 | 93 | ### Creating Tasks 94 | ``` 95 | "Create task 'Team Meeting'" 96 | "Add task 'Review PR' due tomorrow at 2pm" 97 | "Create high priority task 'Fix bug' with description 'Critical performance issue'" 98 | ``` 99 | 100 | ### Getting Tasks 101 | ``` 102 | "Show all my tasks" 103 | "List tasks due today" 104 | "Get high priority tasks" 105 | "Show tasks due this week" 106 | ``` 107 | 108 | ### Updating Tasks 109 | ``` 110 | "Update documentation task to be due next week" 111 | "Change priority of bug fix task to urgent" 112 | "Add description to team meeting task" 113 | ``` 114 | 115 | ### Completing Tasks 116 | ``` 117 | "Mark the PR review task as complete" 118 | "Complete the documentation task" 119 | ``` 120 | 121 | ### Deleting Tasks 122 | ``` 123 | "Delete the PR review task" 124 | "Remove meeting prep task" 125 | ``` 126 | 127 | ## Development 128 | 129 | ### Building from source 130 | ```bash 131 | # Clone the repository 132 | git clone https://github.com/abhiz123/todoist-mcp-server.git 133 | 134 | # Navigate to directory 135 | cd todoist-mcp-server 136 | 137 | # Install dependencies 138 | npm install 139 | 140 | # Build the project 141 | npm run build 142 | ``` 143 | 144 | ## Contributing 145 | Contributions are welcome! Feel free to submit a Pull Request. 146 | 147 | ## License 148 | This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. 149 | 150 | ## Issues and Support 151 | If you encounter any issues or need support, please file an issue on the [GitHub repository](https://github.com/abhiz123/todoist-mcp-server/issues). -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@abhiz123/todoist-mcp-server", 3 | "version": "0.1.0", 4 | "description": "MCP server for Todoist API integration", 5 | "main": "dist/index.js", 6 | "types": "dist/index.d.ts", 7 | "type": "module", 8 | "bin": { 9 | "todoist-mcp-server": "dist/index.js" 10 | }, 11 | "files": [ 12 | "dist" 13 | ], 14 | "scripts": { 15 | "build": "tsc && shx chmod +x dist/*.js", 16 | "prepare": "npm run build", 17 | "watch": "tsc --watch" 18 | }, 19 | "repository": { 20 | "type": "git", 21 | "url": "git+https://github.com/abhiz123/todoist-mcp-server.git" 22 | }, 23 | "keywords": [ 24 | "mcp", 25 | "todoist", 26 | "claude", 27 | "ai", 28 | "task-management" 29 | ], 30 | "author": "abhiz123", 31 | "license": "MIT", 32 | "bugs": { 33 | "url": "https://github.com/abhiz123/todoist-mcp-server/issues" 34 | }, 35 | "homepage": "https://github.com/abhiz123/todoist-mcp-server#readme", 36 | "dependencies": { 37 | "@doist/todoist-api-typescript": "^3.0.3", 38 | "@modelcontextprotocol/sdk": "0.5.0" 39 | }, 40 | "devDependencies": { 41 | "@types/node": "^22.10.1", 42 | "shx": "^0.3.4", 43 | "typescript": "^5.7.2" 44 | } 45 | } -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import { Server } from "@modelcontextprotocol/sdk/server/index.js"; 4 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; 5 | import { 6 | CallToolRequestSchema, 7 | ListToolsRequestSchema, 8 | Tool, 9 | } from "@modelcontextprotocol/sdk/types.js"; 10 | import { TodoistApi } from "@doist/todoist-api-typescript"; 11 | 12 | // Define tools 13 | const CREATE_TASK_TOOL: Tool = { 14 | name: "todoist_create_task", 15 | description: "Create a new task in Todoist with optional description, due date, and priority", 16 | inputSchema: { 17 | type: "object", 18 | properties: { 19 | content: { 20 | type: "string", 21 | description: "The content/title of the task" 22 | }, 23 | description: { 24 | type: "string", 25 | description: "Detailed description of the task (optional)" 26 | }, 27 | due_string: { 28 | type: "string", 29 | description: "Natural language due date like 'tomorrow', 'next Monday', 'Jan 23' (optional)" 30 | }, 31 | priority: { 32 | type: "number", 33 | description: "Task priority from 1 (normal) to 4 (urgent) (optional)", 34 | enum: [1, 2, 3, 4] 35 | } 36 | }, 37 | required: ["content"] 38 | } 39 | }; 40 | 41 | const GET_TASKS_TOOL: Tool = { 42 | name: "todoist_get_tasks", 43 | description: "Get a list of tasks from Todoist with various filters", 44 | inputSchema: { 45 | type: "object", 46 | properties: { 47 | project_id: { 48 | type: "string", 49 | description: "Filter tasks by project ID (optional)" 50 | }, 51 | filter: { 52 | type: "string", 53 | description: "Natural language filter like 'today', 'tomorrow', 'next week', 'priority 1', 'overdue' (optional)" 54 | }, 55 | priority: { 56 | type: "number", 57 | description: "Filter by priority level (1-4) (optional)", 58 | enum: [1, 2, 3, 4] 59 | }, 60 | limit: { 61 | type: "number", 62 | description: "Maximum number of tasks to return (optional)", 63 | default: 10 64 | } 65 | } 66 | } 67 | }; 68 | 69 | const UPDATE_TASK_TOOL: Tool = { 70 | name: "todoist_update_task", 71 | description: "Update an existing task in Todoist by searching for it by name and then updating it", 72 | inputSchema: { 73 | type: "object", 74 | properties: { 75 | task_name: { 76 | type: "string", 77 | description: "Name/content of the task to search for and update" 78 | }, 79 | content: { 80 | type: "string", 81 | description: "New content/title for the task (optional)" 82 | }, 83 | description: { 84 | type: "string", 85 | description: "New description for the task (optional)" 86 | }, 87 | due_string: { 88 | type: "string", 89 | description: "New due date in natural language like 'tomorrow', 'next Monday' (optional)" 90 | }, 91 | priority: { 92 | type: "number", 93 | description: "New priority level from 1 (normal) to 4 (urgent) (optional)", 94 | enum: [1, 2, 3, 4] 95 | } 96 | }, 97 | required: ["task_name"] 98 | } 99 | }; 100 | 101 | const DELETE_TASK_TOOL: Tool = { 102 | name: "todoist_delete_task", 103 | description: "Delete a task from Todoist by searching for it by name", 104 | inputSchema: { 105 | type: "object", 106 | properties: { 107 | task_name: { 108 | type: "string", 109 | description: "Name/content of the task to search for and delete" 110 | } 111 | }, 112 | required: ["task_name"] 113 | } 114 | }; 115 | 116 | const COMPLETE_TASK_TOOL: Tool = { 117 | name: "todoist_complete_task", 118 | description: "Mark a task as complete by searching for it by name", 119 | inputSchema: { 120 | type: "object", 121 | properties: { 122 | task_name: { 123 | type: "string", 124 | description: "Name/content of the task to search for and complete" 125 | } 126 | }, 127 | required: ["task_name"] 128 | } 129 | }; 130 | 131 | // Server implementation 132 | const server = new Server( 133 | { 134 | name: "todoist-mcp-server", 135 | version: "0.1.0", 136 | }, 137 | { 138 | capabilities: { 139 | tools: {}, 140 | }, 141 | }, 142 | ); 143 | 144 | // Check for API token 145 | const TODOIST_API_TOKEN = process.env.TODOIST_API_TOKEN!; 146 | if (!TODOIST_API_TOKEN) { 147 | console.error("Error: TODOIST_API_TOKEN environment variable is required"); 148 | process.exit(1); 149 | } 150 | 151 | // Initialize Todoist client 152 | const todoistClient = new TodoistApi(TODOIST_API_TOKEN); 153 | 154 | // Type guards for arguments 155 | function isCreateTaskArgs(args: unknown): args is { 156 | content: string; 157 | description?: string; 158 | due_string?: string; 159 | priority?: number; 160 | } { 161 | return ( 162 | typeof args === "object" && 163 | args !== null && 164 | "content" in args && 165 | typeof (args as { content: string }).content === "string" 166 | ); 167 | } 168 | 169 | function isGetTasksArgs(args: unknown): args is { 170 | project_id?: string; 171 | filter?: string; 172 | priority?: number; 173 | limit?: number; 174 | } { 175 | return ( 176 | typeof args === "object" && 177 | args !== null 178 | ); 179 | } 180 | 181 | function isUpdateTaskArgs(args: unknown): args is { 182 | task_name: string; 183 | content?: string; 184 | description?: string; 185 | due_string?: string; 186 | priority?: number; 187 | } { 188 | return ( 189 | typeof args === "object" && 190 | args !== null && 191 | "task_name" in args && 192 | typeof (args as { task_name: string }).task_name === "string" 193 | ); 194 | } 195 | 196 | function isDeleteTaskArgs(args: unknown): args is { 197 | task_name: string; 198 | } { 199 | return ( 200 | typeof args === "object" && 201 | args !== null && 202 | "task_name" in args && 203 | typeof (args as { task_name: string }).task_name === "string" 204 | ); 205 | } 206 | 207 | function isCompleteTaskArgs(args: unknown): args is { 208 | task_name: string; 209 | } { 210 | return ( 211 | typeof args === "object" && 212 | args !== null && 213 | "task_name" in args && 214 | typeof (args as { task_name: string }).task_name === "string" 215 | ); 216 | } 217 | 218 | // Tool handlers 219 | server.setRequestHandler(ListToolsRequestSchema, async () => ({ 220 | tools: [CREATE_TASK_TOOL, GET_TASKS_TOOL, UPDATE_TASK_TOOL, DELETE_TASK_TOOL, COMPLETE_TASK_TOOL], 221 | })); 222 | 223 | server.setRequestHandler(CallToolRequestSchema, async (request) => { 224 | try { 225 | const { name, arguments: args } = request.params; 226 | 227 | if (!args) { 228 | throw new Error("No arguments provided"); 229 | } 230 | 231 | if (name === "todoist_create_task") { 232 | if (!isCreateTaskArgs(args)) { 233 | throw new Error("Invalid arguments for todoist_create_task"); 234 | } 235 | const task = await todoistClient.addTask({ 236 | content: args.content, 237 | description: args.description, 238 | dueString: args.due_string, 239 | priority: args.priority 240 | }); 241 | return { 242 | content: [{ 243 | type: "text", 244 | text: `Task created:\nTitle: ${task.content}${task.description ? `\nDescription: ${task.description}` : ''}${task.due ? `\nDue: ${task.due.string}` : ''}${task.priority ? `\nPriority: ${task.priority}` : ''}` 245 | }], 246 | isError: false, 247 | }; 248 | } 249 | 250 | if (name === "todoist_get_tasks") { 251 | if (!isGetTasksArgs(args)) { 252 | throw new Error("Invalid arguments for todoist_get_tasks"); 253 | } 254 | 255 | // Only pass filter if at least one filtering parameter is provided 256 | const apiParams: any = {}; 257 | if (args.project_id) { 258 | apiParams.projectId = args.project_id; 259 | } 260 | if (args.filter) { 261 | apiParams.filter = args.filter; 262 | } 263 | // If no filters provided, default to showing all tasks 264 | const tasks = await todoistClient.getTasks(Object.keys(apiParams).length > 0 ? apiParams : undefined); 265 | 266 | // Apply additional filters 267 | let filteredTasks = tasks; 268 | if (args.priority) { 269 | filteredTasks = filteredTasks.filter(task => task.priority === args.priority); 270 | } 271 | 272 | // Apply limit 273 | if (args.limit && args.limit > 0) { 274 | filteredTasks = filteredTasks.slice(0, args.limit); 275 | } 276 | 277 | const taskList = filteredTasks.map(task => 278 | `- ${task.content}${task.description ? `\n Description: ${task.description}` : ''}${task.due ? `\n Due: ${task.due.string}` : ''}${task.priority ? `\n Priority: ${task.priority}` : ''}` 279 | ).join('\n\n'); 280 | 281 | return { 282 | content: [{ 283 | type: "text", 284 | text: filteredTasks.length > 0 ? taskList : "No tasks found matching the criteria" 285 | }], 286 | isError: false, 287 | }; 288 | } 289 | 290 | if (name === "todoist_update_task") { 291 | if (!isUpdateTaskArgs(args)) { 292 | throw new Error("Invalid arguments for todoist_update_task"); 293 | } 294 | 295 | // First, search for the task 296 | const tasks = await todoistClient.getTasks(); 297 | const matchingTask = tasks.find(task => 298 | task.content.toLowerCase().includes(args.task_name.toLowerCase()) 299 | ); 300 | 301 | if (!matchingTask) { 302 | return { 303 | content: [{ 304 | type: "text", 305 | text: `Could not find a task matching "${args.task_name}"` 306 | }], 307 | isError: true, 308 | }; 309 | } 310 | 311 | // Build update data 312 | const updateData: any = {}; 313 | if (args.content) updateData.content = args.content; 314 | if (args.description) updateData.description = args.description; 315 | if (args.due_string) updateData.dueString = args.due_string; 316 | if (args.priority) updateData.priority = args.priority; 317 | 318 | const updatedTask = await todoistClient.updateTask(matchingTask.id, updateData); 319 | 320 | return { 321 | content: [{ 322 | type: "text", 323 | text: `Task "${matchingTask.content}" updated:\nNew Title: ${updatedTask.content}${updatedTask.description ? `\nNew Description: ${updatedTask.description}` : ''}${updatedTask.due ? `\nNew Due Date: ${updatedTask.due.string}` : ''}${updatedTask.priority ? `\nNew Priority: ${updatedTask.priority}` : ''}` 324 | }], 325 | isError: false, 326 | }; 327 | } 328 | 329 | if (name === "todoist_delete_task") { 330 | if (!isDeleteTaskArgs(args)) { 331 | throw new Error("Invalid arguments for todoist_delete_task"); 332 | } 333 | 334 | // First, search for the task 335 | const tasks = await todoistClient.getTasks(); 336 | const matchingTask = tasks.find(task => 337 | task.content.toLowerCase().includes(args.task_name.toLowerCase()) 338 | ); 339 | 340 | if (!matchingTask) { 341 | return { 342 | content: [{ 343 | type: "text", 344 | text: `Could not find a task matching "${args.task_name}"` 345 | }], 346 | isError: true, 347 | }; 348 | } 349 | 350 | // Delete the task 351 | await todoistClient.deleteTask(matchingTask.id); 352 | 353 | return { 354 | content: [{ 355 | type: "text", 356 | text: `Successfully deleted task: "${matchingTask.content}"` 357 | }], 358 | isError: false, 359 | }; 360 | } 361 | 362 | if (name === "todoist_complete_task") { 363 | if (!isCompleteTaskArgs(args)) { 364 | throw new Error("Invalid arguments for todoist_complete_task"); 365 | } 366 | 367 | // First, search for the task 368 | const tasks = await todoistClient.getTasks(); 369 | const matchingTask = tasks.find(task => 370 | task.content.toLowerCase().includes(args.task_name.toLowerCase()) 371 | ); 372 | 373 | if (!matchingTask) { 374 | return { 375 | content: [{ 376 | type: "text", 377 | text: `Could not find a task matching "${args.task_name}"` 378 | }], 379 | isError: true, 380 | }; 381 | } 382 | 383 | // Complete the task 384 | await todoistClient.closeTask(matchingTask.id); 385 | 386 | return { 387 | content: [{ 388 | type: "text", 389 | text: `Successfully completed task: "${matchingTask.content}"` 390 | }], 391 | isError: false, 392 | }; 393 | } 394 | 395 | return { 396 | content: [{ type: "text", text: `Unknown tool: ${name}` }], 397 | isError: true, 398 | }; 399 | } catch (error) { 400 | return { 401 | content: [ 402 | { 403 | type: "text", 404 | text: `Error: ${error instanceof Error ? error.message : String(error)}`, 405 | }, 406 | ], 407 | isError: true, 408 | }; 409 | } 410 | }); 411 | 412 | async function runServer() { 413 | const transport = new StdioServerTransport(); 414 | await server.connect(transport); 415 | console.error("Todoist MCP Server running on stdio"); 416 | } 417 | 418 | runServer().catch((error) => { 419 | console.error("Fatal error running server:", error); 420 | process.exit(1); 421 | }); -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "module": "ES2022", 5 | "moduleResolution": "bundler", 6 | "outDir": "./dist", 7 | "strict": true, 8 | "esModuleInterop": true, 9 | "skipLibCheck": true, 10 | "declaration": true 11 | }, 12 | "include": ["src/index.ts"], 13 | "exclude": ["node_modules", "dist"] 14 | } --------------------------------------------------------------------------------